From d08933ecda63ce9c4b7e8b78680aa2f0f4712f91 Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 20:08:35 -0700 Subject: [PATCH 1/7] ImageScaleBrightness: truncate on x86 like Default and NEON The SSE4.1, AVX2 and AVX-512 versions round the scaled channels to the nearest integer, but Default and NEON truncate. So on x86 a channel can come out 1 higher: 102 * 1.3 = 132.6 gives 133 instead of 132. Use the truncating conversion. Co-Authored-By: Claude Opus 5.5 --- .../Kernels_ImageScaleBrightness_x64_AVX2.cpp | 4 ++-- .../Kernels_ImageScaleBrightness_x64_AVX512.cpp | 4 ++-- .../Kernels_ImageScaleBrightness_x64_SSE41.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_AVX2.cpp b/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_AVX2.cpp index 1bd3ffb571..7c24acc860 100644 --- a/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_AVX2.cpp +++ b/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_AVX2.cpp @@ -31,7 +31,7 @@ PA_FORCE_INLINE void scale_brightness_x64_AVX2( pf = _mm256_mul_ps(pf, scale); pf = _mm256_min_ps(pf, _mm256_set1_ps(255.)); pf = _mm256_max_ps(pf, _mm256_set1_ps(0.)); - pi = _mm256_cvtps_epi32(pf); + pi = _mm256_cvttps_epi32(pf); pi = _mm256_shuffle_epi8(pi, _mm256_setr_epi8( 0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1 @@ -54,7 +54,7 @@ PA_FORCE_INLINE void scale_brightness_x64_AVX2( pf = _mm_mul_ps(pf, _mm256_castps256_ps128(scale)); pf = _mm_min_ps(pf, _mm_set1_ps(255.)); pf = _mm_max_ps(pf, _mm_set1_ps(0.)); - pi = _mm_cvtps_epi32(pf); + pi = _mm_cvttps_epi32(pf); pi = _mm_shuffle_epi8(pi, _mm_setr_epi8(0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)); image[0] = _mm_cvtsi128_si32(pi); diff --git a/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_AVX512.cpp b/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_AVX512.cpp index b8f393baab..7da1fce2c2 100644 --- a/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_AVX512.cpp +++ b/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_AVX512.cpp @@ -31,7 +31,7 @@ PA_FORCE_INLINE void scale_brightness_x64_AVX512( pf = _mm512_mul_ps(pf, scale); pf = _mm512_min_ps(pf, _mm512_set1_ps(255.)); pf = _mm512_max_ps(pf, _mm512_set1_ps(0.)); - pi = _mm512_cvtps_epi32(pf); + pi = _mm512_cvttps_epi32(pf); pixel = _mm512_cvtepi32_epi8(pi); _mm_storeu_si128((__m128i*)image, pixel); @@ -48,7 +48,7 @@ PA_FORCE_INLINE void scale_brightness_x64_AVX512( pf = _mm512_mul_ps(pf, scale); pf = _mm512_min_ps(pf, _mm512_set1_ps(255.)); pf = _mm512_max_ps(pf, _mm512_set1_ps(0.)); - pi = _mm512_cvtps_epi32(pf); + pi = _mm512_cvttps_epi32(pf); pixel = _mm512_cvtepi32_epi8(pi); _mm_mask_storeu_epi32((__m128i*)image, mask, pixel); diff --git a/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_SSE41.cpp b/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_SSE41.cpp index 74ae498958..1ae1fa7738 100644 --- a/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_SSE41.cpp +++ b/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_x64_SSE41.cpp @@ -26,7 +26,7 @@ PA_FORCE_INLINE void scale_brightness_x64_SSE41( pf = _mm_mul_ps(pf, scale); pf = _mm_min_ps(pf, _mm_set1_ps(255.)); pf = _mm_max_ps(pf, _mm_set1_ps(0.)); - pi = _mm_cvtps_epi32(pf); + pi = _mm_cvttps_epi32(pf); pi = _mm_shuffle_epi8(pi, _mm_setr_epi8(0, 4, 8, 12, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1)); image[c] = _mm_cvtsi128_si32(pi); From 759fe679698a1a7a56c67a7c9f45cd2d8d18156c Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 20:10:06 -0700 Subject: [PATCH 2/7] Test every scale_brightness() implementation on real images The ImageScaleBrightness test was debug code. It was not registered, it only ran the implementation that the dispatch picks, it checked nothing, and it ran a 500-iteration timing loop. Add one test for each image and each implementation that the CPU supports, including Default. Each test scales a guard-page copy of the image with three sets of scales and compares every pixel with a scalar reference. The images are from the CommandLineTests repo, and their widths cover every remainder mod 4. Co-Authored-By: Claude Opus 5.5 --- .../Kernels_ImageScaleBrightness_Tests.cpp | 148 +++++++++++++++--- 1 file changed, 125 insertions(+), 23 deletions(-) diff --git a/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_Tests.cpp b/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_Tests.cpp index 6d047cd7c0..d56ad3f16a 100644 --- a/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_Tests.cpp +++ b/SerialPrograms/Source/Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness_Tests.cpp @@ -4,55 +4,157 @@ * */ -//#include "Common/Cpp/Color.h" -#include "Common/Cpp/Time.h" +#include +#include +#include "Common/Cpp/CpuId/CpuId.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" -//#include "Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64xH_Default.h" -#include "Kernels/ImageScaleBrightness/Kernels_ImageScaleBrightness.h" +#include "Tests/GuardPageImage.h" #include "Kernels_ImageScaleBrightness_Tests.h" -#include -using std::cout; -using std::endl; - namespace PokemonAutomation{ namespace Kernels{ + +// The scale_brightness() implementations, defined in Kernels_ImageScaleBrightness_*.cpp. +// `scale_brightness()` in Kernels_ImageScaleBrightness.cpp declares the same functions +// and calls the best one for the CPU. These tests call them directly, so that every +// implementation this CPU supports is tested. +void scale_brightness_Default (size_t width, size_t height, uint32_t* image, size_t bytes_per_row, float scaleR, float scaleG, float scaleB); +void scale_brightness_x64_SSE41 (size_t width, size_t height, uint32_t* image, size_t bytes_per_row, float scaleR, float scaleG, float scaleB); +void scale_brightness_x64_AVX2 (size_t width, size_t height, uint32_t* image, size_t bytes_per_row, float scaleR, float scaleG, float scaleB); +void scale_brightness_x64_AVX512 (size_t width, size_t height, uint32_t* image, size_t bytes_per_row, float scaleR, float scaleG, float scaleB); +void scale_brightness_arm64_NEON (size_t width, size_t height, uint32_t* image, size_t bytes_per_row, float scaleR, float scaleG, float scaleB); + + +namespace{ + +// One scale_brightness() implementation to test: its name in the test name, and the function. +struct ScaleBrightnessImplementation{ + using Function = void (*)( + size_t width, size_t height, uint32_t* image, size_t bytes_per_row, + float scaleR, float scaleG, float scaleB + ); + + std::string name; + Function scale_brightness; +}; + +// The (R, G, B) scales to test: below 1, above 1 so that some channels reach the 255 +// cap, exactly 1, and a negative scale, which turns the channel to 0. +constexpr float SCALES[][3] = { + {1.2f, 1.3f, 0.5f}, + {0.7f, 2.5f, 1.0f}, + {1.0f, -1.0f, 3.0f}, +}; + +// The expected result for one pixel. Each of R, G and B is multiplied by its scale +// (a negative scale counts as 0), truncated to an integer and capped at 255. Alpha +// does not change. For example, with scale 1.3 a channel of 102 becomes 132 (132.6 +// truncated), and a channel of 200 becomes 255. +uint32_t scaled_pixel(uint32_t pixel, float scaleR, float scaleG, float scaleB){ + auto scale_channel = [](uint32_t value, float scale){ + float scaled = (float)value * std::max(scale, 0.0f); + return std::min((uint32_t)scaled, (uint32_t)255); + }; + return (pixel & 0xff000000) + | scale_channel((pixel >> 16) & 0xff, scaleR) << 16 + | scale_channel((pixel >> 8) & 0xff, scaleG) << 8 + | scale_channel(pixel & 0xff, scaleB); +} + + +// Check one scale_brightness() implementation on a real image, for each of `SCALES`: +// scale a copy of the image in place and compare every pixel with `scaled_pixel()`. +// The copy is a guard-page image, so the test also crashes if the implementation +// reads or writes past the last pixel. class Test_ImageScaleBrightness : public UnitTest{ public: Test_ImageScaleBrightness( + const ScaleBrightnessImplementation& implementation, const std::string& image ) - : UnitTest("Kernels::ImageScaleBrightness - " + image) + : UnitTest("Kernels::ImageScaleBrightness - " + implementation.name + " - " + image) + , m_implementation(implementation) , m_image(UNIT_TEST_RESOURCE_PATH() + image) {} virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - ImageRGB32 new_image(m_image); + ImageRGB32 image(m_image); - int num_iterations = 500; - auto time_start = current_time(); - for (int i = 0; i < num_iterations; i++){ - scale_brightness(new_image.width(), new_image.height(), new_image.data(), new_image.bytes_per_row(), 1.2f, 1.3f, 0.5f); - // break; + for (const auto& scale : SCALES){ + GuardPageImageRGB32 scaled(image); + m_implementation.scale_brightness( + scaled.width(), scaled.height(), scaled.data(), scaled.bytes_per_row(), + scale[0], scale[1], scale[2] + ); + for (size_t y = 0; y < image.height(); y++){ + for (size_t x = 0; x < image.width(); x++){ + const uint32_t pixel = image.pixel(x, y); + const uint32_t expected = scaled_pixel(pixel, scale[0], scale[1], scale[2]); + if (scaled.pixel(x, y) != expected){ + return std::format( + "Error: scales ({}, {}, {}), pixel ({}, {}) is {:08x}: got {:08x} but should be {:08x}.", + scale[0], scale[1], scale[2], x, y, pixel, scaled.pixel(x, y), expected + ); + } + } + } } - auto time_end = current_time(); - const auto ms = std::chrono::duration_cast(time_end - time_start).count(); - cout << "Time: " << ms << " ms, " << ms / 1000. << " s" << endl; - - // new_image.save("./output.png"); - return true; - }; + } private: + ScaleBrightnessImplementation m_implementation; std::string m_image; }; +} + + + +// Add a test for each image and each scale_brightness() implementation that this build +// has and this CPU supports, including Default. void add_tests_ImageScaleBrightness(UnitTestDatabase& database){ + std::vector implementations{ + {"Default", scale_brightness_Default}, + }; +#ifdef PA_AutoDispatch_x64_08_Nehalem + if (CPU_CAPABILITY_CURRENT.OK_08_Nehalem){ + implementations.push_back({"x64_SSE41", scale_brightness_x64_SSE41}); + } +#endif +#ifdef PA_AutoDispatch_x64_13_Haswell + if (CPU_CAPABILITY_CURRENT.OK_13_Haswell){ + implementations.push_back({"x64_AVX2", scale_brightness_x64_AVX2}); + } +#endif +#ifdef PA_AutoDispatch_x64_17_Skylake + if (CPU_CAPABILITY_CURRENT.OK_17_Skylake){ + implementations.push_back({"x64_AVX512", scale_brightness_x64_AVX512}); + } +#endif +#ifdef PA_AutoDispatch_arm64_20_M1 + if (CPU_CAPABILITY_CURRENT.OK_M1){ + implementations.push_back({"arm64_NEON", scale_brightness_arm64_NEON}); + } +#endif + // Images in the CommandLineTests repo. Their widths cover every remainder mod 4, so + // the leftover pixels after the 2- and 4-pixel loops are tested too. + const std::vector images{ + "NintendoSwitch/Switch2/ghostassassin/system_settings_display_dark.png", // 1916 x 1078 + "OCR/Lv.1-0.png", // 237 x 77 + "PokemonSwSh/SelectionArrowFinder/mac_neon_error_1.png", // 106 x 410 + "OCR/german-nature-sanft.png", // 511 x 91 + }; + + for (const ScaleBrightnessImplementation& implementation : implementations){ + for (const std::string& image : images){ + database.add(implementation, image); + } + } } From 52414e021fe6fb75e34e1699f8e35d5c360019b2 Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 20:21:07 -0700 Subject: [PATCH 3/7] Test every range filter and filter_by_mask() implementation on real images The old ImageFilter tests were debug code: they were not registered, ran 3-second timing loops, and only ran the implementation the CPU dispatch picks. Now each range filter and filter_by_mask() implementation that the build has and the CPU supports gets a test on 4 CommandLineTests images, checked against a scalar reference. The two Euclidean tests are removed. Kernels::RGB32Euclidean covers them. Co-Authored-By: Claude Opus 5.5 --- .../Kernels_ImageFilter_Tests.cpp | 858 +++++++----------- 1 file changed, 328 insertions(+), 530 deletions(-) diff --git a/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp b/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp index 6757ab2420..511c7b172d 100644 --- a/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp +++ b/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp @@ -4,608 +4,406 @@ * */ -#include "Common/Cpp/Color.h" -#include "Common/Cpp/Time.h" +#include +#include +#include "Common/Cpp/CancellableScope.h" +#include "Common/Cpp/CpuId/CpuId.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/GlobalAutoPaths.h" -#include "CommonFramework/ImageTypes/BinaryImage.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" #include "Kernels/BinaryMatrix/Kernels_BinaryMatrix.h" -#ifdef PA_AutoDispatch_arm64_20_M1 -#include "Kernels/BinaryMatrix/Kernels_BinaryMatrixTile_64x8_arm64_NEON.h" -#include "Kernels/PartialWordAccess/Kernels_PartialWordAccess_arm64_NEON.h" -#include "Kernels/Waterfill/Kernels_Waterfill_Core_64x8_arm64_NEON.h" -#endif -//#include "Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64xH_Default.h" -#include "Kernels/BinaryImageFilters/Kernels_BinaryImage_BasicFilters.h" -#include "Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h" -#include "Kernels/ImageFilters/RGB32_EuclideanDistance/Kernels_ImageFilter_RGB32_Euclidean.h" -#include "Kernels_ImageFilter_Tests.h" #include "Tests/GuardPageImage.h" -#include "Tests/TestUtils.h" +#include "Kernels_ImageFilter_Tests.h" -#include -using std::cout; -using std::cerr; -using std::endl; -using std::flush; namespace PokemonAutomation{ namespace Kernels{ -class Test_ImageFilterRGB32Range : public UnitTest{ -public: - Test_ImageFilterRGB32Range( - const std::string& image - ) - : UnitTest("Kernels::ImageFilterRGB32Range - " + image) - , m_image(UNIT_TEST_RESOURCE_PATH() + image) - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(), height = image.height(); - cout << "Testing filter_rgb32_range(), image size " << width << " x " << height << endl; - - Color min_color(0, 0, 0); - Color max_color(63, 63, 63); - // Color max_color(238, 24, 42); - - const uint32_t mins = uint32_t(min_color); - const uint32_t maxs = uint32_t(max_color); - - GuardPageImageRGB32 image_out(image.width(), image.height()); - GuardPageImageRGB32 image_out_2(image.width(), image.height()); - size_t pixels_in_range = 0; - - const bool replace_color_within_range = true; - auto time_start = current_time(); - // auto new_image = filter_rgb32_range(image, mins, maxs, COLOR_WHITE, replace_color_within_range); - pixels_in_range = Kernels::filter_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - (uint32_t)COLOR_WHITE, replace_color_within_range, - mins, maxs - ); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One filter time: " << ms << " ms" << endl; - - size_t pixels_in_range_2 = Kernels::filter_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out_2.data(), image_out_2.bytes_per_row(), - (uint32_t)COLOR_WHITE, !replace_color_within_range, - mins, maxs - ); - - TEST_RESULT_EQUAL_STR(pixels_in_range, pixels_in_range_2); - - size_t actual_num_pixels_in_range = 0; - size_t error_count = 0; - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - const Color new_color(image_out.pixel(x, y)); - const Color new_color_2(image_out_2.pixel(x, y)); - bool in_range = (min_color.alpha() <= color.alpha() && color.alpha() <= max_color.alpha()); - in_range = in_range && (min_color.red() <= color.red() && color.red() <= max_color.red()); - in_range = in_range && (min_color.green() <= color.green() && color.green() <= max_color.green()); - in_range = in_range && (min_color.blue() <= color.blue() && color.blue() <= max_color.blue()); - actual_num_pixels_in_range += in_range; - if (error_count < 10){ - // Print first 10 errors: - if (in_range && new_color != COLOR_WHITE){ - cout << "Error: wrong filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be in range but not found by the function" << endl; - ++error_count; - }else if (in_range == false && new_color != color){ - cout << "Error: wrong filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be changed by the function" << endl; - ++error_count; - } - - if (in_range && new_color_2 != color){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be changed by the function" << endl; - ++error_count; - }else if (in_range == false && new_color_2 != COLOR_WHITE){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be in range but not found by the function" << endl; - ++error_count; - } - } - } - } - cout << "Found " << actual_num_pixels_in_range << " pixels in range" << endl; - if (pixels_in_range != actual_num_pixels_in_range){ - cout << "Error: wrong pixels in range: " << pixels_in_range << " actual: " << actual_num_pixels_in_range << endl; - return false; - } - if (error_count){ - return false; - } +// The range filter implementations, defined in RGB32_Range/Kernels_ImageFilter_RGB32_Range_*.cpp. +// `filter_rgb32_range()` and `to_blackwhite_rgb32_range()` in Kernels_ImageFilter_RGB32_Range.cpp +// declare the same functions and call the best one for the CPU. These tests call them directly, +// so that every implementation this CPU supports is tested. +size_t filter_rgb32_range_Default( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_Default( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); +size_t filter_rgb32_range_x64_SSE42( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_x64_SSE42( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); +size_t filter_rgb32_range_x64_AVX2( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_x64_AVX2( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); +size_t filter_rgb32_range_x64_AVX512( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_x64_AVX512( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); +size_t filter_rgb32_range_arm64_NEON( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs +); +size_t to_blackwhite_rgb32_range_arm64_NEON( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs +); + +// The filter_by_mask() implementations, one per binary matrix type, defined in +// BinaryImageFilters/Kernels_BinaryImage_BasicFilters_Core_*.cpp. `filter_by_mask()` in +// Kernels_BinaryImage_BasicFilters.cpp declares the same functions and calls the one for +// the matrix type. +void filter_by_mask_64x4_Default (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x8_x64_SSE42 (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x16_x64_AVX2 (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x32_x64_AVX512 (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x64_x64_AVX512 (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); +void filter_by_mask_64x8_arm64_NEON (const PackedBinaryMatrix_IB& matrix, uint32_t* image, size_t bytes_per_row, uint32_t replace_with, bool replace_if_zero); + + +namespace{ + +// One range filter implementation to test: its name in the test name, and its two functions. +struct RangeFilterImplementation{ + using Filter = size_t (*)( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + uint32_t replacement, bool replace_color_within_range, + uint32_t mins, uint32_t maxs + ); + using ToBlackWhite = size_t (*)( + const uint32_t* in, size_t in_bytes_per_row, size_t width, size_t height, + uint32_t* out, size_t out_bytes_per_row, + bool in_range_black, + uint32_t mins, uint32_t maxs + ); + + std::string name; + Filter filter; + ToBlackWhite to_blackwhite; +}; - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - Kernels::filter_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - (uint32_t)COLOR_WHITE, replace_color_within_range, - mins, maxs - ); - } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; +// One filter_by_mask() implementation to test: its name in the test name, the binary +// matrix type it works on, and the function. +struct FilterByMaskImplementation{ + using Function = void (*)( + const PackedBinaryMatrix_IB& matrix, + uint32_t* image, size_t bytes_per_row, + uint32_t replace_with, bool replace_if_zero + ); + + std::string name; + BinaryMatrixType matrix_type; + Function filter_by_mask; +}; - return true; - }; -private: - std::string m_image; +// An ARGB color range: a pixel is in range if each of its A, R, G and B bytes is within +// the bytes of `mins` and `maxs`. +struct ColorRange{ + uint32_t mins; + uint32_t maxs; }; +// The ranges to test. Each has pixels both in and out of range on every test image. The +// bounds include 0x00, 0x7f, 0x80 and 0xff, the edges of signed and unsigned bytes. +constexpr ColorRange RANGES[] = { + {0xff000000, 0xff8080ff}, + {0xff204060, 0xffe0c0ff}, + {0xff60607f, 0xffc8c8c8}, + {0xffa0a0a0, 0xffffffff}, +}; -class Test_ImageFilterRGB32Euclidean : public UnitTest{ -public: - Test_ImageFilterRGB32Euclidean( - const std::string& image - ) - : UnitTest("Kernels::ImageFilterRGB32Euclidean - " + image) - , m_image(UNIT_TEST_RESOURCE_PATH() + image) - {} +constexpr uint32_t REPLACEMENT_COLOR = 0x13572468; +constexpr uint32_t BLACK = 0xff000000; +constexpr uint32_t WHITE = 0xffffffff; - virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(); - const size_t height = image.height(); - cout << "Testing test_kernels_FilterRGB32Euclidean(), image size " << width << " x " << height << endl; - - Color middle_color = Color(image.pixel(width / 2, height / 2)); - cout << "Expected color: " << middle_color.to_string() << endl; - - double max_dist = 50.0; - size_t max_dist2 = size_t(max_dist * max_dist); - - GuardPageImageRGB32 image_out(image.width(), image.height()); - GuardPageImageRGB32 image_out_2(image.width(), image.height()); - size_t pixels_in_range = 0; - - const bool replace_color_within_range = true; - auto time_start = current_time(); - pixels_in_range = Kernels::filter_rgb32_euclidean( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - (uint32_t)COLOR_WHITE, replace_color_within_range, - uint32_t(middle_color), max_dist - ); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One filter time: " << ms << " ms" << endl; - - size_t pixels_in_range_2 = Kernels::filter_rgb32_euclidean( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out_2.data(), image_out_2.bytes_per_row(), - (uint32_t)COLOR_WHITE, !replace_color_within_range, - uint32_t(middle_color), max_dist - ); - - TEST_RESULT_EQUAL_STR(pixels_in_range, pixels_in_range_2); - - size_t actual_num_pixels_in_range = 0; - size_t error_count = 0; - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - const Color new_color(image_out.pixel(x, y)); - const Color new_color_2(image_out_2.pixel(x, y)); - int red_dif = (color.red() - middle_color.red()); - int green_dif = (color.green() - middle_color.green()); - int blue_dif = (color.blue() - middle_color.blue()); - size_t color_dist2 = red_dif * red_dif + green_dif * green_dif + blue_dif * blue_dif; - bool in_range = color_dist2 <= max_dist2; - actual_num_pixels_in_range += in_range; - if (error_count < 10){ - // Print first 10 errors: - if (in_range && new_color != COLOR_WHITE){ - cout << "Error: wrong filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should be in range but not found by the function" << endl; - ++error_count; - }else if (in_range == false && new_color != color){ - cout << "Error: wrong filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should not be changed by the function" << endl; - ++error_count; - } - - if (in_range && new_color_2 != color){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should not be changed by the function" << endl; - ++error_count; - }else if (in_range == false && new_color_2 != COLOR_WHITE){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should not be in range but not found by the function" << endl; - ++error_count; - } - } - } - } - cout << "Found " << actual_num_pixels_in_range << " pixels in range" << endl; - if (pixels_in_range != actual_num_pixels_in_range){ - cout << "Error: wrong pixels in range: " << pixels_in_range << " actual: " << actual_num_pixels_in_range << endl; +// Whether `pixel` is in `range`. For example, 0xff7f3080 is in the range +// [0xff000000, 0xff8080ff], but 0xff813080 is not, since its R (0x81) is above 0x80. +bool in_range(uint32_t pixel, const ColorRange& range){ + for (int shift = 0; shift < 32; shift += 8){ + const uint32_t value = (pixel >> shift) & 0xff; + if (value < ((range.mins >> shift) & 0xff) || value > ((range.maxs >> shift) & 0xff)){ return false; } + } + return true; +} - if (error_count){ - return false; +size_t count_in_range(const GuardPageImageRGB32& image, const ColorRange& range){ + size_t count = 0; + for (size_t y = 0; y < image.height(); y++){ + for (size_t x = 0; x < image.width(); x++){ + count += in_range(image.pixel(x, y), range); } + } + return count; +} - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - pixels_in_range = Kernels::filter_rgb32_euclidean( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - (uint32_t)COLOR_WHITE, replace_color_within_range, - uint32_t(middle_color), max_dist - ); +// Compare each pixel of `output` with `expected_pixel()`. Return a +// description of the first wrong pixel, or an empty string if all pixels are right. +template +std::string find_wrong_pixel( + const GuardPageImageRGB32& input, const GuardPageImageRGB32& output, + ExpectedPixel expected_pixel +){ + for (size_t y = 0; y < input.height(); y++){ + for (size_t x = 0; x < input.width(); x++){ + const uint32_t pixel = input.pixel(x, y); + const uint32_t expected = expected_pixel(pixel); + if (output.pixel(x, y) != expected){ + return std::format( + "pixel ({}, {}) is {:08x}: got {:08x} but should be {:08x}", + x, y, pixel, output.pixel(x, y), expected + ); + } } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; + } + return ""; +} - return true; - - }; +std::string describe(const ColorRange& range){ + return std::format("range [{:08x}, {:08x}]", range.mins, range.maxs); +} -private: - std::string m_image; -}; -class Test_ImageToBlackWhiteRGB32Range : public UnitTest{ +// Check one implementation of `filter_rgb32_range()` and `to_blackwhite_rgb32_range()` +// on a real image, for each of `RANGES`, against `in_range()`: +// - `filter_rgb32_range()` copies the image, but replaces the pixels in range (or the +// pixels out of range) with `REPLACEMENT_COLOR`. +// - `to_blackwhite_rgb32_range()` makes the pixels in range black and the others white +// (or the opposite). +// Both must return the number of pixels in range. The input and output are guard-page +// images, so the test also crashes if the implementation reads or writes past the last +// pixel. +class Test_ImageFilterRGB32Range : public UnitTest{ public: - Test_ImageToBlackWhiteRGB32Range( + Test_ImageFilterRGB32Range( + const RangeFilterImplementation& implementation, const std::string& image ) - : UnitTest("Kernels::ImageToBlackWhiteRGB32Range - " + image) + : UnitTest("Kernels::ImageFilterRGB32Range - " + implementation.name + " - " + image) + , m_implementation(implementation) , m_image(UNIT_TEST_RESOURCE_PATH() + image) {} virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(); - const size_t height = image.height(); - cout << "Testing to_black_white_rgb32_range(), image size " << width << " x " << height << endl; - - Color min_color(0, 0, 0); - // Color min_color(0); - - Color max_color(63, 63, 63); - // Color max_color(255, 255, 255); - // Color max_color(238, 24, 42); - cout << "min color: " << min_color.to_string() << " max color: " << max_color.to_string() << endl; - - const uint32_t mins = uint32_t(min_color); - const uint32_t maxs = uint32_t(max_color); - - GuardPageImageRGB32 image_out(image.width(), image.height()); - GuardPageImageRGB32 image_out_2(image.width(), image.height()); - size_t pixels_in_range = 0; - - const bool in_range_black = true; - auto time_start = current_time(); - // auto new_image = filter_rgb32_range(image, mins, maxs, COLOR_WHITE, replace_color_within_range); - pixels_in_range = Kernels::to_blackwhite_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - in_range_black, - mins, maxs - ); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One filter time: " << ms << " ms" << endl; - - size_t pixels_in_range_2 = Kernels::to_blackwhite_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out_2.data(), image_out_2.bytes_per_row(), - !in_range_black, - mins, maxs - ); - - TEST_RESULT_EQUAL_STR(pixels_in_range, pixels_in_range_2); - - size_t actual_num_pixels_in_range = 0; - size_t error_count = 0; - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - const Color new_color(image_out.pixel(x, y)); - const Color new_color_2(image_out_2.pixel(x, y)); - bool in_range = (min_color.alpha() <= color.alpha() && color.alpha() <= max_color.alpha()); - in_range = in_range && (min_color.red() <= color.red() && color.red() <= max_color.red()); - in_range = in_range && (min_color.green() <= color.green() && color.green() <= max_color.green()); - in_range = in_range && (min_color.blue() <= color.blue() && color.blue() <= max_color.blue()); - actual_num_pixels_in_range += in_range; - if (error_count < 10){ - // Print first 10 errors: - if (in_range && new_color != COLOR_BLACK){ - cout << "Error: wrong filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be black due to in range but not so" << endl; - ++error_count; - }else if (in_range == false && new_color != COLOR_WHITE){ - cout << "Error: wrong filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be white due to out of range but not so" << endl; - ++error_count; - } - - if (in_range && new_color_2 != COLOR_WHITE){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be white due to in range but not so" << endl; - ++error_count; - }else if (in_range == false && new_color_2 != COLOR_BLACK){ - cout << "Error: wrong inverse filter result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be black due to out of range but not so" << endl; - ++error_count; - } + const ImageRGB32 file_image(m_image); + const GuardPageImageRGB32 image(file_image); + GuardPageImageRGB32 output(image.width(), image.height()); + + for (const ColorRange& range : RANGES){ + scope.throw_if_cancelled(); + const size_t expected_count = count_in_range(image, range); + + for (bool replace_color_within_range : {true, false}){ + const size_t count = m_implementation.filter( + image.data(), image.bytes_per_row(), image.width(), image.height(), + output.data(), output.bytes_per_row(), + REPLACEMENT_COLOR, replace_color_within_range, + range.mins, range.maxs + ); + const std::string error = find_wrong_pixel(image, output, [&](uint32_t pixel){ + return in_range(pixel, range) == replace_color_within_range ? REPLACEMENT_COLOR : pixel; + }); + const std::string name = std::format( + "filter_rgb32_range(), {}, replace_color_within_range = {}", + describe(range), replace_color_within_range + ); + if (count != expected_count){ + return std::format("Error: {}: counted {} pixels in range but should be {}.", name, count, expected_count); + } + if (!error.empty()){ + return std::format("Error: {}: {}.", name, error); } } - } - cout << "Found " << actual_num_pixels_in_range << " pixels in range" << endl; - if (pixels_in_range != actual_num_pixels_in_range){ - cout << "Error: wrong pixels in range: " << pixels_in_range << " actual: " << actual_num_pixels_in_range << endl; - return false; - } - - if (error_count){ - return false; - } - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - Kernels::to_blackwhite_rgb32_range( - image.data(), image.bytes_per_row(), image.width(), image.height(), - image_out.data(), image_out.bytes_per_row(), - in_range_black, - mins, maxs - ); + for (bool in_range_black : {true, false}){ + const size_t count = m_implementation.to_blackwhite( + image.data(), image.bytes_per_row(), image.width(), image.height(), + output.data(), output.bytes_per_row(), + in_range_black, + range.mins, range.maxs + ); + const std::string error = find_wrong_pixel(image, output, [&](uint32_t pixel){ + return in_range(pixel, range) == in_range_black ? BLACK : WHITE; + }); + const std::string name = std::format( + "to_blackwhite_rgb32_range(), {}, in_range_black = {}", + describe(range), in_range_black + ); + if (count != expected_count){ + return std::format("Error: {}: counted {} pixels in range but should be {}.", name, count, expected_count); + } + if (!error.empty()){ + return std::format("Error: {}: {}.", name, error); + } + } } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; - return true; - - }; + } private: + RangeFilterImplementation m_implementation; std::string m_image; }; +// Check one `filter_by_mask()` implementation on a real image, for each of `RANGES`. +// The mask is a binary matrix of the implementation's type with a 1 bit for each pixel +// in range. It is made one bit at a time with `PackedBinaryMatrix_IB::set()`, so it does +// not depend on the SIMD code of `compress_rgb32_to_binary_range()`. Then +// `filter_by_mask()` must replace the pixels with 0 bits (or the pixels with 1 bits) with +// `REPLACEMENT_COLOR`, and keep the other pixels. The image is a guard-page image, so the +// test also crashes if the implementation reads or writes past the last pixel. class Test_ImageFilterByMask : public UnitTest{ public: Test_ImageFilterByMask( + const FilterByMaskImplementation& implementation, const std::string& image ) - : UnitTest("Kernels::ImageFilterByMask - " + image) + : UnitTest("Kernels::ImageFilterByMask - " + implementation.name + " - " + image) + , m_implementation(implementation) , m_image(UNIT_TEST_RESOURCE_PATH() + image) {} virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(), height = image.height(); - cout << "Image width " << width << " height " << height << endl; - - const Color min_color(0, 0, 0), max_color(63, 63, 63); - const uint32_t mins = uint32_t(min_color), maxs = uint32_t(max_color); - - auto binary_matrix = make_PackedBinaryMatrix(get_BinaryMatrixType(), width, height); - compress_rgb32_to_binary_range( - image.data(), image.bytes_per_row(), *binary_matrix, mins, maxs - ); - GuardPageImageRGB32 new_image(file_image); - GuardPageImageRGB32 new_image_2(file_image); - - Color replacement_color = COLOR_WHITE; - bool replace_zero_bits = true; - - auto time_start = current_time(); - filter_by_mask(*binary_matrix, new_image.data(), new_image.bytes_per_row(), uint32_t(replacement_color), replace_zero_bits); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One Filter by mask call. time: " << ms << " ms" << endl; - - filter_by_mask(*binary_matrix, new_image_2.data(), new_image_2.bytes_per_row(), uint32_t(replacement_color), !replace_zero_bits); - - size_t error_count = 0; - - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - const Color new_color(new_image.pixel(x, y)); - const Color new_color_2(new_image_2.pixel(x, y)); - bool in_range = (min_color.alpha() <= color.alpha() && color.alpha() <= max_color.alpha()); - in_range = in_range && (min_color.red() <= color.red() && color.red() <= max_color.red()); - in_range = in_range && (min_color.green() <= color.green() && color.green() <= max_color.green()); - in_range = in_range && (min_color.blue() <= color.blue() && color.blue() <= max_color.blue()); - - if (error_count <= 10){ - if (in_range && new_color != color){ - cout << "Error: wrong filter(replace_zero_bits) result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be changed due to being one bit but not so" << endl; - ++error_count; - }else if (!in_range && new_color != replacement_color){ - cout << "Error: wrong filter(replace_zero_bits) result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be changed due to being zero bit but not so" << endl; - ++error_count; - } - - if (in_range && new_color_2 != replacement_color){ - cout << "Error: wrong filter(replace_one_bits) result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should be changed due to being one bit but not so" << endl; - ++error_count; - }else if (!in_range && new_color_2 != color){ - cout << "Error: wrong filter(replace_one_bits) result: old color " << color.to_string() << ", (x,y) = " - << x << ", " << y << ", should not be changed due to being zero bit but not so" << endl; - ++error_count; - } + const ImageRGB32 file_image(m_image); + const GuardPageImageRGB32 image(file_image); + + for (const ColorRange& range : RANGES){ + scope.throw_if_cancelled(); + std::unique_ptr mask = make_PackedBinaryMatrix( + m_implementation.matrix_type, image.width(), image.height() + ); + for (size_t y = 0; y < image.height(); y++){ + for (size_t x = 0; x < image.width(); x++){ + mask->set(x, y, in_range(image.pixel(x, y), range)); } } - } - - if (error_count){ - return false; - } - - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - filter_by_mask(*binary_matrix, new_image.data(), new_image.bytes_per_row(), uint32_t(replacement_color), replace_zero_bits); - } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; - - return true; - - }; - -private: - std::string m_image; -}; - -class Test_CompressRGB32ToBinaryEuclidean : public UnitTest{ -public: - Test_CompressRGB32ToBinaryEuclidean( - const std::string& image - ) - : UnitTest("Kernels::CompressRGB32ToBinaryEuclidean - " + image) - , m_image(UNIT_TEST_RESOURCE_PATH() + image) - {} - - virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); - - const size_t width = image.width(); - const size_t height = image.height(); - cout << "Testing test_kernels_CompressRGB32ToBinaryEuclidean(), image size " << width << " x " << height << endl; - - Color middle_color = Color(image.pixel(width / 2, height / 2)); - cout << "Expected color: " << middle_color.to_string() << endl; - - double max_dist = 50.0; - size_t max_dist2 = size_t(max_dist * max_dist); - - PackedBinaryMatrix matrix(image.width(), image.height()); - - auto time_start = current_time(); - Kernels::compress_rgb32_to_binary_euclidean( - image.data(), image.bytes_per_row(), matrix, - uint32_t(middle_color), max_dist - ); - auto time_end = current_time(); - auto ns = std::chrono::duration_cast(time_end - time_start).count(); - auto ms = ns / 1000000.; - cout << "One filter time: " << ms << " ms" << endl; - - size_t error_count = 0; - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - int red_dif = (color.red() - middle_color.red()); - int green_dif = (color.green() - middle_color.green()); - int blue_dif = (color.blue() - middle_color.blue()); - size_t color_dist2 = red_dif * red_dif + green_dif * green_dif + blue_dif * blue_dif; - bool in_range = color_dist2 <= max_dist2; - if (error_count < 10){ - // Print first 10 errors: - if (in_range && matrix.get(x, y) == false){ - cout << "Error: wrong filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should be in range but not set on matrix" << endl; - ++error_count; - }else if (in_range == false && matrix.get(x, y) == true){ - cout << "Error: wrong filter result: old color " << color.to_string() - << ", (x,y) = (" << x << ", " << y << ")" - << ", dist2 " << color_dist2 << ", max dist2 " << max_dist2 - << ", should not be in range but set on matrix" << endl; - ++error_count; - } + for (bool replace_if_zero : {true, false}){ + GuardPageImageRGB32 filtered(file_image); + m_implementation.filter_by_mask( + *mask, filtered.data(), filtered.bytes_per_row(), + REPLACEMENT_COLOR, replace_if_zero + ); + // The mask bit is 1 if the pixel is in range. The pixel is replaced if + // that bit is 0 and `replace_if_zero` is true, or if it is 1 and + // `replace_if_zero` is false. + const std::string error = find_wrong_pixel(image, filtered, [&](uint32_t pixel){ + return in_range(pixel, range) != replace_if_zero ? REPLACEMENT_COLOR : pixel; + }); + if (!error.empty()){ + return std::format( + "Error: mask of {}, replace_if_zero = {}: {}.", + describe(range), replace_if_zero, error + ); } } } - if (error_count){ - return false; - } - - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - Kernels::compress_rgb32_to_binary_euclidean( - image.data(), image.bytes_per_row(), matrix, - uint32_t(middle_color), max_dist - ); - } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, avg filter time: " << ms / num_iters << " ms" << endl; - return true; - }; + } private: + FilterByMaskImplementation m_implementation; std::string m_image; }; +} - +// Add a test for each image and each range filter and filter_by_mask() implementation +// that this build has and this CPU supports, including Default. void add_tests_ImageFilters(UnitTestDatabase& database){ + std::vector range_filters{ + {"Default", filter_rgb32_range_Default, to_blackwhite_rgb32_range_Default}, + }; + std::vector mask_filters{ + {"64x4_Default", BinaryMatrixType::i64x4_Default, filter_by_mask_64x4_Default}, + }; +#ifdef PA_AutoDispatch_x64_08_Nehalem + if (CPU_CAPABILITY_CURRENT.OK_08_Nehalem){ + range_filters.push_back({"x64_SSE42", filter_rgb32_range_x64_SSE42, to_blackwhite_rgb32_range_x64_SSE42}); + mask_filters.push_back({"64x8_x64_SSE42", BinaryMatrixType::i64x8_x64_SSE42, filter_by_mask_64x8_x64_SSE42}); + } +#endif +#ifdef PA_AutoDispatch_x64_13_Haswell + if (CPU_CAPABILITY_CURRENT.OK_13_Haswell){ + range_filters.push_back({"x64_AVX2", filter_rgb32_range_x64_AVX2, to_blackwhite_rgb32_range_x64_AVX2}); + mask_filters.push_back({"64x16_x64_AVX2", BinaryMatrixType::i64x16_x64_AVX2, filter_by_mask_64x16_x64_AVX2}); + } +#endif +#ifdef PA_AutoDispatch_x64_17_Skylake + if (CPU_CAPABILITY_CURRENT.OK_17_Skylake){ + range_filters.push_back({"x64_AVX512", filter_rgb32_range_x64_AVX512, to_blackwhite_rgb32_range_x64_AVX512}); + mask_filters.push_back({"64x32_x64_AVX512", BinaryMatrixType::i64x32_x64_AVX512, filter_by_mask_64x32_x64_AVX512}); + mask_filters.push_back({"64x64_x64_AVX512", BinaryMatrixType::i64x64_x64_AVX512, filter_by_mask_64x64_x64_AVX512}); + } +#endif +#ifdef PA_AutoDispatch_arm64_20_M1 + if (CPU_CAPABILITY_CURRENT.OK_M1){ + range_filters.push_back({"arm64_NEON", filter_rgb32_range_arm64_NEON, to_blackwhite_rgb32_range_arm64_NEON}); + mask_filters.push_back({"64x8_arm64_NEON", BinaryMatrixType::arm64x8_x64_NEON, filter_by_mask_64x8_arm64_NEON}); + } +#endif + // Images in the CommandLineTests repo. Their widths cover every remainder mod 4, and + // no width or height is a multiple of 8, so the leftover pixels after the SIMD loops + // and the binary matrix tiles at the right and bottom edges are tested too. + const std::vector images{ + "NintendoSwitch/Switch2/ghostassassin/system_settings_display_dark.png", // 1916 x 1078 + "OCR/Lv.1-0.png", // 237 x 77 + "PokemonSwSh/SelectionArrowFinder/mac_neon_error_1.png", // 106 x 410 + "OCR/german-nature-sanft.png", // 511 x 91 + }; - + for (const std::string& image : images){ + for (const RangeFilterImplementation& implementation : range_filters){ + database.add(implementation, image); + } + for (const FilterByMaskImplementation& implementation : mask_filters){ + database.add(implementation, image); + } + } } From 4ad3b94a7cde80b92bb444b0795ac401d8904d61 Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 20:28:15 -0700 Subject: [PATCH 4/7] Fix PackedBinaryMatrix::submatrix() at tile edges If the submatrix width is a multiple of 64, or its height a multiple of the tile height, submatrix() called clear_padding() with 0 bits and cleared the whole last column or row of tiles. The tile height depends on the CPU (4 to 64), so results were different on each machine. SparseBinaryMatrix::submatrix() got this fix in ecdc7fccb. This makes the packed version the same. Co-Authored-By: Claude Opus 5.5 --- .../Kernels_PackedBinaryMatrixCore.tpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/SerialPrograms/Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrixCore.tpp b/SerialPrograms/Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrixCore.tpp index acef198332..45c593eabf 100644 --- a/SerialPrograms/Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrixCore.tpp +++ b/SerialPrograms/Source/Kernels/BinaryMatrix/Kernels_PackedBinaryMatrixCore.tpp @@ -300,13 +300,20 @@ PackedBinaryMatrixCore PackedBinaryMatrixCore::submatrix( } #if 1 + // Clear the bits past the right and bottom edges, which came from outside the + // box. Skip an edge that ends at a tile boundary: `clear_padding()` with 0 bits + // would clear the whole last column or row of tiles. size_t wbits = width % TILE_WIDTH; - for (size_t r = 0; r < tile_height; r++){ - ret.tile(tile_width - 1, r).clear_padding(wbits, TILE_HEIGHT); + if (wbits != 0){ + for (size_t r = 0; r < tile_height; r++){ + ret.tile(tile_width - 1, r).clear_padding(wbits, TILE_HEIGHT); + } } size_t hbits = height % TILE_HEIGHT; - for (size_t c = 0; c < tile_width; c++){ - ret.tile(c, tile_height - 1).clear_padding(TILE_WIDTH, hbits); + if (hbits != 0){ + for (size_t c = 0; c < tile_width; c++){ + ret.tile(c, tile_height - 1).clear_padding(TILE_WIDTH, hbits); + } } #endif From 46c83940a06913014dfbbd935be0ece709c43d92 Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 20:28:15 -0700 Subject: [PATCH 5/7] Test every binary matrix type on real images The old BinaryMatrix test was debug code: it was not registered, ran timing loops, and only checked the matrix type the CPU dispatch picks. Now each matrix type that the build has and the CPU supports gets a test on 4 CommandLineTests images. It checks compress_rgb32_to_binary_range(), the bitwise operations and submatrix() against the image pixels, and that the padding bits stay 0. The tile checks now also run on the Default tile. Co-Authored-By: Claude Opus 5.5 --- .../Kernels_BinaryMatrix_Tests.cpp | 806 ++++++++++-------- 1 file changed, 472 insertions(+), 334 deletions(-) diff --git a/SerialPrograms/Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Tests.cpp b/SerialPrograms/Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Tests.cpp index 9729ba729e..72c2ea836b 100644 --- a/SerialPrograms/Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Tests.cpp +++ b/SerialPrograms/Source/Kernels/BinaryMatrix/Kernels_BinaryMatrix_Tests.cpp @@ -4,424 +4,562 @@ * */ -#include "Common/Cpp/Color.h" -#include "Common/Cpp/Time.h" +#include +#include +#include +#include +#include +#include "Common/Cpp/CancellableScope.h" +#include "Common/Cpp/CpuId/CpuId.h" +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" #include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" -#include "CommonFramework/ImageTypes/ImageViewRGB32.h" -#include "Kernels/BinaryMatrix/Kernels_BinaryMatrix.h" +#include "Kernels/BinaryImageFilters/Kernels_BinaryImage_BasicFilters.h" +#include "Kernels_BinaryMatrixTile_64x4_Default.h" #ifdef PA_AutoDispatch_arm64_20_M1 -#include "Kernels/BinaryMatrix/Kernels_BinaryMatrixTile_64x8_arm64_NEON.h" #include "Kernels/PartialWordAccess/Kernels_PartialWordAccess_arm64_NEON.h" -#include "Kernels/Waterfill/Kernels_Waterfill_Core_64x8_arm64_NEON.h" +#include "Kernels_BinaryMatrixTile_64x8_arm64_NEON.h" #endif -//#include "Kernels/BinaryMatrix/Kernels_BinaryMatrix_Arch_64xH_Default.h" -#include "Kernels/BinaryImageFilters/Kernels_BinaryImage_BasicFilters.h" #include "Tests/GuardPageImage.h" #include "Kernels_BinaryMatrix_Tests.h" -#include -#include -using std::cout; -using std::cerr; -using std::endl; -using std::flush; - namespace PokemonAutomation{ namespace Kernels{ -// Additional tests on binary matrix tile implementation -template int test_binary_matrix_tile_t(){ - size_t num_iters = 100000; - size_t sum = 0; - WallClock time_start, time_end; - size_t ns = 0; - double ms = 0; +namespace{ + +// Tile tests: check one binary matrix tile class against `get_bit()`. Only the tiles +// that compile with this file's default compiler flags are tested here: Default, and NEON +// on arm64. The x86 tiles need AVX2 or AVX512 flags. They are tested through the matrix +// tests below and the Waterfill tests. + +template +Tile random_tile(std::mt19937& generator){ Tile tile; - // BinaryTile_64x8_arm64_NEON tile; - // BinaryTile_64xH_Default<8, BinaryMatrixType::arm64x8_x64_NEON> tile; - for (size_t height = 0; height < tile.HEIGHT; ++height){ - for (size_t width = 0; width < tile.WIDTH; ++width){ - tile.set_zero(); - tile.set_ones(width, height); - for (size_t y = 0; y < tile.HEIGHT; ++y){ - for (size_t x = 0; x < tile.WIDTH; ++x){ - bool gt = y < height && x < width; - if (gt != tile.get_bit(x, y)){ - cout << "Tile::set_ones(width = " << width << ", height = " << height << "), tile (x = " << - x << ", y = " << y << "), wrong bit. Should be " << gt << endl; - return 1; - } - } - } + for (size_t y = 0; y < Tile::HEIGHT; y++){ + for (size_t x = 0; x < Tile::WIDTH; x++){ + tile.set_bit(x, y, generator() & 1); } } + return tile; +} - sum = 0; - time_start = current_time(); - for (size_t i = 0; i < num_iters; ++i){ - tile.set_ones(32, 4); - sum += tile.row(0) + tile.row(1) + tile.row(2) + tile.row(3); +// Compare each bit of `tile` with `expected_bit(x, y)`. Return a description of the first +// wrong bit, or an empty string if all bits are right. +template +std::string find_wrong_tile_bit(const Tile& tile, ExpectedBit expected_bit){ + for (size_t y = 0; y < Tile::HEIGHT; y++){ + for (size_t x = 0; x < Tile::WIDTH; x++){ + const bool expected = expected_bit(x, y); + if (tile.get_bit(x, y) != expected){ + return std::format("tile bit ({}, {}) should be {}", x, y, (int)expected); + } + } } - time_end = current_time(); - ns = std::chrono::duration_cast(time_end - time_start).count(); - ms = ns / 1000000.; - cout << "Execution enforcer: " << sum << endl; - cout << "Running " << num_iters << " iters, Tile::set_ones() time: " << ms / num_iters << " ms" << endl; - - for (size_t height = 0; height < tile.HEIGHT; ++height){ - for (size_t width = 0; width < tile.WIDTH; ++width){ - tile.set_ones(); + return ""; +} + +// `set_ones(width, height)` sets the bits (x, y) with x < width and y < height, and clears +// the others. `clear_padding(width, height)` clears the other bits and keeps these. +template +std::string check_tile_masks(std::mt19937& generator){ + for (size_t height = 0; height <= Tile::HEIGHT; height++){ + for (size_t width = 0; width <= Tile::WIDTH; width++){ + auto inside = [=](size_t x, size_t y){ + return x < width && y < height; + }; + + Tile tile = random_tile(generator); + tile.set_ones(width, height); + std::string error = find_wrong_tile_bit(tile, inside); + if (!error.empty()){ + return std::format("set_ones({}, {}): {}", width, height, error); + } + + const Tile original = random_tile(generator); + tile = original; tile.clear_padding(width, height); - for (size_t y = 0; y < tile.HEIGHT; ++y){ - for (size_t x = 0; x < tile.WIDTH; ++x){ - bool gt = y < height && x < width; - if (gt != tile.get_bit(x, y)){ - cout << "Tile::clear_padding(width = " << width << ", height = " << height << "), tile (x = " << - x << ", y = " << y << "), wrong bit. Should be " << gt << endl; - return 1; - } - } + error = find_wrong_tile_bit(tile, [&](size_t x, size_t y){ + return inside(x, y) && original.get_bit(x, y); + }); + if (!error.empty()){ + return std::format("clear_padding({}, {}): {}", width, height, error); } } } - sum = 0; - time_start = current_time(); - for (size_t i = 0; i < num_iters; ++i){ - tile.clear_padding(32, 4); - sum += tile.row(0) + tile.row(1) + tile.row(2) + tile.row(3); - } - time_end = current_time(); - ns = std::chrono::duration_cast(time_end - time_start).count(); - ms = ns / 1000000.; - cout << "Execution enforcer: " << sum << endl; - cout << "Running " << num_iters << " iters, Tile::clear_padding() time: " << ms / num_iters << " ms" << endl; + return ""; +} - tile.set_ones(35, 3); +// `invert()`, `^=`, `|=`, `&=` and `andnot()` on random tiles. `a.andnot(b)` keeps the +// bits of `a` that are not set in `b`. +template +std::string check_tile_bitwise(std::mt19937& generator){ + const Tile a = random_tile(generator); + const Tile b = random_tile(generator); + + Tile tile = a; tile.invert(); - for (size_t y = 0; y < tile.HEIGHT; ++y){ - for (size_t x = 0; x < tile.WIDTH; ++x){ - bool gt = !(x < 35 && y < 3); - if (gt != tile.get_bit(x, y)){ - cout << "Tile::invert(), tile (x = " << - x << ", y = " << y << "), wrong bit. Should be " << gt << endl; - return 1; - } - } + std::string error = find_wrong_tile_bit(tile, [&](size_t x, size_t y){ return !a.get_bit(x, y); }); + if (!error.empty()){ + return "invert(): " + error; + } + tile = a; + tile ^= b; + error = find_wrong_tile_bit(tile, [&](size_t x, size_t y){ return a.get_bit(x, y) != b.get_bit(x, y); }); + if (!error.empty()){ + return "operator^=(): " + error; } + tile = a; + tile |= b; + error = find_wrong_tile_bit(tile, [&](size_t x, size_t y){ return a.get_bit(x, y) || b.get_bit(x, y); }); + if (!error.empty()){ + return "operator|=(): " + error; + } + tile = a; + tile &= b; + error = find_wrong_tile_bit(tile, [&](size_t x, size_t y){ return a.get_bit(x, y) && b.get_bit(x, y); }); + if (!error.empty()){ + return "operator&=(): " + error; + } + tile = a; + tile.andnot(b); + error = find_wrong_tile_bit(tile, [&](size_t x, size_t y){ return a.get_bit(x, y) && !b.get_bit(x, y); }); + if (!error.empty()){ + return "andnot(): " + error; + } + return ""; +} - auto tile2 = tile; - tile.set_ones(35, 3); - tile2.set_ones(13, 6); - tile.andnot(tile2); - for (size_t y = 0; y < tile.HEIGHT; ++y){ - for (size_t x = 0; x < tile.WIDTH; ++x){ - bool gt = !(x < 13 && y < 6) && (x < 35 && y < 3); - if (gt != tile.get_bit(x, y)){ - cout << "Tile::andnot(), tile (x = " << - x << ", y = " << y << "), wrong bit. Should be " << gt << endl; - return 1; - } - } +// `row(y)` is the 64-bit word of row y, with bit x of the word at (x, y). `top()` is row 0 +// and `bottom()` is the last row. `set_bit()` changes only one bit. +template +std::string check_tile_rows(std::mt19937& generator){ + std::vector words(Tile::HEIGHT); + Tile tile; + for (size_t y = 0; y < Tile::HEIGHT; y++){ + words[y] = ((uint64_t)generator() << 32) | generator(); + tile.row(y) = words[y]; + } + std::string error = find_wrong_tile_bit(tile, [&](size_t x, size_t y){ return (words[y] >> x) & 1; }); + if (!error.empty()){ + return "row(): " + error; + } + if (tile.top() != words[0] || tile.bottom() != words[Tile::HEIGHT - 1]){ + return "top() or bottom() is not the first or last row"; + } + tile.top() = words[0] = ~words[0]; + tile.bottom() = words[Tile::HEIGHT - 1] = ~words[Tile::HEIGHT - 1]; + error = find_wrong_tile_bit(tile, [&](size_t x, size_t y){ return (words[y] >> x) & 1; }); + if (!error.empty()){ + return "writing top() or bottom(): " + error; } - tile.set_ones(35, 3); - { - uint64_t top_row = tile.top(); - for (size_t x = 0; x < tile.WIDTH; ++x){ - uint64_t bit = ((top_row >> x) & 1); - uint64_t gt = uint64_t(x < 35); - if (bit != gt){ - cout << "Tile::top(), tile (x = " << - x << "), wrong bit. Should be " << gt << endl; - return 1; + for (size_t y = 0; y < Tile::HEIGHT; y++){ + for (size_t x = 0; x < Tile::WIDTH; x++){ + const Tile original = tile; + const bool value = !original.get_bit(x, y); + tile.set_bit(x, y, value); + error = find_wrong_tile_bit(tile, [&](size_t bit_x, size_t bit_y){ + return bit_x == x && bit_y == y ? value : original.get_bit(bit_x, bit_y); + }); + if (!error.empty()){ + return std::format("set_bit({}, {}, {}): {}", x, y, (int)value, error); } } } - { - tile.set_zero(); - uint64_t& top_row = tile.top(); - top_row = 0xFFFFFFFFFFFFFFFF; - uint64_t top = tile.top(); - for (size_t x = 0; x < tile.WIDTH; ++x){ - uint64_t bit = ((top >> x) & 1); - if (bit != 1){ - cout << "& Tile::top(), tile (x = " << - x << "), wrong bit. Should be 1" << endl; - return 1; - } - } + return ""; +} + +// One of the `copy_to_shift_*()` functions, which OR a shifted copy of a tile into another +// tile. They are used by `submatrix()` to build each output tile from the (up to 4) input +// tiles that it overlaps. The first letter of the suffix is for x and the second for y: +// "p" means that destination bit x gets source bit x + shift, and "n" means that +// destination bit x gets source bit x - shift. +template +struct CopyToShift{ + const char* name; + void (Tile::*function)(Tile& tile, size_t shift_x, size_t shift_y) const; + bool positive_x; + bool positive_y; + + // The source bit that lands on destination bit (x, y), or false if none does. Going + // below 0 wraps around to a huge size_t, which is out of the tile too. + bool source_bit(const Tile& source, size_t shift_x, size_t shift_y, size_t x, size_t y) const{ + const size_t source_x = positive_x ? x + shift_x : x - shift_x; + const size_t source_y = positive_y ? y + shift_y : y - shift_y; + return source_x < Tile::WIDTH && source_y < Tile::HEIGHT && source.get_bit(source_x, source_y); } - { - tile.set_zero(); - uint64_t& bottom_row = tile.bottom(); - bottom_row = 0xFFFFFFFFFFFFFFFF; - uint64_t bottom = tile.bottom(); - for (size_t x = 0; x < tile.WIDTH; ++x){ - uint64_t bit = ((bottom >> x) & 1); - if (bit != 1){ - cout << "& Tile::bottom(), tile (x = " << - x << "), wrong bit. Should be 1" << endl; - return 1; +}; + +template +std::string check_tile_shifts(std::mt19937& generator){ + const CopyToShift shifts[] = { + {"copy_to_shift_pp", &Tile::copy_to_shift_pp, true, true}, + {"copy_to_shift_np", &Tile::copy_to_shift_np, false, true}, + {"copy_to_shift_pn", &Tile::copy_to_shift_pn, true, false}, + {"copy_to_shift_nn", &Tile::copy_to_shift_nn, false, false}, + }; + const Tile source = random_tile(generator); + const Tile destination = random_tile(generator); + for (const CopyToShift& shift : shifts){ + for (size_t shift_y = 0; shift_y < Tile::HEIGHT; shift_y++){ + for (size_t shift_x = 0; shift_x < Tile::WIDTH; shift_x++){ + Tile tile = destination; + (source.*shift.function)(tile, shift_x, shift_y); + const std::string error = find_wrong_tile_bit(tile, [&](size_t x, size_t y){ + return destination.get_bit(x, y) || shift.source_bit(source, shift_x, shift_y, x, y); + }); + if (!error.empty()){ + return std::format("{}(shift_x = {}, shift_y = {}): {}", shift.name, shift_x, shift_y, error); + } } } } - tile.set_zero(); - for (size_t y = 0; y < tile.HEIGHT; ++y){ - for (size_t x = 0; x < tile.WIDTH; ++x){ - tile.set_bit(x, y); - if (1 != tile.get_bit(x, y)){ - cout << "Tile::set_bit(), tile (x = " << - x << ", y = " << y << "), wrong bit. Should be 1" << endl; - return 1; - } - tile.set_bit(x, y, 1); - if (1 != tile.get_bit(x, y)){ - cout << "Tile::set_bit(), tile (x = " << - x << ", y = " << y << "), wrong bit. Should be 1" << endl; - return 1; - } - tile.set_bit(x, y, 0); - if (0 != tile.get_bit(x, y)){ - cout << "Tile::set_bit(), tile (x = " << - x << ", y = " << y << "), wrong bit. Should be 0" << endl; - return 1; + return ""; +} + +template +class Test_BinaryTile : public UnitTest{ +public: + Test_BinaryTile(const std::string& tile_name) + : UnitTest("Kernels::BinaryMatrix - tile - " + tile_name) + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ + std::mt19937 generator(0); + for (auto check : {check_tile_masks, check_tile_bitwise, check_tile_rows, check_tile_shifts}){ + const std::string error = check(generator); + if (!error.empty()){ + return "Error: " + error + "."; } } + return true; } +}; - std::srand(0); - auto src_tile = tile; - auto dst_tile = tile; - for (size_t num_tests = 0; num_tests < 30; ++num_tests){ - src_tile.set_zero(); - dst_tile.set_zero(); - for (size_t y = 0; y < tile.HEIGHT; ++y){ - for (size_t x = 0; x < tile.WIDTH; ++x){ - src_tile.set_bit(x, y, std::rand() % 2); - dst_tile.set_bit(x, y, std::rand() % 2); - } - } - auto test_copy_to_shift = [&](size_t shift_x, size_t shift_y, const std::string& function_name, - std::function gt_test, - std::function get_src_bit_test){ - // check against GT - for (size_t y = 0; y < tile.HEIGHT; ++y){ - for (size_t x = 0; x < tile.WIDTH; ++x){ - bool gt = false; - if (gt_test(shift_x, shift_y, x, y)){ - gt = dst_tile.get_bit(x, y) || get_src_bit_test(shift_x, shift_y, x, y); - // cout << "set the source bit with ||" << endl; - }else{ - // cout << "Use the original dest bit" << endl; - gt = dst_tile.get_bit(x, y); - } - if (tile.get_bit(x, y) != gt){ - cout << "Tile::" << function_name << "(shift_x=" << shift_x << ", shift_y=" << shift_y << "), tile (x = " << - x << ", y = " << y << "), wrong bit. Should be " << gt << endl; - cout << src_tile.dump() << endl; - cout << dst_tile.dump() << endl; - cout << tile.dump() << endl; - return 1; - } - } - } - return 0; - }; - for (size_t shift_y = 0; shift_y < tile.HEIGHT; ++shift_y){ - for (size_t shift_x = 0; shift_x < tile.WIDTH; ++shift_x){ - tile = dst_tile; - src_tile.copy_to_shift_pp(tile, shift_x, shift_y); - auto gt_test_pp = [&](size_t shift_x, size_t shift_y, size_t x, size_t y) -> bool{ - return x + shift_x < tile.WIDTH && y + shift_y < tile.HEIGHT; - }; - auto get_src_bit_test_pp = [&](size_t shift_x, size_t shift_y, size_t x, size_t y){ - return src_tile.get_bit(x + shift_x, y + shift_y); - }; - if (test_copy_to_shift(shift_x, shift_y, "copy_to_shift_pp", gt_test_pp, get_src_bit_test_pp) != 0){ - return 1; - } +#ifdef PA_AutoDispatch_arm64_20_M1 +// Check `PartialWordAccess_arm64_NEON`, which loads and stores only the first 0 to 16 +// bytes of a 16-byte NEON vector. NEON kernels use it for the leftover pixels at the end +// of a row. The test data sits in the middle of a buffer filled with `UNTOUCHED`: +// - Both loads must return the data bytes in the low lanes and 0 in the other lanes. +// - The store must write the data bytes and keep every other byte of the buffer. +class Test_PartialWordAccess_arm64_NEON : public UnitTest{ +public: + Test_PartialWordAccess_arm64_NEON() + : UnitTest("Kernels::PartialWordAccess - arm64_NEON") + {} - tile = dst_tile; - src_tile.copy_to_shift_np(tile, shift_x, shift_y); - auto gt_test_np = [&](size_t shift_x, size_t shift_y, size_t x, size_t y) -> bool{ - return x >= shift_x && y + shift_y < tile.HEIGHT; - }; - auto get_src_bit_test_np = [&](size_t shift_x, size_t shift_y, size_t x, size_t y){ - return src_tile.get_bit(x - shift_x, y + shift_y); - }; - if (test_copy_to_shift(shift_x, shift_y, "copy_to_shift_np", gt_test_np, get_src_bit_test_np) != 0){ - return 1; - } + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ + constexpr uint8_t UNTOUCHED = 99; + constexpr size_t START = 16; + + for (size_t bytes = 0; bytes <= 16; bytes++){ + const PartialWordAccess_arm64_NEON access(bytes); - tile = dst_tile; - src_tile.copy_to_shift_pn(tile, shift_x, shift_y); - auto gt_test_pn = [&](size_t shift_x, size_t shift_y, size_t x, size_t y) -> bool{ - return x + shift_x < tile.WIDTH && y >= shift_y; - }; - auto get_src_bit_test_pn = [&](size_t shift_x, size_t shift_y, size_t x, size_t y){ - return src_tile.get_bit(x + shift_x, y - shift_y); - }; - if (test_copy_to_shift(shift_x, shift_y, "copy_to_shift_pn", gt_test_pn, get_src_bit_test_pn) != 0){ - return 1; + uint8_t buffer[48]; + for (size_t i = 0; i < 48; i++){ + buffer[i] = i < START || i >= START + bytes ? UNTOUCHED : (uint8_t)(100 + i); + } + const uint8x16_t loads[] = { + access.load_int_no_read_past_end(buffer + START), + access.load_int_no_read_before_ptr(buffer + START), + }; + for (const uint8x16_t& loaded : loads){ + uint8_t lanes[16]; + vst1q_u8(lanes, loaded); + for (size_t i = 0; i < 16; i++){ + const int expected = i < bytes ? buffer[START + i] : 0; + if (lanes[i] != expected){ + return std::format("Error: loading {} bytes: lane {} is {} but should be {}.", bytes, i, (int)lanes[i], expected); + } } + } - tile = dst_tile; - src_tile.copy_to_shift_nn(tile, shift_x, shift_y); - auto gt_test_nn = [&](size_t shift_x, size_t shift_y, size_t x, size_t y) -> bool{ - return x >= shift_x && y >= shift_y; - }; - auto get_src_bit_test_nn = [&](size_t shift_x, size_t shift_y, size_t x, size_t y){ - return src_tile.get_bit(x - shift_x, y - shift_y); - }; - if (test_copy_to_shift(shift_x, shift_y, "copy_to_shift_nn", gt_test_nn, get_src_bit_test_nn) != 0){ - return 1; + uint8_t stored[48]; + for (size_t i = 0; i < 48; i++){ + stored[i] = UNTOUCHED; + } + access.store_int_no_past_end(stored + START, vld1q_u8(buffer + START)); + for (size_t i = 0; i < 48; i++){ + if (stored[i] != buffer[i]){ + return std::format("Error: storing {} bytes: byte {} is {} but should be {}.", bytes, i, (int)stored[i], (int)buffer[i]); } } } + return true; } +}; +#endif - cout << "Tile tests passed" << endl; - return 0; -} -int test_binary_matrix_tile(){ -#ifdef PA_AutoDispatch_arm64_20_M1 - if (test_binary_matrix_tile_t() != 0){ - return 1; - } - for (size_t num_bytes = 0; num_bytes <= 16; ++num_bytes){ - PartialWordAccess_arm64_NEON partial(num_bytes); +// Matrix tests: check every matrix operation that has code per matrix type, through the +// `PackedBinaryMatrix_IB` interface, against the pixels of a real image. - uint8_t buffer[48]; - for (int i = 0; i < 48; i++){ - buffer[i] = 99; - } - for (uint8_t i = 0; i < 16; i++){ - buffer[i + 16] = i; - } +// An ARGB color range: a pixel is in range if each of its A, R, G and B bytes is within +// the bytes of `mins` and `maxs`. +struct ColorRange{ + uint32_t mins; + uint32_t maxs; +}; - // uint8x16_t x = partial.load(buffer+16); - uint8x16_t x = partial.load_int_no_read_past_end(buffer + 16); - for (size_t i = 0; i < num_bytes; ++i){ - if (x[i] != buffer[i + 16]){ - cout << "Error: PartialWordAccess_arm64_NEON(" << num_bytes << ")::load_int_no_read_past_end(), i = " << i << " is " << int(x[i]) - << ", but should be " << int(buffer[i + 16]) << endl; - return 1; - } +// The ranges to test. Each has pixels both in and out of range on every test image. +constexpr ColorRange RANGES[] = { + {0xff000000, 0xff8080ff}, + {0xff204060, 0xffe0c0ff}, + {0xff60607f, 0xffc8c8c8}, + {0xffa0a0a0, 0xffffffff}, +}; + +bool in_range(uint32_t pixel, const ColorRange& range){ + for (int shift = 0; shift < 32; shift += 8){ + const uint32_t value = (pixel >> shift) & 0xff; + if (value < ((range.mins >> shift) & 0xff) || value > ((range.maxs >> shift) & 0xff)){ + return false; } - x = partial.load_int_no_read_before_ptr(buffer + 16); - for (size_t i = 0; i < num_bytes; ++i){ - if (x[i] != buffer[i + 16]){ - cout << "Error: PartialWordAccess_arm64_NEON(" << num_bytes << ")::load_int_no_read_before_ptr(), i = " << i << " is " << int(x[i]) - << ", but should be " << int(buffer[i + 16]) << endl; - return 1; + } + return true; +} + +// Compare `matrix` with `expected_bit(x, y)`: its size must be `width` x `height`, each bit +// must match, and each padding bit must be 0. The padding bits are the bits of the edge +// tiles past the right and bottom of the matrix. They must stay 0, since Waterfill and +// other code read whole tiles. Return a description of the first problem, or an empty +// string if there is none. +template +std::string find_wrong_bit( + const PackedBinaryMatrix_IB& matrix, size_t width, size_t height, + ExpectedBit expected_bit +){ + if (matrix.width() != width || matrix.height() != height){ + return std::format("size is {} x {} but should be {} x {}", matrix.width(), matrix.height(), width, height); + } + for (size_t y = 0; y < height; y++){ + for (size_t x = 0; x < width; x++){ + const bool expected = expected_bit(x, y); + if (matrix.get(x, y) != expected){ + return std::format("bit ({}, {}) should be {}", x, y, (int)expected); } } - for (int i = 0; i < 48; i++){ - buffer[i] = 99; + } + + // `dump_tiles()` prints every bit of every tile, padding included, as rows of '0' and + // '1' that end with '\n'. + size_t x = 0, y = 0; + for (char bit : matrix.dump_tiles()){ + if (bit == '\n'){ + x = 0; + y++; + continue; } - partial.store_int_no_past_end(buffer + 16, x); - for (size_t i = 0; i < num_bytes; ++i){ - if (x[i] != buffer[i + 16]){ - cout << "Error: PartialWordAccess_arm64_NEON(" << num_bytes << ")::store_int_no_past_end(), i = " << i << " is " << int(buffer[i + 16]) - << ", but should be " << int(x[i]) << endl; - return 1; - } + if (bit == '1' && (x >= width || y >= height)){ + return std::format("padding bit ({}, {}) is 1", x, y); } + x++; } -#endif - return 0; + return ""; } +// A rectangle to cut out with `submatrix()`. +struct SubmatrixBox{ + size_t x; + size_t y; + size_t width; + size_t height; +}; + + +// Check one binary matrix type on a real image: +// 1. `compress_rgb32_to_binary_range()` with one range at a time, and with all of `RANGES` +// in one pass. Bit (x, y) must be 1 if pixel (x, y) is in range. +// 2. `invert()`, `^=`, `|=`, `&=`, `set_ones()` and `set_zero()` on those matrices. +// 3. `submatrix()` on boxes that start inside or outside a tile, that end at a tile +// edge or inside a tile, and that go past the edge of the matrix. +// The image is a guard-page image, so the test also crashes if a compress function reads +// past the last pixel. class Test_BinaryMatrix : public UnitTest{ public: - Test_BinaryMatrix( - const std::string& image - ) - : UnitTest("Kernels::BinaryMatrix - " + image) + Test_BinaryMatrix(const std::string& type_name, BinaryMatrixType type, const std::string& image) + : UnitTest("Kernels::BinaryMatrix - " + type_name + " - " + image) + , m_type(type) , m_image(UNIT_TEST_RESOURCE_PATH() + image) {} virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ - // Copy the test image into guard-page memory with packed rows, so a - // kernel that reads or writes past the last pixel faults. - ImageRGB32 file_image(m_image); - GuardPageImageRGB32 image(file_image); + const ImageRGB32 file_image(m_image); + const GuardPageImageRGB32 image(file_image); + const size_t width = image.width(); + const size_t height = image.height(); + auto bit_in_range = [&](size_t range_index){ + return [&, range_index](size_t x, size_t y){ + return in_range(image.pixel(x, y), RANGES[range_index]); + }; + }; - if (test_binary_matrix_tile() != 0){ - return false; + // 1. Compress, one range at a time. + std::vector> matrices; + for (size_t c = 0; c < std::size(RANGES); c++){ + matrices.emplace_back(make_PackedBinaryMatrix(m_type, width, height)); + compress_rgb32_to_binary_range( + image.data(), image.bytes_per_row(), *matrices.back(), + RANGES[c].mins, RANGES[c].maxs + ); + const std::string error = find_wrong_bit(*matrices.back(), width, height, bit_in_range(c)); + if (!error.empty()){ + return std::format("Error: compress range [{:08x}, {:08x}]: {}.", RANGES[c].mins, RANGES[c].maxs, error); + } } - const size_t width = image.width(), height = image.height(); - - const Color min_color(0, 0, 0), max_color(63, 63, 63); - const uint32_t mins = uint32_t(min_color), maxs = uint32_t(max_color); - - auto binary_matrix = make_PackedBinaryMatrix(get_BinaryMatrixType(), width, height); - - auto time_start = current_time(); - compress_rgb32_to_binary_range( - image.data(), image.bytes_per_row(), *binary_matrix, mins, maxs - ); - auto time_end = current_time(); - size_t ns = std::chrono::duration_cast(time_end - time_start).count(); - double ms = ns / 1000000.; - cout << "One binary matrix creation. time: " << ms << " ms" << endl; - - size_t error_count = 0; - for (size_t y = 0; y < height; y++){ - for (size_t x = 0; x < width; x++){ - const Color color(image.pixel(x, y)); - bool in_range = (min_color.alpha() <= color.alpha() && color.alpha() <= max_color.alpha()); - in_range = in_range && (min_color.red() <= color.red() && color.red() <= max_color.red()); - in_range = in_range && (min_color.green() <= color.green() && color.green() <= max_color.green()); - in_range = in_range && (min_color.blue() <= color.blue() && color.blue() <= max_color.blue()); - - const bool v_default = binary_matrix->get(x, y); - - if (error_count < 10){ - if (v_default != in_range){ - cout << "Error: matrix (" << x << ", " << y << ") got " - << v_default << " but GT is " << in_range << endl; - ++error_count; - } - } + // 1. Compress, all ranges in one pass. + std::vector> multi_matrices; + std::vector filters; + for (const ColorRange& range : RANGES){ + multi_matrices.emplace_back(make_PackedBinaryMatrix(m_type, width, height)); + filters.emplace_back(*multi_matrices.back(), range.mins, range.maxs); + } + compress_rgb32_to_binary_range(image.data(), image.bytes_per_row(), filters.data(), filters.size()); + for (size_t c = 0; c < std::size(RANGES); c++){ + const std::string error = find_wrong_bit(*multi_matrices[c], width, height, bit_in_range(c)); + if (!error.empty()){ + return std::format("Error: compress all ranges at once, range [{:08x}, {:08x}]: {}.", RANGES[c].mins, RANGES[c].maxs, error); } } - if (error_count){ - return false; + scope.throw_if_cancelled(); + + // 2. Operations on the matrices of the first two ranges. + const PackedBinaryMatrix_IB& a = *matrices[0]; + const PackedBinaryMatrix_IB& b = *matrices[1]; + auto bit_a = bit_in_range(0); + auto bit_b = bit_in_range(1); + + std::unique_ptr matrix = a.clone(); + matrix->invert(); + std::string error = find_wrong_bit(*matrix, width, height, [&](size_t x, size_t y){ return !bit_a(x, y); }); + if (!error.empty()){ + return "Error: invert(): " + error + "."; } - - - // We try to wait for three seconds: - const size_t num_iters = size_t(3000 / ms); - time_start = current_time(); - for (size_t i = 0; i < num_iters; i++){ - compress_rgb32_to_binary_range( - image.data(), image.bytes_per_row(), *binary_matrix, mins, maxs + matrix = a.clone(); + *matrix ^= b; + error = find_wrong_bit(*matrix, width, height, [&](size_t x, size_t y){ return bit_a(x, y) != bit_b(x, y); }); + if (!error.empty()){ + return "Error: operator^=(): " + error + "."; + } + matrix = a.clone(); + *matrix |= b; + error = find_wrong_bit(*matrix, width, height, [&](size_t x, size_t y){ return bit_a(x, y) || bit_b(x, y); }); + if (!error.empty()){ + return "Error: operator|=(): " + error + "."; + } + matrix = a.clone(); + *matrix &= b; + error = find_wrong_bit(*matrix, width, height, [&](size_t x, size_t y){ return bit_a(x, y) && bit_b(x, y); }); + if (!error.empty()){ + return "Error: operator&=(): " + error + "."; + } + matrix->set_ones(); + error = find_wrong_bit(*matrix, width, height, [](size_t, size_t){ return true; }); + if (!error.empty()){ + return "Error: set_ones(): " + error + "."; + } + matrix->set_zero(); + error = find_wrong_bit(*matrix, width, height, [](size_t, size_t){ return false; }); + if (!error.empty()){ + return "Error: set_zero(): " + error + "."; + } + scope.throw_if_cancelled(); + + // 3. Submatrices of the first range's matrix. The parts past the edge of the matrix + // must be 0. The 64 x 64 box ends at a tile edge for every matrix type. + const SubmatrixBox boxes[] = { + {0, 0, width, height}, + {1, 1, width - 1, height - 1}, + {5, 3, 64, 64}, + {37, 11, 40, 32}, + {70, 9, 100, 100}, + {width + 3, 0, 10, 10}, + }; + for (const SubmatrixBox& box : boxes){ + const std::unique_ptr sub = a.submatrix( + box.x, box.y, box.width, box.height ); + error = find_wrong_bit(*sub, box.width, box.height, [&](size_t x, size_t y){ + x += box.x; + y += box.y; + return x < width && y < height && bit_a(x, y); + }); + if (!error.empty()){ + return std::format( + "Error: submatrix({}, {}, {}, {}): {}.", + box.x, box.y, box.width, box.height, error + ); + } } - time_end = current_time(); - ms = (double)std::chrono::duration_cast(time_end - time_start).count(); - cout << "Running " << num_iters << " iters, average creation impl. time: " << ms / (double)num_iters << " ms" << endl; - - // cout << binary_matrix->dump() << flush; - return true; - }; + } private: + BinaryMatrixType m_type; std::string m_image; }; +} + + + +// Add the tile tests, and a matrix test for each image and each binary matrix type that +// this build has and this CPU supports, including Default. void add_tests_BinaryMatrix(UnitTestDatabase& database){ + database.add>("64x4_Default"); + struct MatrixType{ + std::string name; + BinaryMatrixType type; + }; + std::vector types{ + {"64x4_Default", BinaryMatrixType::i64x4_Default}, + }; +#ifdef PA_AutoDispatch_x64_08_Nehalem + if (CPU_CAPABILITY_CURRENT.OK_08_Nehalem){ + types.push_back({"64x8_x64_SSE42", BinaryMatrixType::i64x8_x64_SSE42}); + } +#endif +#ifdef PA_AutoDispatch_x64_13_Haswell + if (CPU_CAPABILITY_CURRENT.OK_13_Haswell){ + types.push_back({"64x16_x64_AVX2", BinaryMatrixType::i64x16_x64_AVX2}); + } +#endif +#ifdef PA_AutoDispatch_x64_17_Skylake + if (CPU_CAPABILITY_CURRENT.OK_17_Skylake){ + types.push_back({"64x32_x64_AVX512", BinaryMatrixType::i64x32_x64_AVX512}); + types.push_back({"64x64_x64_AVX512", BinaryMatrixType::i64x64_x64_AVX512}); + } +#endif +#ifdef PA_AutoDispatch_arm64_20_M1 + if (CPU_CAPABILITY_CURRENT.OK_M1){ + database.add>("64x8_arm64_NEON"); + database.add(); + types.push_back({"64x8_arm64_NEON", BinaryMatrixType::arm64x8_x64_NEON}); + } +#endif + + // Images in the CommandLineTests repo. No width or height is a multiple of 8, so the + // tiles at the right and bottom edges are only partly used. + const std::vector images{ + "NintendoSwitch/Switch2/ghostassassin/system_settings_display_dark.png", // 1916 x 1078 + "OCR/Lv.1-0.png", // 237 x 77 + "PokemonSwSh/SelectionArrowFinder/mac_neon_error_1.png", // 106 x 410 + "OCR/german-nature-sanft.png", // 511 x 91 + }; + + for (const MatrixType& type : types){ + for (const std::string& image : images){ + database.add(type.name, type.type, image); + } + } } From ec764d050fa851c2c5d6fbfd280dca1018ca522a Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 23:01:22 -0700 Subject: [PATCH 6/7] Fix the replace flag of the multi-filter filter_rgb32_range() The overload that runs several range filters in one call passed the filter's pixels_in_range, which is not set yet, as replace_color_within_range. It should pass the filter's own flag, invert. This broke in 5bb2f457b. The PokemonLA MountDetector uses this overload. Also set pixels_in_range to 0 in both multi-filter structs. Co-Authored-By: Claude Opus 5.5 --- .../RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp | 2 +- .../ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp b/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp index 0c5943a5ec..12e94686ac 100644 --- a/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp +++ b/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.cpp @@ -171,7 +171,7 @@ void filter_rgb32_range( image, bytes_per_row, width, height, filter[c].data, filter[c].bytes_per_row, filter[c].replacement, - filter[c].pixels_in_range, + filter[c].invert, filter[c].mins, filter[c].maxs ); diff --git a/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h b/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h index d312e22151..23c9ddfc10 100644 --- a/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h +++ b/SerialPrograms/Source/Kernels/ImageFilters/RGB32_Range/Kernels_ImageFilter_RGB32_Range.h @@ -52,6 +52,7 @@ struct FilterRgb32RangeFilter{ , invert(p_invert) , mins(p_mins) , maxs(p_maxs) + , pixels_in_range(0) {} }; void filter_rgb32_range( @@ -94,6 +95,7 @@ struct ToBlackWhiteRgb32RangeFilter{ , mins(p_mins) , maxs(p_maxs) , in_range_black(p_in_range_black) + , pixels_in_range(0) {} }; void to_blackwhite_rgb32_range( From 18c141711a9406bed528667c10f47ce5c9707d91 Mon Sep 17 00:00:00 2001 From: Gin <> Date: Wed, 21 Oct 2026 23:02:45 -0700 Subject: [PATCH 7/7] Test the multi-filter overloads of the range filters Add a test that runs filter_rgb32_range() and to_blackwhite_rgb32_range() with 8 filters in one call, and checks each output against the scalar reference. It fails without the fix in the previous commit. Co-Authored-By: Claude Opus 5.5 --- .../Kernels_ImageFilter_Tests.cpp | 149 +++++++++++++++--- 1 file changed, 126 insertions(+), 23 deletions(-) diff --git a/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp b/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp index 511c7b172d..f35c15f57c 100644 --- a/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp +++ b/SerialPrograms/Source/Kernels/ImageFilters/Kernels_ImageFilter_Tests.cpp @@ -12,6 +12,7 @@ #include "CommonFramework/GlobalAutoPaths.h" #include "CommonFramework/ImageTypes/ImageRGB32.h" #include "Kernels/BinaryMatrix/Kernels_BinaryMatrix.h" +#include "RGB32_Range/Kernels_ImageFilter_RGB32_Range.h" #include "Tests/GuardPageImage.h" #include "Kernels_ImageFilter_Tests.h" @@ -201,6 +202,36 @@ std::string describe(const ColorRange& range){ return std::format("range [{:08x}, {:08x}]", range.mins, range.maxs); } +// Check one result of `filter_rgb32_range()` on `image`: the number of pixels in range +// that it returned, and its output image. Return a description of the first problem, or +// an empty string if there is none. +std::string check_filter_result( + const GuardPageImageRGB32& image, const GuardPageImageRGB32& output, size_t count, + const ColorRange& range, bool replace_color_within_range +){ + const size_t expected_count = count_in_range(image, range); + if (count != expected_count){ + return std::format("counted {} pixels in range but should be {}", count, expected_count); + } + return find_wrong_pixel(image, output, [&](uint32_t pixel){ + return in_range(pixel, range) == replace_color_within_range ? REPLACEMENT_COLOR : pixel; + }); +} + +// Same as `check_filter_result()`, for one result of `to_blackwhite_rgb32_range()`. +std::string check_blackwhite_result( + const GuardPageImageRGB32& image, const GuardPageImageRGB32& output, size_t count, + const ColorRange& range, bool in_range_black +){ + const size_t expected_count = count_in_range(image, range); + if (count != expected_count){ + return std::format("counted {} pixels in range but should be {}", count, expected_count); + } + return find_wrong_pixel(image, output, [&](uint32_t pixel){ + return in_range(pixel, range) == in_range_black ? BLACK : WHITE; + }); +} + // Check one implementation of `filter_rgb32_range()` and `to_blackwhite_rgb32_range()` @@ -230,7 +261,6 @@ class Test_ImageFilterRGB32Range : public UnitTest{ for (const ColorRange& range : RANGES){ scope.throw_if_cancelled(); - const size_t expected_count = count_in_range(image, range); for (bool replace_color_within_range : {true, false}){ const size_t count = m_implementation.filter( @@ -239,18 +269,12 @@ class Test_ImageFilterRGB32Range : public UnitTest{ REPLACEMENT_COLOR, replace_color_within_range, range.mins, range.maxs ); - const std::string error = find_wrong_pixel(image, output, [&](uint32_t pixel){ - return in_range(pixel, range) == replace_color_within_range ? REPLACEMENT_COLOR : pixel; - }); - const std::string name = std::format( - "filter_rgb32_range(), {}, replace_color_within_range = {}", - describe(range), replace_color_within_range - ); - if (count != expected_count){ - return std::format("Error: {}: counted {} pixels in range but should be {}.", name, count, expected_count); - } + const std::string error = check_filter_result(image, output, count, range, replace_color_within_range); if (!error.empty()){ - return std::format("Error: {}: {}.", name, error); + return std::format( + "Error: filter_rgb32_range(), {}, replace_color_within_range = {}: {}.", + describe(range), replace_color_within_range, error + ); } } @@ -261,18 +285,12 @@ class Test_ImageFilterRGB32Range : public UnitTest{ in_range_black, range.mins, range.maxs ); - const std::string error = find_wrong_pixel(image, output, [&](uint32_t pixel){ - return in_range(pixel, range) == in_range_black ? BLACK : WHITE; - }); - const std::string name = std::format( - "to_blackwhite_rgb32_range(), {}, in_range_black = {}", - describe(range), in_range_black - ); - if (count != expected_count){ - return std::format("Error: {}: counted {} pixels in range but should be {}.", name, count, expected_count); - } + const std::string error = check_blackwhite_result(image, output, count, range, in_range_black); if (!error.empty()){ - return std::format("Error: {}: {}.", name, error); + return std::format( + "Error: to_blackwhite_rgb32_range(), {}, in_range_black = {}: {}.", + describe(range), in_range_black, error + ); } } } @@ -286,6 +304,90 @@ class Test_ImageFilterRGB32Range : public UnitTest{ +// Check the overloads of `filter_rgb32_range()` and `to_blackwhite_rgb32_range()` that +// run several filters in one call, like `MountDetector` in PokemonLA does. They call the +// single-filter versions through the CPU dispatch, so this test runs once, not once per +// implementation. There is one filter for each of `RANGES` and each flag value, so the +// test also checks that each filter uses its own range and flag. +class Test_ImageFilterRGB32RangeMultiple : public UnitTest{ +public: + Test_ImageFilterRGB32RangeMultiple(const std::string& image) + : UnitTest("Kernels::ImageFilterRGB32Range - multiple filters - " + image) + , m_image(UNIT_TEST_RESOURCE_PATH() + image) + {} + + virtual UnitTestResult run(Logger& logger, CancellableScope& scope) const override{ + const ImageRGB32 file_image(m_image); + const GuardPageImageRGB32 image(file_image); + + struct FilterSetting{ + ColorRange range; + bool flag; + }; + std::vector settings; + std::vector> outputs; + for (const ColorRange& range : RANGES){ + for (bool flag : {true, false}){ + settings.push_back({range, flag}); + outputs.emplace_back(std::make_unique(image.width(), image.height())); + } + } + + std::vector filters; + for (size_t c = 0; c < settings.size(); c++){ + filters.emplace_back( + outputs[c]->data(), outputs[c]->bytes_per_row(), + REPLACEMENT_COLOR, settings[c].flag, + settings[c].range.mins, settings[c].range.maxs + ); + } + filter_rgb32_range( + image.data(), image.bytes_per_row(), image.width(), image.height(), + filters.data(), filters.size() + ); + for (size_t c = 0; c < settings.size(); c++){ + const std::string error = check_filter_result( + image, *outputs[c], filters[c].pixels_in_range, settings[c].range, settings[c].flag + ); + if (!error.empty()){ + return std::format( + "Error: filter_rgb32_range() with {} filters, {}, replace_color_within_range = {}: {}.", + filters.size(), describe(settings[c].range), settings[c].flag, error + ); + } + } + + std::vector blackwhite_filters; + for (size_t c = 0; c < settings.size(); c++){ + blackwhite_filters.emplace_back( + outputs[c]->data(), outputs[c]->bytes_per_row(), + settings[c].range.mins, settings[c].range.maxs, settings[c].flag + ); + } + to_blackwhite_rgb32_range( + image.data(), image.bytes_per_row(), image.width(), image.height(), + blackwhite_filters.data(), blackwhite_filters.size() + ); + for (size_t c = 0; c < settings.size(); c++){ + const std::string error = check_blackwhite_result( + image, *outputs[c], blackwhite_filters[c].pixels_in_range, settings[c].range, settings[c].flag + ); + if (!error.empty()){ + return std::format( + "Error: to_blackwhite_rgb32_range() with {} filters, {}, in_range_black = {}: {}.", + blackwhite_filters.size(), describe(settings[c].range), settings[c].flag, error + ); + } + } + return true; + } + +private: + std::string m_image; +}; + + + // Check one `filter_by_mask()` implementation on a real image, for each of `RANGES`. // The mask is a binary matrix of the implementation's type with a 1 bit for each pixel // in range. It is made one bit at a time with `PackedBinaryMatrix_IB::set()`, so it does @@ -396,6 +498,7 @@ void add_tests_ImageFilters(UnitTestDatabase& database){ "OCR/german-nature-sanft.png", // 511 x 91 }; + database.add(images[2]); for (const std::string& image : images){ for (const RangeFilterImplementation& implementation : range_filters){ database.add(implementation, image);