From patchwork Fri Oct 16 20:29:14 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Ray Strode X-Patchwork-Id: 28489 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 1F308C324C for ; Fri, 16 Oct 2026 20:29:36 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id C5E7068E7A; Fri, 16 Oct 2026 22:29:35 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="M/Qc6HsT"; dkim-atps=neutral Received: from mail-ua1-x933.google.com (mail-ua1-x933.google.com [IPv6:2607:f8b0:4864:20::933]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id B0C4F68E51 for ; Fri, 16 Oct 2026 22:29:31 +0200 (CEST) Received: by mail-ua1-x933.google.com with SMTP id a1e0cc1a2514c-990703f817fso180540241.1 for ; Fri, 16 Oct 2026 13:29:31 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792182570; x=1792787370; 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=1LGZ+rGrPlSG7hr8w6CsXp+KWk5U0yHcC8sMgUhjO8g=; b=M/Qc6HsTaL0Amo4MIfavZizMkQIffGkrIhBC7VCTJubxyq4HGyjWecJrHLdeF9DLX/ 8eP74oGs2Wk56cfEfbH5DJ176a08pos5cHCUNYdwpfNdGp01sXRWfAxEE9LufqkGYdQE Bru5qPDTEbctNKA7wwQzCDHwXqt08eoW0aQKnxQIDKuWJtZhevmVdkmDbxPaottsdluF UxDOptrSVOppDWlg8EW6A26xhFDZQgZsUgyhrkogRDQzeKdX1A4CTH3vnAu9KlA3ad5k Yll3ZQFmoSITdgivFdGlpCeozZsNitGPJnUoeBBJu4USEQgSLsYDsdMbP6IiIp90ZdP9 p74w== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792182570; x=1792787370; 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=1LGZ+rGrPlSG7hr8w6CsXp+KWk5U0yHcC8sMgUhjO8g=; b=kNZZ6xXwd0bORknAZOyIyciV5X9JO8BOos0bsByYrJqxi8ZhAN/yfbh+/B6xDwZ1Hu aPkJEF2B5/Zqz2ven6mqoDtgllDpDyj0wNOzNGw68M2/iWkYWptiAjGMZc/aV7/w9iXy MsiLF/GaCd37gu6up4us1QdYvroxjtJMCl3ZS/3eAn/vTuMDZ9qpRd6GI1NgMOHrIK4j 6tbnt6g/3SEuwLyeddfPz8NJkiQZ4HJ7xImKdGJra7oTZN3ZpGO5KBhdjDg6iNGMo1+U ihDMS595XQZmDFrZbNAk51Cx5eoBeCqn0hazbCh1Q2t+eegi4Ijk/+cBFv21ZPq72JNQ Vh9w== X-Gm-Message-State: AFq9FYKR+5H224S7WMlai9gWrKj/1x8oZfRM1V3/8BcA1mS9zvooyaeh goJdZVDjM5SamV4kiE7bCU+IRbWf3+/b0pqLsqEBcuyrs/UNkPfJAt5vj2V1iqCwUoQ= X-Gm-Gg: AYBFou1PTnpzq7LDoupK1+iWPvWgju8PwBd2e2uHCOEh5kbibtKeE0XWwRI1lJC+Buc q+Vo4rS7uLoPtB6PDxyvSz5q9YV00aj+9O8PU/W4/E3VUIECV06EEUGUcLuqS8Cwcw3ftxphDyc rpTLdxZbsUMW7xbqOwGVzGyn03Sj2TYzyVCIUGwig9Q+s5rN6KGXn6BuY3DhPj3cTngFrpfPSto /p0RULOgpt0w7Ehf3HxjOb+SrZyhBPpo16YsFpU/c97GDSeIvAwFGBvaSOZD+bzcJzWe8PKKPFP aRzBtGaLHqdK7fDO7HMas/0HciBEHZExBaGxDtduYxLQ9zRuP0IVHIFh+8KnATMkQnv+xCQevpv NE0rAJwiils1Sw8+b5lz3LMvA2GSN/7dGtm3uPQ3FHc1tjTaZX9PHKv2FrGW35TMJvtKBOHHixs 3marKKE418P5/m/xb6vhBofsGxU2KcOreFoQrkjwqx/NVFrdblsqtgEvB3rlqI6Hx1qWfaya9Hj BurHPT+yIx1SZM9BXFBPSUid0EI X-Received: by 2002:a05:6102:3f0f:b0:7cc:16e2:c1a8 with SMTP id ada2fe7eead31-7cc3691f500mr290074137.8.1792182570475; Fri, 16 Oct 2026 13:29:30 -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.29 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Fri, 16 Oct 2026 13:29:29 -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 5/7] tests: Validate rendered crops in the integration suite Date: Fri, 16 Oct 2026 16:29:14 -0400 Message-ID: <20261016202916.1405813-6-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 rendered software ISP pixels in the opt-in integration test suite. The geometry unit tests check crop calculations. They do not exercise shader uniforms, the viewport or rasterization. The EGL drawing interface allows those parts of the production path to run with CPU-uploaded textures and pixel readback, including on software renderers without DMA-BUF support. The test checks centered horizontal and vertical stripes and compares resized output with a centered crop of the native render. Cases cover two Bayer patterns in 8-bit and packed RAW10 formats at four output sizes. The llvmpipe test setup selects surfaceless Mesa software rendering. An unavailable EGL display fails this setup so CI cannot silently skip the rendering coverage. The normal setup skips when no display is available. Signed-off-by: Ray Strode --- test/integration/meson.build | 2 +- test/integration/software_isp/gpu-crop.cpp | 222 +++++++++++++++++++++ test/integration/software_isp/meson.build | 28 +++ 3 files changed, 251 insertions(+), 1 deletion(-) create mode 100644 test/integration/software_isp/gpu-crop.cpp create mode 100644 test/integration/software_isp/meson.build diff --git a/test/integration/meson.build b/test/integration/meson.build index 51e97101..8a293951 100644 --- a/test/integration/meson.build +++ b/test/integration/meson.build @@ -1,3 +1,3 @@ # SPDX-License-Identifier: CC0-1.0 -# Integration test subdirectories register tests in the 'integration' suite. +subdir('software_isp') diff --git a/test/integration/software_isp/gpu-crop.cpp b/test/integration/software_isp/gpu-crop.cpp new file mode 100644 index 00000000..c327f3a9 --- /dev/null +++ b/test/integration/software_isp/gpu-crop.cpp @@ -0,0 +1,222 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ +/* + * Copyright (C) 2026, Red Hat Inc. + * + * Software ISP rendered crop tests + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include + +#include "libcamera/internal/egl.h" +#include "libcamera/internal/software_isp/debayer_params.h" +#include "libcamera/internal/software_isp/swstats_cpu.h" + +#include +#include + +#include "debayer_egl.h" +#include "test.h" + +using namespace libcamera; + +class TextureDebayer : public DebayerEGL +{ +public: + using DebayerEGL::DebayerEGL; + using DebayerEGL::render; +}; + +class GpuCropTest : public Test +{ +protected: + int init() override + { + display_ = eGL::probeDisplay(); + if (display_ == EGL_NO_DISPLAY) { + std::cout << "No surfaceless EGL display available" << std::endl; + return utils::secure_getenv("LIBCAMERA_TEST_REQUIRE_EGL") ? TestFail : TestSkip; + } + + return TestPass; + } + + int run() override + { + CameraManager manager; + for (PixelFormat format : { formats::SBGGR8, formats::SGRBG8, + formats::SBGGR10_CSI2P, formats::SGRBG10_CSI2P }) { + for (bool vertical : { true, false }) { + std::vector reference; + Size native; + for (Size size : { Size(1920, 1080), Size(1280, 720), + Size(640, 480), Size(512, 768) }) { + std::vector image; + if (render(manager, format, size, vertical, &image, &native)) + return TestFail; + + if (!checkCenter(image, size, vertical)) { + std::cerr << format << " " << size + << " stripe is not centered" << std::endl; + return TestFail; + } + + if (reference.empty()) { + if (size != native) + return TestFail; + reference = std::move(image); + continue; + } + + /* Compare with a centered crop of the native render. */ + double scale = std::max(double(size.width) / native.width, + double(size.height) / native.height); + double error = 0; + for (unsigned int y = 0; y < size.height; y++) { + double offsetY = (y + 0.5 - size.height / 2.0) / scale; + int ry = std::lround(native.height / 2.0 + offsetY - 0.5); + for (unsigned int x = 0; x < size.width; x++) { + double offsetX = (x + 0.5 - size.width / 2.0) / scale; + int rx = std::lround(native.width / 2.0 + offsetX - 0.5); + unsigned int actual = (y * size.width + x) * 4; + unsigned int expected = (ry * native.width + rx) * 4; + for (unsigned int c = 0; c < 3; c++) + error += std::abs(int(image[actual + c]) - + reference[expected + c]); + } + } + error /= size.width * size.height * 3; + std::cout << format << " " << size << " " + << (vertical ? "vertical" : "horizontal") + << " stripe: mean pixel error " << error << std::endl; + /* Allow interpolation and rounding at stripe edges. */ + if (error > 2.0) + return TestFail; + } + } + } + + return TestPass; + } + + void cleanup() override + { + eglTerminate(display_); + } + +private: + bool checkCenter(const std::vector &image, const Size &size, bool vertical) + { + unsigned int length = vertical ? size.width : size.height; + double sum = 0; + unsigned int count = 0; + for (unsigned int i = 0; i < length; i++) { + unsigned int x = vertical ? i : size.width / 2; + unsigned int y = vertical ? size.height / 2 : i; + /* The achromatic scene is independent of channel ordering. */ + if (image[(y * size.width + x) * 4 + 1] > 128) { + sum += i + 0.5; + count++; + } + } + if (!count) + return false; + + double center = sum / count; + return std::abs(center - length / 2.0) <= 1.0; + } + + bool createTexture(eGLImage &image, const void *data) + { + glActiveTexture(image.texture_unit_); + glBindTexture(GL_TEXTURE_2D, image.texture_); + glTexImage2D(GL_TEXTURE_2D, 0, image.format_, image.width_, image.height_, + 0, image.format_, GL_UNSIGNED_BYTE, data); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + return glGetError() == GL_NO_ERROR; + } + + int render(CameraManager &manager, PixelFormat format, const Size &size, + bool vertical, std::vector *image, Size *native) + { + auto stats = std::make_unique(manager); + if (!stats->isValid()) + return TestFail; + + TextureDebayer debayer(std::move(stats), manager, display_); + bool packed = format == formats::SBGGR10_CSI2P || format == formats::SGRBG10_CSI2P; + StreamConfiguration input, output; + input.pixelFormat = format; + input.size = { packed ? 1928U : 1924U, 1080 }; + input.stride = debayer.preferredInputStride(format, input.size); + input.bufferCount = 1; + output.pixelFormat = formats::XBGR8888; + output.size = size; + std::tie(output.stride, output.frameSize) = + debayer.strideAndFrameSize(output.pixelFormat, size); + output.bufferCount = 1; + *native = debayer.sizes(format, input.size).max; + if (debayer.configure(input, { output }, false)) + return TestFail; + + /* Destroy the textures before releasing their context. */ + utils::scope_exit stop([&] { debayer.stop(); }); + if (debayer.start()) + return TestFail; + + std::cout << "GL renderer: " << glGetString(GL_RENDERER) << std::endl; + std::vector raw(input.stride * input.size.height, 0); + for (unsigned int y = 0; y < input.size.height; y++) { + for (unsigned int x = 0; x < input.size.width; x++) { + unsigned int coordinate = vertical ? x : y; + unsigned int middle = (vertical ? input.size.width : input.size.height) / 2; + bool bright = coordinate >= middle - 64 && coordinate < middle + 64; + unsigned int offset = packed ? x / 4 * 5 + x % 4 : x; + /* RAW10 stores the high eight bits before the group's low bits. */ + raw[y * input.stride + offset] = bright ? 224 : 32; + } + } + + eGLImage source(GL_LUMINANCE, input.stride, input.size.height, + input.stride, GL_TEXTURE0, 0); + eGLImage target(GL_RGBA, size.width, size.height, output.stride, GL_TEXTURE1, 1); + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + if (!createTexture(source, raw.data()) || !createTexture(target, nullptr)) { + std::cerr << "Unable to allocate test textures" << std::endl; + return TestFail; + } + + if (debayer.render(source, target, DebayerParams{})) + return TestFail; + + image->resize(size.width * size.height * 4); + glPixelStorei(GL_PACK_ALIGNMENT, 1); + glReadPixels(0, 0, size.width, size.height, GL_RGBA, GL_UNSIGNED_BYTE, image->data()); + if (glGetError() != GL_NO_ERROR) { + std::cerr << "Unable to read rendered pixels" << std::endl; + return TestFail; + } + + return TestPass; + } + + EGLDisplay display_ = EGL_NO_DISPLAY; +}; + +TEST_REGISTER(GpuCropTest) diff --git a/test/integration/software_isp/meson.build b/test/integration/software_isp/meson.build new file mode 100644 index 00000000..2ec17ea0 --- /dev/null +++ b/test/integration/software_isp/meson.build @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: CC0-1.0 + +if not softisp_enabled or not mesa_works + subdir_done() +endif + +add_test_setup('llvmpipe', env : { + 'EGL_PLATFORM': 'surfaceless', + 'GALLIUM_DRIVER': 'llvmpipe', + 'LIBGL_ALWAYS_SOFTWARE': '1', + 'LIBCAMERA_TEST_REQUIRE_EGL': '1', +}) + +software_isp_integration_tests = [ + {'name': 'software-isp-gpu-crop', 'sources': ['gpu-crop.cpp']}, +] + +foreach test : software_isp_integration_tests + exe = executable(test['name'], test['sources'], + dependencies : [libcamera_private, libegl, libglesv2], + implicit_include_directories : false, + link_with : test_libraries, + include_directories : [test_includes_internal, + '../../../src/libcamera/software_isp/']) + + test(test['name'], exe, suite : ['integration', 'software_isp'], + timeout : 120) +endforeach