From patchwork Fri Oct 16 20:29:15 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Ray Strode X-Patchwork-Id: 28490 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 B07A5C324C for ; Fri, 16 Oct 2026 20:29:37 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 67A2C68E8D; Fri, 16 Oct 2026 22:29:37 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="QolPdtg0"; dkim-atps=neutral Received: from mail-ua1-x936.google.com (mail-ua1-x936.google.com [IPv6:2607:f8b0:4864:20::936]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id E0C1D68E82 for ; Fri, 16 Oct 2026 22:29:32 +0200 (CEST) Received: by mail-ua1-x936.google.com with SMTP id a1e0cc1a2514c-982d6efbeadso102778241.0 for ; Fri, 16 Oct 2026 13:29:32 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792182572; x=1792787372; darn=lists.libcamera.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=ze+6S/LQa3OwT7S6GxdDe3/AapVYcHNXZHTZzZXKwBA=; b=QolPdtg0HvkwcPnG/up89W8NMcbUgilNDP/7cH666JTh+ry3LxKmhmwKVq5BhZjZ5N DhJzgLGq+x0p0k4w1Wj4jANzkoZJ9ZazWJYud1dcgPL5qFd/E0FLusapPWppZ0J2OgDA E6aaVWsY/Iy6Y8Ysu/gqF6A/3kGNstgX0eQexp8+FX+5VJj2Qjy29jSSIM+da8cz997K DiidX3KLyzbKEtXKlhqq8KHkwWk4OMMJ3h9sp30x9pbdFGg3Kt7V5I4PGhAiMS16xlT4 8gWngmNlbdtZaLKys7l8CPfAo2HAXjoxZnj05X+JVzte4VwzOszllMja39CRdS1OU+ek hDww== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792182572; x=1792787372; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=ze+6S/LQa3OwT7S6GxdDe3/AapVYcHNXZHTZzZXKwBA=; b=B6zYSkxDxjmct2k5BT8CTTQonODo16zf8RzMMW38kavZ8viaenUmVLzXe5EYmc52xF aIyAV1TPhccOmzAN4fUi7gYanmrYQDO0125Kpmn8ORmmWem4VbBnCfFQY+FEUkdPeIxK X6ddeI2gx54gmGfV+/JkNfUTcUDZCgIphCJP9PISuCLO8oNv2qvPe69EEahZJ0euVo+U SZEC/DRkMqG9QTwGjwA08C0SHKCE7ISMivyb/s0LIf1IMJtMAjqrBXim8DT9DUG5Ebu1 j682VYHGHIauSKiMx6PWfwh4fS+h6ZvxD83YXffNNO/sjwfWbG0Wx7ahydlJ2OjgVU3k TwUw== X-Gm-Message-State: AFq9FYLKMJh2dSYgHnSdDms8ihyfvrYFT5NdsylMjuvrOM7+UyLTMY5c 3AdQhzpxrJWF7Oa0l74XKrNKGGj5c+7586LuVEz4Pyzz3mRZJL/tp81ledc+FOgOMl8= X-Gm-Gg: AYBFou3T7aAyae6pJsJtdrrljZmuF5f2r8SuTErrTAJGO6sJJrRXVL+VuutgNqkzDyi xppUpVF11jPBlXSLouNX9uur1RaTcYn+ePqunkdZzelL2e0lD4n0gWGsD2o8xJoXLl5zpJLr6tY Tfm0nd7indaibsZbMl/MwRpHUYfzP0QOKaI1wdR/Qt6RR/YTTlvRoaJeTnVu38ncHZRmairBjQb KDOFstlIvZtM2iVSG7s/G9nFARG3tZ5MZTbxsjXJl11nMyjl9r6cxB+lrn30OR1W+28oJPkbcEY cVKFyIBbLRmq+luRNv+9Rq8zJRP2RsGvL0DqBa88dUleE5HCbfoDTPdurjowXUUdOZlF+JdDjhe uRaT7Z/6c9nEiqk+PLrx9nblHbJwGiGgh4VHEIzdVNU423wnXQxQXkPWxQ8D9cIV/Z8d1vJxQfE KVoTKHloH/DqNgZkxJc7Pz9eCokZ2Px+1Pp3JNIW2tmCWLvrvksloWdeuvMkg8YYfHHOO35vFnW cGJ+JS0PyGZZ7el/CnTF0dZPe8WwtAIpfnI3LU= X-Received: by 2002:a05:6102:3911:b0:7bf:d9dd:82f8 with SMTP id ada2fe7eead31-7cc36a18080mr196941137.21.1792182571653; Fri, 16 Oct 2026 13:29:31 -0700 (PDT) Received: from localhost ([2601:19c:4a00:897d:3391:ed89:5571:41a8]) by smtp.gmail.com with ESMTPSA id a1e0cc1a2514c-990835c29eesm2271202241.4.2026.10.16.13.29.30 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 16 Oct 2026 13:29:30 -0700 (PDT) From: Ray Strode To: libcamera-devel@lists.libcamera.org Cc: johannes.goede@oss.qualcomm.com, laurent.pinchart@ideasonboard.com, kieran.bingham@ideasonboard.com Subject: [PATCH 6/7] tests: Validate pixel statistics for the visible GPU crop Date: Fri, 16 Oct 2026 16:29:15 -0400 Message-ID: <20261016202916.1405813-7-halfline@gmail.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261016202916.1405813-1-halfline@gmail.com> References: <8fd58439-d19e-4539-962f-a6110be5c971@oss.qualcomm.com> <20261016202916.1405813-1-halfline@gmail.com> MIME-Version: 1.0 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 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 --- 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 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 +#include +#include +#include +#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 "libcamera/internal/software_isp/swisp_stats.h" +#include "libcamera/internal/software_isp/swstats_cpu.h" + +#include +#include + +#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(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(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 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(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; + 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(x), input.size.height }, + { rightEdge, 0, input.size.width - rightEdge, input.size.height }, + { 0, 0, input.size.width, static_cast(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