| Message ID | 20261015194335.287960-5-halfline@gmail.com |
|---|---|
| State | New |
| Headers | show |
| Series |
|
| Related | show |
On Thu, Oct 15, 2026 at 03:43:35PM -0400, Ray Strode wrote: > From: Ray Strode <rstrode@redhat.com> > > The GPU debayer path centers the statistics window on the sensor region > visible in the output. > > The project lacks a regression check that the measured window follows the > visible region across different output sizes and Bayer patterns. > > This commit adds a statistics test for 8-bit and packed RAW10 input. It > checks the expected sample count, finite colour ratios, symmetric metering, > a response to pixels inside crop edges, and exclusion of pixels outside > them. Meson registers the test when software ISP and EGL support are > available. Same as for 2/4. > Signed-off-by: Ray Strode <rstrode@redhat.com> > --- > test/meson.build | 3 + > test/software-isp-gpu-stats.cpp | 188 ++++++++++++++++++++++++++++++++ > 2 files changed, 191 insertions(+) > create mode 100644 test/software-isp-gpu-stats.cpp > > diff --git a/test/meson.build b/test/meson.build > index 644a8cba..e7510646 100644 > --- a/test/meson.build > +++ b/test/meson.build > @@ -57,60 +57,63 @@ internal_tests = [ > {'name': 'event-dispatcher', 'sources': ['event-dispatcher.cpp']}, > {'name': 'event-thread', 'sources': ['event-thread.cpp']}, > {'name': 'file', 'sources': ['file.cpp']}, > {'name': 'flags', 'sources': ['flags.cpp']}, > {'name': 'hotplug-cameras', 'sources': ['hotplug-cameras.cpp']}, > {'name': 'matrix', 'sources': ['matrix.cpp']}, > {'name': 'message', 'sources': ['message.cpp']}, > {'name': 'object', 'sources': ['object.cpp']}, > {'name': 'object-delete', 'sources': ['object-delete.cpp']}, > {'name': 'object-invoke', 'sources': ['object-invoke.cpp']}, > {'name': 'pixel-format', 'sources': ['pixel-format.cpp']}, > {'name': 'shared-fd', 'sources': ['shared-fd.cpp']}, > {'name': 'signal-threads', 'sources': ['signal-threads.cpp']}, > {'name': 'threads', 'sources': 'threads.cpp', 'dependencies': [libthreads]}, > {'name': 'timer', 'sources': ['timer.cpp']}, > {'name': 'timer-fail', 'sources': ['timer-fail.cpp'], 'should_fail': true}, > {'name': 'timer-thread', 'sources': ['timer-thread.cpp']}, > {'name': 'unique-fd', 'sources': ['unique-fd.cpp']}, > {'name': 'utils', 'sources': ['utils.cpp']}, > {'name': 'value-node', 'sources': ['value-node.cpp']}, > {'name': 'vector', 'sources': ['vector.cpp']}, > {'name': 'yaml-parser', 'sources': ['yaml-parser.cpp']}, > ] > > if softisp_enabled and mesa_works > softisp_gpu_includes = include_directories('../src/libcamera/software_isp') > internal_tests += [ > {'name': 'software-isp-gpu-crop', 'sources': ['software-isp-gpu-crop.cpp'], > 'dependencies': [libegl, libglesv2], > 'include_directories': softisp_gpu_includes}, > + {'name': 'software-isp-gpu-stats', 'sources': ['software-isp-gpu-stats.cpp'], > + 'dependencies': [libegl, libglesv2], > + 'include_directories': softisp_gpu_includes}, > ] > endif > > internal_non_parallel_tests = [ > {'name': 'fence', 'sources': ['fence.cpp']}, > {'name': 'mapped-buffer', 'sources': ['mapped-buffer.cpp']}, > ] > > foreach test : public_tests > deps = [libcamera_public] > if 'dependencies' in test > deps += test['dependencies'] > endif > > exe = executable(test['name'], test['sources'], > dependencies : deps, > implicit_include_directories : false, > link_with : test_libraries, > include_directories : test_includes_public) > > test(test['name'], exe, should_fail : test.get('should_fail', false)) > endforeach > > foreach test : internal_tests > deps = [libcamera_private] > if 'dependencies' in test > deps += test['dependencies'] > endif > > includes = [test_includes_internal] > diff --git a/test/software-isp-gpu-stats.cpp b/test/software-isp-gpu-stats.cpp > new file mode 100644 > index 00000000..2575c1c4 > --- /dev/null > +++ b/test/software-isp-gpu-stats.cpp > @@ -0,0 +1,188 @@ > +/* SPDX-License-Identifier: GPL-2.0-or-later */ > + > +#include <algorithm> > +#include <cmath> > +#include <iostream> > +#include <memory> > +#include <numeric> > +#include <stdint.h> > +#include <sys/mman.h> > +#include <vector> > + > +#include <libcamera/camera_manager.h> > +#include <libcamera/formats.h> > +#include <libcamera/stream.h> > + > +#include "libcamera/internal/mapped_framebuffer.h" > +#include "libcamera/internal/shared_mem_object.h" > + > +#include "debayer_egl.h" > +#include "test.h" > + > +using namespace libcamera; > + > +class GpuStatisticsTest : public Test > +{ > +protected: > + int run() override > + { > + CameraManager manager; > + /* configure() stores the display without creating a rendering context. */ > + EGLDisplay display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, > + EGL_DEFAULT_DISPLAY, nullptr); > + if (display == EGL_NO_DISPLAY) > + return TestSkip; > + for (PixelFormat format : { formats::SBGGR8, formats::SGRBG8, > + formats::SBGGR10_CSI2P, formats::SGRBG10_CSI2P }) { > + bool packed = format == formats::SBGGR10_CSI2P || format == formats::SGRBG10_CSI2P; > + bool bggr = format == formats::SBGGR8 || format == formats::SBGGR10_CSI2P; > + for (Size outputSize : { Size(1280, 720), Size(640, 480), Size(1920, 1080) }) { > + auto statistics = std::make_unique<SwStatsCpu>(manager); > + SwStatsCpu *stats = statistics.get(); > + if (!stats->isValid()) > + return TestFail; > + DebayerEGL debayer(std::move(statistics), manager, display); > + StreamConfiguration input, output; > + input.pixelFormat = format; > + input.size = { 1928, 1092 }; > + input.stride = debayer.preferredInputStride(format, input.size); > + input.bufferCount = 4; > + output.pixelFormat = formats::XBGR8888; > + output.size = outputSize; > + output.stride = output.size.width * 4; > + output.bufferCount = 4; > + if (debayer.configure(input, { output }, false)) > + return TestFail; > + > + auto *shared = static_cast<const SwIspStats *>(mmap(nullptr, sizeof(SwIspStats), > + PROT_READ, MAP_SHARED, stats->getStatsFD().get(), 0)); > + if (shared == MAP_FAILED) > + return TestFail; > + > + SharedMem raw("gpu-statistics-test", input.stride * input.size.height); > + if (!raw) { > + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); > + return TestFail; > + } > + std::fill(raw.mem().begin(), raw.mem().end(), 0); > + FrameBuffer::Plane plane; > + plane.fd = raw.fd(); > + plane.offset = 0; > + plane.length = raw.mem().size(); > + std::vector<FrameBuffer::Plane> planes = { plane }; > + FrameBuffer buffer(planes); > + MappedFrameBuffer mapped(&buffer, MappedFrameBuffer::MapFlag::Read); > + if (!mapped.isValid()) { > + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); > + return TestFail; > + } > + > + /* Meter the original input buffer, as the GPU debayer does. */ > + auto measure = [&](const Rectangle &target) { > + auto *pixels = raw.mem().data(); > + for (unsigned int y = 0; y < input.size.height; y++) { > + for (unsigned int x = 0; x < input.size.width; x++) { > + bool green = ((x + y) & 1) == (bggr ? 1 : 0); > + bool white = x >= static_cast<unsigned int>(target.x) && > + x < target.x + target.width && > + y >= static_cast<unsigned int>(target.y) && > + y < target.y + target.height; > + unsigned int offset = packed ? x / 4 * 5 + x % 4 : x; > + /* Packed RAW10 statistics use the eight high bits. */ > + pixels[y * input.stride + offset] = green || white ? 150 : 75; > + } > + } > + stats->processFrame(0, 0, mapped); > + return *shared; > + }; > + > + /* > + * A 4:3 output sees 1456 sensor columns. A 16:9 output > + * sees all native columns, excluding four/eight border > + * columns for 8-bit/packed RAW10. The 8-bit top edge at > + * row 5 is rounded down to row 4 by the Bayer alignment. > + */ > + Rectangle visible = outputSize == Size(640, 480) > + ? Rectangle(236, 0, 1456, 1092) > + : packed ? Rectangle(4, 6, 1920, 1080) > + : Rectangle(2, 4, 1924, 1082); > + uint64_t expectedSamples = uint64_t((visible.width + 3) / 4) * > + ((visible.height + 3) / 4); > + SwIspStats background = measure({}); > + uint64_t samples = std::accumulate(background.yHistogram.begin(), > + background.yHistogram.end(), uint64_t{ 0 }); > + if (!background.valid || !samples || samples != expectedSamples || > + !background.sum_.r() || > + !background.sum_.g() || !background.sum_.b()) { > + std::cerr << "Invalid background statistics: " << samples > + << " samples, expected " << expectedSamples << std::endl; > + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); > + return TestFail; > + } > + > + SwIspStats left = measure({ 418, 0, 128, input.size.height }); > + SwIspStats right = measure({ 1382, 0, 128, input.size.height }); > + if (!left.valid || !right.valid || !left.sum_.g() || !right.sum_.g() || > + left.sum_.r() <= background.sum_.r() || > + right.sum_.r() <= background.sum_.r() || > + left.yHistogram == background.yHistogram || > + right.yHistogram == background.yHistogram) { > + std::cerr << "No response to pixels inside the visible crop" << std::endl; > + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); > + return TestFail; > + } > + > + double leftRatio = double(left.sum_.r()) / left.sum_.g(); > + double rightRatio = double(right.sum_.r()) / right.sum_.g(); > + std::cout << format << " " << outputSize << " left R/G=" << leftRatio > + << " right R/G=" << rightRatio << std::endl; > + if (!std::isfinite(leftRatio) || !std::isfinite(rightRatio) || > + std::abs(leftRatio - rightRatio) > 0.001 || > + left.sum_.g() != right.sum_.g() || > + left.sum_.b() != right.sum_.b() || > + left.yHistogram != right.yHistogram) { > + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); > + return TestFail; > + } > + > + unsigned int x = visible.x; > + unsigned int y = visible.y; > + unsigned int rightEdge = x + visible.width; > + unsigned int bottomEdge = y + visible.height; > + for (Rectangle target : { Rectangle(x, y, 8, visible.height), > + Rectangle(rightEdge - 8, y, 8, visible.height), > + Rectangle(x, y, visible.width, 4), > + Rectangle(x, bottomEdge - 4, visible.width, 4) }) { > + SwIspStats inside = measure(target); > + if (!inside.valid || inside.sum_.r() <= background.sum_.r() || > + inside.sum_.b() <= background.sum_.b() || > + inside.yHistogram == background.yHistogram) { > + std::cerr << "No response inside crop edge " << target << std::endl; > + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); > + return TestFail; > + } > + } > + for (Rectangle target : { Rectangle(0, 0, x, input.size.height), > + Rectangle(rightEdge, 0, input.size.width - rightEdge, > + input.size.height), > + Rectangle(0, 0, input.size.width, y), > + Rectangle(0, bottomEdge, input.size.width, > + input.size.height - bottomEdge) }) { > + if (!target.width || !target.height) > + continue; > + SwIspStats outside = measure(target); > + if (!outside.valid || background.sum_ != outside.sum_ || > + background.yHistogram != outside.yHistogram) { > + std::cerr << "Metered pixels outside crop edge " << target << std::endl; > + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); > + return TestFail; > + } > + } > + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); > + } > + } > + return TestPass; > + } > +}; > + > +TEST_REGISTER(GpuStatisticsTest)
diff --git a/test/meson.build b/test/meson.build index 644a8cba..e7510646 100644 --- a/test/meson.build +++ b/test/meson.build @@ -57,60 +57,63 @@ internal_tests = [ {'name': 'event-dispatcher', 'sources': ['event-dispatcher.cpp']}, {'name': 'event-thread', 'sources': ['event-thread.cpp']}, {'name': 'file', 'sources': ['file.cpp']}, {'name': 'flags', 'sources': ['flags.cpp']}, {'name': 'hotplug-cameras', 'sources': ['hotplug-cameras.cpp']}, {'name': 'matrix', 'sources': ['matrix.cpp']}, {'name': 'message', 'sources': ['message.cpp']}, {'name': 'object', 'sources': ['object.cpp']}, {'name': 'object-delete', 'sources': ['object-delete.cpp']}, {'name': 'object-invoke', 'sources': ['object-invoke.cpp']}, {'name': 'pixel-format', 'sources': ['pixel-format.cpp']}, {'name': 'shared-fd', 'sources': ['shared-fd.cpp']}, {'name': 'signal-threads', 'sources': ['signal-threads.cpp']}, {'name': 'threads', 'sources': 'threads.cpp', 'dependencies': [libthreads]}, {'name': 'timer', 'sources': ['timer.cpp']}, {'name': 'timer-fail', 'sources': ['timer-fail.cpp'], 'should_fail': true}, {'name': 'timer-thread', 'sources': ['timer-thread.cpp']}, {'name': 'unique-fd', 'sources': ['unique-fd.cpp']}, {'name': 'utils', 'sources': ['utils.cpp']}, {'name': 'value-node', 'sources': ['value-node.cpp']}, {'name': 'vector', 'sources': ['vector.cpp']}, {'name': 'yaml-parser', 'sources': ['yaml-parser.cpp']}, ] if softisp_enabled and mesa_works softisp_gpu_includes = include_directories('../src/libcamera/software_isp') internal_tests += [ {'name': 'software-isp-gpu-crop', 'sources': ['software-isp-gpu-crop.cpp'], 'dependencies': [libegl, libglesv2], 'include_directories': softisp_gpu_includes}, + {'name': 'software-isp-gpu-stats', 'sources': ['software-isp-gpu-stats.cpp'], + 'dependencies': [libegl, libglesv2], + 'include_directories': softisp_gpu_includes}, ] endif internal_non_parallel_tests = [ {'name': 'fence', 'sources': ['fence.cpp']}, {'name': 'mapped-buffer', 'sources': ['mapped-buffer.cpp']}, ] foreach test : public_tests deps = [libcamera_public] if 'dependencies' in test deps += test['dependencies'] endif exe = executable(test['name'], test['sources'], dependencies : deps, implicit_include_directories : false, link_with : test_libraries, include_directories : test_includes_public) test(test['name'], exe, should_fail : test.get('should_fail', false)) endforeach foreach test : internal_tests deps = [libcamera_private] if 'dependencies' in test deps += test['dependencies'] endif includes = [test_includes_internal] diff --git a/test/software-isp-gpu-stats.cpp b/test/software-isp-gpu-stats.cpp new file mode 100644 index 00000000..2575c1c4 --- /dev/null +++ b/test/software-isp-gpu-stats.cpp @@ -0,0 +1,188 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include <algorithm> +#include <cmath> +#include <iostream> +#include <memory> +#include <numeric> +#include <stdint.h> +#include <sys/mman.h> +#include <vector> + +#include <libcamera/camera_manager.h> +#include <libcamera/formats.h> +#include <libcamera/stream.h> + +#include "libcamera/internal/mapped_framebuffer.h" +#include "libcamera/internal/shared_mem_object.h" + +#include "debayer_egl.h" +#include "test.h" + +using namespace libcamera; + +class GpuStatisticsTest : public Test +{ +protected: + int run() override + { + CameraManager manager; + /* configure() stores the display without creating a rendering context. */ + EGLDisplay display = eglGetPlatformDisplay(EGL_PLATFORM_SURFACELESS_MESA, + EGL_DEFAULT_DISPLAY, nullptr); + if (display == EGL_NO_DISPLAY) + return TestSkip; + for (PixelFormat format : { formats::SBGGR8, formats::SGRBG8, + formats::SBGGR10_CSI2P, formats::SGRBG10_CSI2P }) { + bool packed = format == formats::SBGGR10_CSI2P || format == formats::SGRBG10_CSI2P; + bool bggr = format == formats::SBGGR8 || format == formats::SBGGR10_CSI2P; + for (Size outputSize : { Size(1280, 720), Size(640, 480), Size(1920, 1080) }) { + auto statistics = std::make_unique<SwStatsCpu>(manager); + SwStatsCpu *stats = statistics.get(); + if (!stats->isValid()) + return TestFail; + DebayerEGL debayer(std::move(statistics), manager, display); + StreamConfiguration input, output; + input.pixelFormat = format; + input.size = { 1928, 1092 }; + input.stride = debayer.preferredInputStride(format, input.size); + input.bufferCount = 4; + output.pixelFormat = formats::XBGR8888; + output.size = outputSize; + output.stride = output.size.width * 4; + output.bufferCount = 4; + if (debayer.configure(input, { output }, false)) + return TestFail; + + auto *shared = static_cast<const SwIspStats *>(mmap(nullptr, sizeof(SwIspStats), + PROT_READ, MAP_SHARED, stats->getStatsFD().get(), 0)); + if (shared == MAP_FAILED) + return TestFail; + + SharedMem raw("gpu-statistics-test", input.stride * input.size.height); + if (!raw) { + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); + return TestFail; + } + std::fill(raw.mem().begin(), raw.mem().end(), 0); + FrameBuffer::Plane plane; + plane.fd = raw.fd(); + plane.offset = 0; + plane.length = raw.mem().size(); + std::vector<FrameBuffer::Plane> planes = { plane }; + FrameBuffer buffer(planes); + MappedFrameBuffer mapped(&buffer, MappedFrameBuffer::MapFlag::Read); + if (!mapped.isValid()) { + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); + return TestFail; + } + + /* Meter the original input buffer, as the GPU debayer does. */ + auto measure = [&](const Rectangle &target) { + auto *pixels = raw.mem().data(); + for (unsigned int y = 0; y < input.size.height; y++) { + for (unsigned int x = 0; x < input.size.width; x++) { + bool green = ((x + y) & 1) == (bggr ? 1 : 0); + bool white = x >= static_cast<unsigned int>(target.x) && + x < target.x + target.width && + y >= static_cast<unsigned int>(target.y) && + y < target.y + target.height; + unsigned int offset = packed ? x / 4 * 5 + x % 4 : x; + /* Packed RAW10 statistics use the eight high bits. */ + pixels[y * input.stride + offset] = green || white ? 150 : 75; + } + } + stats->processFrame(0, 0, mapped); + return *shared; + }; + + /* + * A 4:3 output sees 1456 sensor columns. A 16:9 output + * sees all native columns, excluding four/eight border + * columns for 8-bit/packed RAW10. The 8-bit top edge at + * row 5 is rounded down to row 4 by the Bayer alignment. + */ + Rectangle visible = outputSize == Size(640, 480) + ? Rectangle(236, 0, 1456, 1092) + : packed ? Rectangle(4, 6, 1920, 1080) + : Rectangle(2, 4, 1924, 1082); + uint64_t expectedSamples = uint64_t((visible.width + 3) / 4) * + ((visible.height + 3) / 4); + SwIspStats background = measure({}); + uint64_t samples = std::accumulate(background.yHistogram.begin(), + background.yHistogram.end(), uint64_t{ 0 }); + if (!background.valid || !samples || samples != expectedSamples || + !background.sum_.r() || + !background.sum_.g() || !background.sum_.b()) { + std::cerr << "Invalid background statistics: " << samples + << " samples, expected " << expectedSamples << std::endl; + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); + return TestFail; + } + + SwIspStats left = measure({ 418, 0, 128, input.size.height }); + SwIspStats right = measure({ 1382, 0, 128, input.size.height }); + if (!left.valid || !right.valid || !left.sum_.g() || !right.sum_.g() || + left.sum_.r() <= background.sum_.r() || + right.sum_.r() <= background.sum_.r() || + left.yHistogram == background.yHistogram || + right.yHistogram == background.yHistogram) { + std::cerr << "No response to pixels inside the visible crop" << std::endl; + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); + return TestFail; + } + + double leftRatio = double(left.sum_.r()) / left.sum_.g(); + double rightRatio = double(right.sum_.r()) / right.sum_.g(); + std::cout << format << " " << outputSize << " left R/G=" << leftRatio + << " right R/G=" << rightRatio << std::endl; + if (!std::isfinite(leftRatio) || !std::isfinite(rightRatio) || + std::abs(leftRatio - rightRatio) > 0.001 || + left.sum_.g() != right.sum_.g() || + left.sum_.b() != right.sum_.b() || + left.yHistogram != right.yHistogram) { + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); + return TestFail; + } + + unsigned int x = visible.x; + unsigned int y = visible.y; + unsigned int rightEdge = x + visible.width; + unsigned int bottomEdge = y + visible.height; + for (Rectangle target : { Rectangle(x, y, 8, visible.height), + Rectangle(rightEdge - 8, y, 8, visible.height), + Rectangle(x, y, visible.width, 4), + Rectangle(x, bottomEdge - 4, visible.width, 4) }) { + SwIspStats inside = measure(target); + if (!inside.valid || inside.sum_.r() <= background.sum_.r() || + inside.sum_.b() <= background.sum_.b() || + inside.yHistogram == background.yHistogram) { + std::cerr << "No response inside crop edge " << target << std::endl; + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); + return TestFail; + } + } + for (Rectangle target : { Rectangle(0, 0, x, input.size.height), + Rectangle(rightEdge, 0, input.size.width - rightEdge, + input.size.height), + Rectangle(0, 0, input.size.width, y), + Rectangle(0, bottomEdge, input.size.width, + input.size.height - bottomEdge) }) { + if (!target.width || !target.height) + continue; + SwIspStats outside = measure(target); + if (!outside.valid || background.sum_ != outside.sum_ || + background.yHistogram != outside.yHistogram) { + std::cerr << "Metered pixels outside crop edge " << target << std::endl; + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); + return TestFail; + } + } + munmap(const_cast<SwIspStats *>(shared), sizeof(SwIspStats)); + } + } + return TestPass; + } +}; + +TEST_REGISTER(GpuStatisticsTest)