[6/7] tests: Validate pixel statistics for the visible GPU crop
diff mbox series

Message ID 20261016202916.1405813-7-halfline@gmail.com
State New
Headers show
Series
  • tests: Separate suites and check software ISP crops
Related show

Commit Message

Ray Strode Oct. 16, 2026, 8:29 p.m. UTC
From: Ray Strode <rstrode@redhat.com>

This commit checks the pixels measured by the EGL backend's configured
statistics engine in the integration suite.

The geometry unit test verifies the expected crop rectangle, but it cannot
check that statistics actually sample that region in the original sensor
buffer.

Synthetic input checks the sample count, symmetric responses within the
crop, and pixel inclusion and exclusion at its boundaries. Cases cover two
Bayer patterns in 8-bit and packed RAW10 formats at three output sizes. The
test configures the EGL backend without creating a rendering context, then
processes the sensor buffer with its CPU statistics engine.

Signed-off-by: Ray Strode <rstrode@redhat.com>
---
 test/integration/software_isp/gpu-stats.cpp | 208 ++++++++++++++++++++
 test/integration/software_isp/meson.build   |   1 +
 2 files changed, 209 insertions(+)
 create mode 100644 test/integration/software_isp/gpu-stats.cpp

Patch
diff mbox series

diff --git a/test/integration/software_isp/gpu-stats.cpp b/test/integration/software_isp/gpu-stats.cpp
new file mode 100644
index 00000000..617f9836
--- /dev/null
+++ b/test/integration/software_isp/gpu-stats.cpp
@@ -0,0 +1,208 @@ 
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2026, Red Hat Inc.
+ *
+ * Software ISP statistics crop integration tests
+ */
+
+#include <algorithm>
+#include <cmath>
+#include <iostream>
+#include <memory>
+#include <numeric>
+#include <stdint.h>
+#include <sys/mman.h>
+#include <utility>
+#include <vector>
+
+#include <libcamera/base/utils.h>
+
+#include <libcamera/camera_manager.h>
+#include <libcamera/formats.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/geometry.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/mapped_framebuffer.h"
+#include "libcamera/internal/shared_mem_object.h"
+#include "libcamera/internal/software_isp/swisp_stats.h"
+#include "libcamera/internal/software_isp/swstats_cpu.h"
+
+#include <EGL/egl.h>
+#include <EGL/eglext.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 utils::secure_getenv("LIBCAMERA_TEST_REQUIRE_EGL") ? TestFail : 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;
+
+				void *mapping = mmap(nullptr, sizeof(SwIspStats), PROT_READ, MAP_SHARED,
+						     stats->getStatsFD().get(), 0);
+				if (mapping == MAP_FAILED)
+					return TestFail;
+				utils::scope_exit unmap([&] {
+					munmap(mapping, sizeof(SwIspStats));
+				});
+				const auto *shared = static_cast<const SwIspStats *>(mapping);
+
+				SharedMem raw("gpu-statistics-test", input.stride * input.size.height);
+				if (!raw)
+					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())
+					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;
+				if (outputSize == Size(640, 480))
+					visible = { 236, 0, 1456, 1092 };
+				else if (packed)
+					visible = { 4, 6, 1920, 1080 };
+				else
+					visible = { 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;
+
+					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;
+
+					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) {
+					return TestFail;
+				}
+
+				int x = visible.x;
+				int y = visible.y;
+				int rightEdge = x + visible.width;
+				int bottomEdge = y + visible.height;
+				const Rectangle insideRegions[] = {
+					{ x, y, 8, visible.height },
+					{ rightEdge - 8, y, 8, visible.height },
+					{ x, y, visible.width, 4 },
+					{ x, bottomEdge - 4, visible.width, 4 },
+				};
+				for (Rectangle target : insideRegions) {
+					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;
+
+						return TestFail;
+					}
+				}
+				const Rectangle outsideRegions[] = {
+					{ 0, 0, static_cast<unsigned int>(x), input.size.height },
+					{ rightEdge, 0, input.size.width - rightEdge, input.size.height },
+					{ 0, 0, input.size.width, static_cast<unsigned int>(y) },
+					{ 0, bottomEdge, input.size.width, input.size.height - bottomEdge },
+				};
+				for (Rectangle target : outsideRegions) {
+					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;
+
+						return TestFail;
+					}
+				}
+			}
+		}
+		return TestPass;
+	}
+};
+
+TEST_REGISTER(GpuStatisticsTest)
diff --git a/test/integration/software_isp/meson.build b/test/integration/software_isp/meson.build
index 2ec17ea0..0d75f9b7 100644
--- a/test/integration/software_isp/meson.build
+++ b/test/integration/software_isp/meson.build
@@ -13,6 +13,7 @@  add_test_setup('llvmpipe', env : {
 
 software_isp_integration_tests = [
     {'name': 'software-isp-gpu-crop', 'sources': ['gpu-crop.cpp']},
+    {'name': 'software-isp-gpu-stats', 'sources': ['gpu-stats.cpp']},
 ]
 
 foreach test : software_isp_integration_tests