diff --git a/.vscode/launch.json b/.vscode/launch.json index fa5ed59..768448f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,30 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "(gdb) Launch", + "type": "cppdbg", + "request": "launch", + "program": "${command:cmake.launchTargetPath}", + "args": [], + "stopAtEntry": false, + "cwd": "${fileDirname}", + "environment": [], + "externalConsole": false, + "MIMode": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + }, + { + "description": "Set Disassembly Flavor to Intel", + "text": "-gdb-set disassembly-flavor intel", + "ignoreFailures": true + } + ] + }, { "name": "(Windows) Launch", "type": "cppvsdbg", diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..14701dc --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "cmake.coverageInfoFiles": [ + "${workspaceFolder}/build/coverage_filtered.info" + ], + // NOTE: The 'lcov' target is only defined when lcov is installed and the build type is Debug. + // In other configurations, "Run with coverage" may fail if this target is not available. + "cmake.postRunCoverageTarget": "lcov", + "testing.coverageToolbarEnabled": true, + "testing.defaultGutterClickAction": "runWithCoverage" +} diff --git a/CMakeLists.txt b/CMakeLists.txt index 2ab8525..73a022a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,12 @@ cmake_minimum_required(VERSION 3.25) project(blit) +# Enable generation of compile_commands.json for tools like cppcheck and +# clang-tidy. This will allow these tools to understand the compilation +# commands and flags used for the project, which is good for accurate +# analysis and reporting. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + add_library(blit # Source files for the blit library. ${CMAKE_CURRENT_SOURCE_DIR}/src/blit/rop2.c @@ -22,6 +28,7 @@ create_test_sourcelist(test_sources test_driver.c test/pat.c test/left_shift_edge.c + test/extra_scan_count.c ) # Add a test executable that links against the library. @@ -34,16 +41,18 @@ target_link_libraries(test_runner PRIVATE blit) add_test(NAME pat COMMAND test_runner test/pat) add_test(NAME left_shift_edge COMMAND test_runner test/left_shift_edge) +add_test(NAME extra_scan_count COMMAND test_runner test/extra_scan_count) -# https://cmake.org/cmake/help/latest/module/FindDoxygen.html -# https://www.mcternan.me.uk/mscgen/ -find_package(Doxygen OPTIONAL_COMPONENTS dot mscgen dia) -if(DOXYGEN_FOUND) - set(DOXYGEN_USE_MDFILE_AS_MAINPAGE README.md) - set(DOXYGEN_OPTIMIZE_OUTPUT_FOR_C YES) - set(DOXYGEN_SOURCE_BROWSER YES) - set(DOXYGEN_EXCLUDE ${CMAKE_BINARY_DIR}) - doxygen_add_docs(doxy ${CMAKE_SOURCE_DIR} - COMMENT "Generating API documentation with Doxygen" - ) +list(APPEND CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/cmake) +include(lcov) +if(LCOV) + # Add the necessary compiler and linker flags to enable code coverage analysis. + # Use generator expressions so coverage is only enabled for Debug configuration, + # which works for both single- and multi-config generators. + foreach(target IN ITEMS blit test_runner) + target_compile_options(${target} PRIVATE $<$:--coverage>) + target_link_options(${target} PRIVATE $<$:--coverage>) + endforeach() endif() +include(doxy) +include(cppcheck) diff --git a/cmake/cppcheck.cmake b/cmake/cppcheck.cmake new file mode 100644 index 0000000..bf67dd2 --- /dev/null +++ b/cmake/cppcheck.cmake @@ -0,0 +1,22 @@ +# Add custom commands for code analysis. Enable cppcheck for Debug build type. +# This will run cppcheck only when building the Debug configuration, using configuration-aware logic. +find_program(CPPCHECK cppcheck) +if(CPPCHECK) + add_custom_target(cppcheck + COMMAND $<$:${CPPCHECK}> + $<$:--project=${CMAKE_BINARY_DIR}/compile_commands.json> + $<$:--output-file=${CMAKE_BINARY_DIR}/cppcheck_report.xml> + $<$:--xml> + $<$:--enable=all> + $<$:--enable=warning,style,performance,portability,information> + $<$:--suppressions-list=${CMAKE_SOURCE_DIR}/cppcheck_suppressions.txt> + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + COMMENT "Running cppcheck for code analysis (Debug configuration only)" + # Use VERBATIM to ensure the command is executed as written. + # This is useful for handling paths with spaces or special characters. + VERBATIM + ) + set_property(TARGET cppcheck PROPERTY FOLDER "Analysis") +else() + message(STATUS "Cppcheck not found; code analysis target will not be created") +endif() diff --git a/cmake/doxy.cmake b/cmake/doxy.cmake new file mode 100644 index 0000000..87655ec --- /dev/null +++ b/cmake/doxy.cmake @@ -0,0 +1,13 @@ +# https://cmake.org/cmake/help/latest/module/FindDoxygen.html +# https://www.mcternan.me.uk/mscgen/ +find_package(Doxygen OPTIONAL_COMPONENTS dot mscgen dia) +if(DOXYGEN_FOUND) + set(DOXYGEN_USE_MDFILE_AS_MAINPAGE ${CMAKE_SOURCE_DIR}/README.md) + set(DOXYGEN_USE_MATHJAX YES) + set(DOXYGEN_OPTIMIZE_OUTPUT_FOR_C YES) + set(DOXYGEN_SOURCE_BROWSER YES) + set(DOXYGEN_EXCLUDE ${CMAKE_BINARY_DIR}) + doxygen_add_docs(doxy ${CMAKE_SOURCE_DIR} + COMMENT "Generating API documentation with Doxygen" + ) +endif() diff --git a/cmake/lcov.cmake b/cmake/lcov.cmake new file mode 100644 index 0000000..365a383 --- /dev/null +++ b/cmake/lcov.cmake @@ -0,0 +1,46 @@ +# Add custom commands for code coverage analysis. Enable lcov and genhtml if +# they are found. Only enable code coverage analysis for Debug build type, as it +# is typically used for development and testing. Code coverage analysis can add +# overhead to the build and runtime, so it is generally not recommended for +# Release builds. +# +# If lcov is found, add the coverage compiler and linker flags to enable code +# coverage analysis. +# +# Note that find_program will return the path to the executable if found, or +# NOTFOUND if not found. It will also cache the result, so subsequent calls will +# return the cached value. This allows us to check if lcov is available and +# conditionally add code coverage targets. +find_program(LCOV lcov) +if(LCOV) + # Add custom targets for code coverage analysis using lcov and genhtml. + # These targets will run the tests, capture coverage data, and generate an HTML report. + add_custom_target(ctest + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure + DEPENDS test_runner + ) + add_custom_target(lcov + COMMAND ${LCOV} --capture --directory . --output-file coverage.info + COMMAND ${LCOV} --remove coverage.info '/usr/*' '*/test*' --output-file coverage_filtered.info + DEPENDS ctest + ) + set_property(TARGET lcov PROPERTY FOLDER "Analysis") + + # If genhtml is found, add a custom target to generate an HTML report from the + # filtered coverage data. This will create a coverage_report directory with an + # index.html file that can be opened in a web browser to view the coverage report. + find_program(GENHTML genhtml) + if(GENHTML) + message(STATUS "lcov and genhtml found, code coverage targets will be available") + add_custom_target(genhtml + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + COMMAND ${GENHTML} coverage_filtered.info --output-directory coverage_report + COMMAND ${CMAKE_COMMAND} -E echo "Coverage report generated in: ${CMAKE_BINARY_DIR}/coverage_report/index.html" + DEPENDS lcov + ) + set_property(TARGET genhtml PROPERTY FOLDER "Analysis") + else() + message(STATUS "genhtml not found, HTML code coverage report target will not be available") + endif() +endif() diff --git a/cppcheck_suppressions.txt b/cppcheck_suppressions.txt new file mode 100644 index 0000000..e69de29 diff --git a/inc/blit/rgn1.h b/inc/blit/rgn1.h index 9818bdf..a4a1b3b 100644 --- a/inc/blit/rgn1.h +++ b/inc/blit/rgn1.h @@ -7,7 +7,7 @@ * \brief One-dimensional region structure. * \details This header file defines the \c blit_rgn1 structure, which * represents a one-dimensional region with an origin, extent, and source - * origin. It also provides inline functions for normalising, slipping, and + * origin. It also provides inline functions for normalising, moving, and * clipping the region. */ @@ -40,7 +40,7 @@ struct blit_rgn1 { /*! * \brief Normalise a one-dimensional region. - * \details This function normalizes a one-dimensional region represented by the + * \details This function normalises a one-dimensional region represented by the * \c blit_rgn1 structure. If the extent of the region is negative, it * adjusts the origin and origin_source accordingly to ensure that the * extent is non-negative. @@ -62,18 +62,18 @@ static inline void blit_rgn1_norm(struct blit_rgn1 *rgn1) { } /*! - * \brief Slip a one-dimensional region into positive space. + * \brief Move a one-dimensional region into positive space. * \details This function adjusts a one-dimensional region represented by the * \c blit_rgn1 structure to ensure that both the origin and source origin are * non-negative. If either the origin or source origin is negative, the region - * is "slipped" into positive space by adjusting the origins and reducing the + * is "moved" into positive space by adjusting the origins and reducing the * extent accordingly. If the entire region is outside positive space, the * function returns false. - * \param rgn1 Pointer to the \c blit_rgn1 structure to slip. - * \retval true if the region was successfully slipped into positive space. + * \param rgn1 Pointer to the \c blit_rgn1 structure to move. + * \retval true if the region was successfully moved into positive space. * \retval false if the entire region is outside positive space. */ -static inline bool blit_rgn1_slip(struct blit_rgn1 *rgn1) { +static inline bool blit_rgn1_move(struct blit_rgn1 *rgn1) { int offset = rgn1->origin < 0 ? (rgn1->origin < rgn1->origin_source ? -rgn1->origin : -rgn1->origin_source) diff --git a/inc/blit/rop2.h b/inc/blit/rop2.h index fca9e2f..9ac3360 100644 --- a/inc/blit/rop2.h +++ b/inc/blit/rop2.h @@ -72,11 +72,17 @@ enum blit_rop2 { * \param y Pointer to the one-dimensional region structure for the y-axis. * \param source Pointer to the source scan structure. * \param rop2 The raster operation code. - * \return true if the operation was successful, false otherwise. + * \return The number of logic operations performed. This includes all + * operations performed on the destination scan space, including those + * that may not have resulted in a change to the pixel values (e.g., + * operations that resulted in the same value being written back to the + * destination). It includes partial operations on the first and last + * bytes of each scanline, which may have been masked to only affect + * certain bits. The count reflects the total number of operations + * attempted based on the region and raster operation specified, + * regardless of the actual changes made to the pixel data. */ -bool blit_rgn1_rop2(struct blit_scan *result, struct blit_rgn1 *x, - struct blit_rgn1 *y, const struct blit_scan *source, - enum blit_rop2 rop2); +int blit_rgn1_rop2(struct blit_scan *result, struct blit_rgn1 *x, struct blit_rgn1 *y, const struct blit_scan *source, enum blit_rop2 rop2); /*! * \brief Convenience inline function for performing raster operations. @@ -98,10 +104,14 @@ bool blit_rgn1_rop2(struct blit_scan *result, struct blit_rgn1 *x, * \param x_source The x-coordinate of the origin of the region in the source. * \param y_source The y-coordinate of the origin of the region in the source. * \param rop2 The raster operation code to apply. + * \return The number of logic operations performed. */ -bool blit_rop2(struct blit_scan *result, const int x, const int y, - const int x_extent, const int y_extent, - const struct blit_scan *source, const int x_source, - const int y_source, enum blit_rop2 rop2); +int blit_rop2(struct blit_scan *result, + /* destination region */ + const int x, const int y, const int x_extent, const int y_extent, + /* source region */ + const struct blit_scan *source, const int x_source, const int y_source, + /* raster operation */ + enum blit_rop2 rop2); #endif /* __BLIT_ROP2_H__ */ diff --git a/src/blit/rop2.c b/src/blit/rop2.c index 560a047..55f8d4f 100644 --- a/src/blit/rop2.c +++ b/src/blit/rop2.c @@ -32,8 +32,7 @@ * (`fetch`) and the destination operand (`store`). The function returns an * 8-bit result of the raster operation. */ -typedef blit_scanline_t (*blit_rop2_func_t)(blit_scanline_t fetch, - blit_scanline_t store); +typedef blit_scanline_t (*blit_rop2_func_t)(blit_scanline_t fetch, blit_scanline_t store); /*! * \brief Macro to define a raster operation function. @@ -66,13 +65,9 @@ typedef blit_scanline_t (*blit_rop2_func_t)(blit_scanline_t fetch, * \param revPolish The reverse polish notation name of the raster operation. * \param x The expression defining the raster operation using D and S. */ -#define ROP_REV_POLISH(revPolish, x) \ - static blit_scanline_t rop##revPolish(blit_scanline_t fetch, \ - blit_scanline_t store); \ - blit_scanline_t rop##revPolish(blit_scanline_t fetch, \ - blit_scanline_t store) { \ - return x; \ - } +#define ROP_REV_POLISH(revPolish, x) \ + static blit_scanline_t rop##revPolish(blit_scanline_t fetch, blit_scanline_t store); \ + blit_scanline_t rop##revPolish(blit_scanline_t fetch, blit_scanline_t store) { return x; } /*! * \brief Raster operation: 0. @@ -167,10 +162,8 @@ ROP_REV_POLISH(1, 0xffU); * functions. Each function implements a specific raster operation defined * using bitwise operations on the source (S) and destination (D) operands. */ -static blit_rop2_func_t rop2_func[] = {&rop0, &ropDSon, &ropDSna, &ropSn, - &ropSDna, &ropDn, &ropDSx, &ropDSan, - &ropDSa, &ropDSxn, &ropD, &ropDSno, - &ropS, &ropSDno, &ropDSo, &rop1}; +static blit_rop2_func_t rop2_func[] = {&rop0, &ropDSon, &ropDSna, &ropSn, &ropSDna, &ropDn, &ropDSx, &ropDSan, + &ropDSa, &ropDSxn, &ropD, &ropDSno, &ropS, &ropSDno, &ropDSo, &rop1}; /*! * \brief Perform raster operation with masking and store the result. @@ -179,9 +172,7 @@ static blit_rop2_func_t rop2_func[] = {&rop0, &ropDSon, &ropDSna, &ropSn, * \param mask The mask to apply to the operation. * \param store Pointer to the destination operand. */ -static void fetch_logic_mask_store(struct blit_phase_align *align, - enum blit_rop2 rop2, blit_scanline_t mask, - blit_scanline_t *store); +static void fetch_logic_mask_store(struct blit_phase_align *align, enum blit_rop2 rop2, blit_scanline_t mask, blit_scanline_t *store); /*! * \brief Perform raster operation and store the result. @@ -189,31 +180,26 @@ static void fetch_logic_mask_store(struct blit_phase_align *align, * \param rop2 The raster operation code. * \param store Pointer to the destination operand. */ -static void fetch_logic_store(struct blit_phase_align *align, - enum blit_rop2 rop2, blit_scanline_t *store); +static void fetch_logic_store(struct blit_phase_align *align, enum blit_rop2 rop2, blit_scanline_t *store); -bool blit_rgn1_rop2(struct blit_scan *result, struct blit_rgn1 *x, - struct blit_rgn1 *y, const struct blit_scan *source, - enum blit_rop2 rop2) { +int blit_rgn1_rop2(struct blit_scan *result, struct blit_rgn1 *x, struct blit_rgn1 *y, const struct blit_scan *source, enum blit_rop2 rop2) { /* - * Normalise, slip, and clip the x region. The regions are first normalised to - * ensure that their extents are non-negative. Then, they are slipped to + * Normalise, move, and clip the x region. The regions are first normalised to + * ensure that their extents are non-negative. Then, they are moved to * ensure that their origins are non-negative. Finally, they are clipped to * ensure that they fit within the bounds of the destination and source scan * structures. */ blit_rgn1_norm(x); - if (!blit_rgn1_slip(x) || !blit_rgn1_clip(x, result->width - x->origin) || - !blit_rgn1_clip(x, source->width - x->origin_source)) - return false; + if (!blit_rgn1_move(x) || !blit_rgn1_clip(x, result->width - x->origin) || !blit_rgn1_clip(x, source->width - x->origin_source)) + return 0; /* - * Normalise, slip, and clip the y region. + * Normalise, move, and clip the y region. */ blit_rgn1_norm(y); - if (!blit_rgn1_slip(y) || !blit_rgn1_clip(y, result->height - y->origin) || - !blit_rgn1_clip(y, source->height - y->origin_source)) - return false; + if (!blit_rgn1_move(y) || !blit_rgn1_clip(y, result->height - y->origin) || !blit_rgn1_clip(y, source->height - y->origin_source)) + return 0; /* * Compute some important values up front to avoid doing it inside the bit @@ -252,9 +238,7 @@ bool blit_rgn1_rop2(struct blit_scan *result, struct blit_rgn1 *x, * get out of sync! Keep them in step! */ struct blit_phase_align align; - blit_phase_align_start( - &align, x->origin, x->origin_source & 7, - blit_scan_find(source, x->origin_source, y->origin_source)); + blit_phase_align_start(&align, x->origin, x->origin_source & 7, blit_scan_find(source, x->origin_source, y->origin_source)); /* * Perform the bit block transfer using the specified raster operation. The @@ -272,12 +256,13 @@ bool blit_rgn1_rop2(struct blit_scan *result, struct blit_rgn1 *x, * of the phase alignment structure ensures that the source data is correctly * aligned with the destination data during the transfer. */ - int extent = y->extent; + int extent = y->extent, logic_count = 0; if (extra_scan_count == 0) { const blit_scanline_t scan_mask = scan_origin_mask & scan_extent_mask; while (extent--) { blit_phase_align_prefetch(&align); fetch_logic_mask_store(&align, rop2, scan_mask, store++); + logic_count++; store += offset; align.store += offset_source; } @@ -285,22 +270,28 @@ bool blit_rgn1_rop2(struct blit_scan *result, struct blit_rgn1 *x, while (extent--) { blit_phase_align_prefetch(&align); fetch_logic_mask_store(&align, rop2, scan_origin_mask, store++); + logic_count++; int extra = extra_scan_count; while (--extra) { fetch_logic_store(&align, rop2, store++); + logic_count++; } fetch_logic_mask_store(&align, rop2, scan_extent_mask, store++); + logic_count++; store += offset; align.store += offset_source; } } - return true; + return logic_count; } -bool blit_rop2(struct blit_scan *result, const int x, const int y, - const int x_extent, const int y_extent, - const struct blit_scan *source, const int x_source, - const int y_source, enum blit_rop2 rop2) { +int blit_rop2(struct blit_scan *result, + /* destination region */ + const int x, const int y, const int x_extent, const int y_extent, + /* source region */ + const struct blit_scan *source, const int x_source, const int y_source, + /* raster operation */ + enum blit_rop2 rop2) { /* * Build the one-dimensional region structures for x and y axes from the * arguments. These structures define the origin and extent of the region to @@ -320,13 +311,10 @@ bool blit_rop2(struct blit_scan *result, const int x, const int y, return blit_rgn1_rop2(result, &x_rgn1, &y_rgn1, source, rop2); } -void fetch_logic_mask_store(struct blit_phase_align *align, enum blit_rop2 rop2, - blit_scanline_t mask, blit_scanline_t *store) { - *store = (*store & ~mask) | - (mask & rop2_func[rop2](blit_phase_align_fetch(align), *store)); +void fetch_logic_mask_store(struct blit_phase_align *align, enum blit_rop2 rop2, blit_scanline_t mask, blit_scanline_t *store) { + *store = (*store & ~mask) | (mask & rop2_func[rop2](blit_phase_align_fetch(align), *store)); } -void fetch_logic_store(struct blit_phase_align *align, enum blit_rop2 rop2, - blit_scanline_t *store) { +void fetch_logic_store(struct blit_phase_align *align, enum blit_rop2 rop2, blit_scanline_t *store) { *store = rop2_func[rop2](blit_phase_align_fetch(align), *store); } diff --git a/test/extra_scan_count.c b/test/extra_scan_count.c new file mode 100644 index 0000000..0a88120 --- /dev/null +++ b/test/extra_scan_count.c @@ -0,0 +1,68 @@ +#include + +#include +#include +#include + +int test_extra_scan_count() { + blit_scanline_t pat_store[] = { + 0x55U, 0x55U, 0x55U, // .# (alternating) + 0xAAU, 0xAAU, 0xAAU, // #. (alternating) + 0x55U, 0x55U, 0x55U, // .# (alternating) + 0xAAU, 0xAAU, 0xAAU, // #. (alternating) + 0x55U, 0x55U, 0x55U, // .# (alternating) + 0xAAU, 0xAAU, 0xAAU, // #. (alternating) + 0x55U, 0x55U, 0x55U, // .# (alternating) + 0xAAU, 0xAAU, 0xAAU, // #. (alternating) + 0x55U, 0x55U, 0x55U, // .# (alternating) + 0xAAU, 0xAAU, 0xAAU, // #. (alternating) + 0x55U, 0x55U, 0x55U, // .# (alternating) + 0xAAU, 0xAAU, 0xAAU, // #. (alternating) + }; + struct blit_scan pat = { + .store = pat_store, + .width = 18, + .height = 12, + .stride = 3, + }; + BLIT_SCAN_DEFINE(image, 48, 24); + + for (int y = 0; y < image.height; y += pat.height) { + for (int x = 0; x < image.width; x += pat.width) { + struct blit_rgn1 x_rgn1 = { + .origin = x, + .extent = pat.width, + .origin_source = 0, + }; + struct blit_rgn1 y_rgn1 = { + .origin = y, + .extent = pat.height, + .origin_source = 0, + }; + assert(blit_rgn1_rop2(&image, &x_rgn1, &y_rgn1, &pat, blit_rop2_copy)); + } + } + + for (int x = 0; x < image.width; ++x) { + for (int y = 0; y < image.height; ++y) { + BLIT_SCAN_DEFINE(bit, 1, 1); + struct blit_rgn1 x_rgn1 = { + .origin = 0, + .extent = 1, + .origin_source = x, + }; + struct blit_rgn1 y_rgn1 = { + .origin = 0, + .extent = 1, + .origin_source = y, + }; + assert(blit_rgn1_rop2(&bit, &x_rgn1, &y_rgn1, &image, blit_rop2_copy)); + blit_scanline_t bit_scanline = bit_store[0] >> 7; + (void)printf("%c", bit_scanline ? '#' : '.'); + assert(bit_scanline == ((x & 1U) ^ (y & 1U))); + } + (void)printf("\n"); + } + + return EXIT_SUCCESS; +}