From patchwork Thu Oct 15 19:43:35 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Ray Strode X-Patchwork-Id: 28465 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id DE91DC335A for ; Thu, 15 Oct 2026 19:49:50 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 8F77A68E2A; Thu, 15 Oct 2026 21:49:47 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="gWw1iBN5"; dkim-atps=neutral Received: from mail-qk1-x733.google.com (mail-qk1-x733.google.com [IPv6:2607:f8b0:4864:20::733]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 334DB68E0E for ; Thu, 15 Oct 2026 21:43:50 +0200 (CEST) Received: by mail-qk1-x733.google.com with SMTP id af79cd13be357-93e64d52949so538189785a.1 for ; Thu, 15 Oct 2026 12:43:50 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792093429; x=1792698229; darn=lists.libcamera.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to:content-type; bh=9vbVthZKRbu4rGra3VoQrnOTaEFsozlW/F6QcPqK1qM=; b=gWw1iBN5cqIPsgpY1zR7L76sJ63rSSy1ku/oeN7MglPD0qBYzKppcTRigRNLZaZPKW jp1pGcop2prmkgKbMn7IwkOnLqGh+D5ptMqE0kxsWX22lLbOBmAH9EEyWCw2kZzW2cXU /C5Lo9JMWcHwVyHXS/LdaYF8pNGVgbWl72+gJl+TDoWADg+vuMMZqXWylvkpJA1IL13z bIG5YFX1AlDmPV8OiVnVKMMT7KczjkwXzFOHMRTWpuDLAQY8K+CkSqL2hIhTeP0wbBr0 em1+BayKAQvJg3UM9GQmcUsbbuRsLcAlfu0+8fWMholSoAhlf1S9nHLr1Mb6xgpQCB7o LW1A== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792093429; x=1792698229; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-gg:x-gm-message-state:from:to :cc:subject:date:message-id:reply-to:content-type; bh=9vbVthZKRbu4rGra3VoQrnOTaEFsozlW/F6QcPqK1qM=; b=dj63LnUP4eiti1bry3dWt3A36QZEpCBQF+13gsMM13EgnzCD0meGw7Xo9uf7/VHZDA XFoOvLN3lv37rM3AR3luq3YIRNRwZqFVHt0Zi9oyQZipP53NRk3vZSFZh/G3NHgRqkyG j/qyOjfgBV/i/qFwamsrXViKEA/YdDLGkp1D0rfVZVaEVv8vg0mb8UbIJS+oqNx5/1jp SUbeOPKKtum5EsXtJ8rFspfkd5QkiNlMppox41hULfuvc0a0yanCgJ3bRvnsK1kgBSVG HeY7l3jYzDl71Q895r50YpkRggwBtto2n6vMl9kHcOdaY5+256O/yYlPBHgtSYhKFiiC ZjDw== X-Gm-Message-State: AFq9FYI/ohS0w7AE+697MuXopb4rjEvRWYylm+aVUIyGtFgbVaKL+Mf6 ioRzBTsXg05thYQn3J3uvyiKC5U0CbK0PbdnnA1nyjMixFklh06o+y9K2hugr7JN49I= X-Gm-Gg: AYBFou3XbT/ihosamLcZZ8LfOO4FKZLl3MeNl7IckB7+tod2DyihcCIYcNkNiQHR0K4 GaPp002oRxwsVFuSNhOmYYaKhV8Jq3tEdKN5oOFBJbviA1CG4e2TqFd3S18Lcb9Ygxc/fp8eoHm 3GuT9D93KvgJFj/J5U6ayjQrMJExts1viGNK5NR9OhSFYVl3sKPhjbCDF1vO5tPOLbiPHd7Blt5 WFa4MZcR3yzl1sZpLogEbmSNfQeKKcjNVwUSHVucv8cs0syXqmpol2xnvtvhkLXkT5X/2Zl2IRm OYo6ADIBZiVUHgDHdmTnmd2Iz5482UULSAZhImofJ7U4b0N2SzqFOTUDKdlTJrNFovI33jnsSQN IW8pvbnIZeRWaNVIaBeDMsNMFqO80j1jmG4Iw48HuM8A04/E4LEY6yBaOveb0iAtU0eYKfIlIBG 2LOKUuABeIivGoZzAA5MAn90c8JbLGjhKzcJ8si/pPzMhFKVOYznSeYAObJioycLwjGuMW9uIhn y5JmvT/KUd4OlCdbKyjj5wItQ== X-Received: by 2002:a05:620a:46a0:b0:93e:9292:1b9e with SMTP id af79cd13be357-93ebd0cc5ecmr3298300985a.1.1792093428785; Thu, 15 Oct 2026 12:43:48 -0700 (PDT) Received: from localhost ([144.121.52.163]) by smtp.gmail.com with ESMTPSA id af79cd13be357-93f1a18176esm15370385a.20.2026.10.15.12.43.48 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 15 Oct 2026 12:43:48 -0700 (PDT) From: Ray Strode To: libcamera-devel@lists.libcamera.org Subject: [PATCH 4/4] tests: Validate visible GPU statistics across output sizes Date: Thu, 15 Oct 2026 15:43:35 -0400 Message-ID: <20261015194335.287960-5-halfline@gmail.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261015194335.287960-1-halfline@gmail.com> References: <20261015194335.287960-1-halfline@gmail.com> MIME-Version: 1.0 X-Mailman-Approved-At: Thu, 15 Oct 2026 21:49:41 +0200 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From: Ray Strode 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. Signed-off-by: Ray Strode --- 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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#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(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(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(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 planes = { plane }; + FrameBuffer buffer(planes); + MappedFrameBuffer mapped(&buffer, MappedFrameBuffer::MapFlag::Read); + if (!mapped.isValid()) { + munmap(const_cast(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(target.x) && + x < target.x + target.width && + y >= static_cast(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(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(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(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(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(shared), sizeof(SwIspStats)); + return TestFail; + } + } + munmap(const_cast(shared), sizeof(SwIspStats)); + } + } + return TestPass; + } +}; + +TEST_REGISTER(GpuStatisticsTest)