From patchwork Thu Oct 15 19:43:32 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Ray Strode X-Patchwork-Id: 28462 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 0A890C3316 for ; Thu, 15 Oct 2026 19:49:48 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id CAEAF68E17; Thu, 15 Oct 2026 21:49:45 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="aZw1q5bn"; dkim-atps=neutral Received: from mail-qv1-xf2a.google.com (mail-qv1-xf2a.google.com [IPv6:2607:f8b0:4864:20::f2a]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id C84BF68E06 for ; Thu, 15 Oct 2026 21:43:47 +0200 (CEST) Received: by mail-qv1-xf2a.google.com with SMTP id 6a1803df08f44-9178aabd30dso56357056d6.1 for ; Thu, 15 Oct 2026 12:43:47 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792093427; x=1792698227; 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=ebe8FD7l513a+k0oPXIiPYPmPSSM8yZTgjPaSBvBQac=; b=aZw1q5bnfx3UOQJq09BDo1NbjlrviaOMSuu8oKwSMY1jE3UlmIO/9FtjHIWmwZ+y3z oKYNrjbVHOGI3ntrpG2XATnblml8L8KJ5jZGeL5bz5ObBDtpNrmZqIntE8sGpjZMt8SQ T+WpFLiaEc3Fxt2wV2i8XVieeVNDiwAUw4RsqRFFLElD3TQpbkeiMDW2rJah3fiQiBDm sQO4EZntIGKgFuhViR8527BqOsXBD4G9Q3S8u8ih8W5zCHd8QJ5IdlXNYf/w37s5X3G5 XlHfuAAvp0hFEHyYBCaAMtnrxdHVDAZSFEG4BPznJRFpAtPFKekqcLJBPplYHRjp4ShY MqgA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792093427; x=1792698227; 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=ebe8FD7l513a+k0oPXIiPYPmPSSM8yZTgjPaSBvBQac=; b=cHfT3WkD3UEpm/nMm/B9GYqJXOpKh+DXZmTpLR5G/RbJTexbWebFR+zly4mR42KtoE sYvTh6OBmKWTfBzkgdZfk/6hAaqfwgN7lQUHXaeKKfn2k8YCWBV3YbxVL7t4r1HODe5D Coovja2jTIfrhKS7nn/eWqSUkdv0hTUdAIVgLLJNvM8Q3bDf83E/5NBiEekmpL3DUdWj ylTw0r+C1s2oF06yXdj4zdq/Ld+XjqKh/mSTMn12UJg/J3X0PzgIPptFCHepVPPnQRqq UoToWbvESMW7Gjyunzc22qJyyiTHwZ7sBC4C1JN6KYn743Neh/8VxWqZW7v6cKaJM6cB GgKw== X-Gm-Message-State: AFq9FYIgvTb2hBwmjOCBxv6d4EPmEwTy8EHgqNAhT9lfW6GpQQsEPP29 fGdPYsTJz6+RQi04bIJmGlBgk0oUeGPrkAklsYgLBbRoyM8by5iKXpcCz9z25e5gsN4= X-Gm-Gg: AYBFou302c72mzLIDNrdBJHZAd8nl8dRxLvwqcZhjJA/Wul9tdSIHd3Pm6rjCn6vSnf I389IAJM5c45A9VSsX60VRjl+naYO7GEJJx5FPrgHCkVwe6inaKsTu3l7IDU87kfCI6DR/fI7Bs e6+y/6/kENz0+SpjSBd47YpQXyQOxya37cwkPfzUzLFKrRcLnvltexmPKC7B9lrHoODB7mrpaj1 0F1ETN/5R3KtTnMpp5uKLnzAR4RG/MCzabTrXXAJG59AXQwRC99PPd2JRB5SH9dvlYpRL5OqaSY TgE4IMfkFDl6ZJB1A2yIVyCSADdS0Sv/j0DZWJvkiGrxRDF84v+aLRgM0vh3cRBkTKbmdqoCjeF FEtCiOtM7isrzMpL5HwuFNFZRFG66am0NfVq9gPY6/5BAPO473C5F8fouIRgDTRb77hzxCjzSva bEHtrrYQgYQrNv/6yW5duL57JLfqnu+lMPrdnlA9DqvAn7s8+CHQZb+ByOG2eS9kCPUZq/2Zkn/ QVwcI6beZIUvRsxoh8je4GjzxevULGVSqGr X-Received: by 2002:a05:620a:440e:b0:93e:9834:3863 with SMTP id af79cd13be357-93ebd22a0d3mr3296902385a.15.1792093426498; Thu, 15 Oct 2026 12:43:46 -0700 (PDT) Received: from localhost ([144.121.52.163]) by smtp.gmail.com with ESMTPSA id af79cd13be357-93f1a18176esm15370385a.20.2026.10.15.12.43.45 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 15 Oct 2026 12:43:46 -0700 (PDT) From: Ray Strode To: libcamera-devel@lists.libcamera.org Subject: [PATCH 1/4] software-isp: Center the GPU image crop Date: Thu, 15 Oct 2026 15:43:32 -0400 Message-ID: <20261015194335.287960-2-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 scales the image to fill a requested output size and crops any excess image area. Its projection positions the scaled image against one side of the output, so aspect-ratio changes and Bayer border removal shift the visible scene. This commit centers the projection on the output region in sensor coordinates, distributing the crop across both sides. Signed-off-by: Ray Strode --- src/libcamera/software_isp/debayer_egl.cpp | 23 +++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp index 300822f9..0f515c5e 100644 --- a/src/libcamera/software_isp/debayer_egl.cpp +++ b/src/libcamera/software_isp/debayer_egl.cpp @@ -364,70 +364,87 @@ std::vector DebayerEGL::formats(PixelFormat inputFormat) std::tuple DebayerEGL::strideAndFrameSize(const PixelFormat &outputFormat, const Size &size) { /* Align stride to 256 bytes as a generic GPU memory access alignment */ const PixelFormatInfo &info = PixelFormatInfo::info(outputFormat); return std::make_tuple(info.stride(size.width, 0, 256), info.frameSize(size, 256)); } uint32_t DebayerEGL::preferredInputStride(const PixelFormat &inputFormat, const Size &size) { const PixelFormatInfo &info = PixelFormatInfo::info(inputFormat); return info.stride(size.width, 0, 256); } void DebayerEGL::setShaderVariableValues(eGLImage &eglImageIn, const DebayerParams ¶ms) { /* * Raw Bayer 8-bit, and packed raw Bayer 10-bit/12-bit formats * are stored in a GL_LUMINANCE texture. The texture width is * equal to the stride. */ GLfloat firstRed[] = { firstRed_x_, firstRed_y_ }; GLfloat imgSize[] = { (GLfloat)width_, (GLfloat)height_ }; GLfloat Step[] = { static_cast(bytesPerPixel_) / (inputConfig_.stride - 1), 1.0f / (height_ - 1) }; GLfloat Stride = (GLfloat)width_ / (shaderStridePixels_ / bytesPerPixel_); /* * Scale the output size from the native size the algorithm produces for * the input size. Keep the aspect ratio and prefer cropping over black - * bars. + * bars - but crop from both sides, not from one. + * + * The viewport is the sensor frame while the buffer drawn into is the + * requested size, so the buffer covers only the corner of the viewport + * that starts at NDC -1 and ends where its own width reaches. Placing + * the quad at -(1 - scale) puts the texture's left edge exactly at + * that corner, so everything cropped is taken off one side: metered + * by registering a 640x480 stream against a 1920x1080 one of the same + * scene, the frame shown ran from x=48 rather than from the x=240 + * that centring the 4:3 window in a 16:9 sensor calls for. + * + * Centre it on the region the buffer actually covers instead. That + * region spans NDC -1 to 2*out/viewport - 1, whose midpoint is + * out/viewport - 1, and putting the quad's centre there splits the + * crop evenly between the two edges at every size, the native one + * included - where it shares the eight columns of Bayer border + * instead of taking them all off the right. */ GLfloat scale = std::max((GLfloat)outputSize_.width / nativeOutputSize_.width, (GLfloat)outputSize_.height / nativeOutputSize_.height); - GLfloat trans = -(1.0f - scale); + GLfloat transX = (GLfloat)outputSize_.width / width_ - 1.0f; + GLfloat transY = (GLfloat)outputSize_.height / height_ - 1.0f; GLfloat projMatrix[] = { scale, 0, 0, 0, 0, scale, 0, 0, 0, 0, 1, 0, - trans, trans, 0, 1 + transX, transY, 0, 1 }; /* Static const coordinates */ static const GLfloat vcoordinates[4][2] = { { -1.0f, -1.0f }, { -1.0f, +1.0f }, { +1.0f, +1.0f }, { +1.0f, -1.0f }, }; static const GLfloat tcoordinates[4][2] = { { 0.0f, 0.0f }, { 0.0f, 1.0f }, { 1.0f, 1.0f }, { 1.0f, 0.0f }, }; /* vertexIn - bayer_8.vert */ glEnableVertexAttribArray(attributeVertex_); glVertexAttribPointer(attributeVertex_, 2, GL_FLOAT, GL_TRUE, 2 * sizeof(GLfloat), vcoordinates); /* textureIn - bayer_8.vert */ glEnableVertexAttribArray(attributeTexture_); glVertexAttribPointer(attributeTexture_, 2, GL_FLOAT, GL_TRUE, 2 * sizeof(GLfloat), tcoordinates); /* * Set the sampler2D to the respective texture unit for each texutre * To simultaneously sample multiple textures we need to use multiple * texture units */ From patchwork Thu Oct 15 19:43:33 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Ray Strode X-Patchwork-Id: 28463 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 9CCE7C3357 for ; Thu, 15 Oct 2026 19:49:49 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 5EAB168E11; Thu, 15 Oct 2026 21:49:46 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="ABNYUkHr"; dkim-atps=neutral Received: from mail-qv1-xf31.google.com (mail-qv1-xf31.google.com [IPv6:2607:f8b0:4864:20::f31]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id D2AA668E06 for ; Thu, 15 Oct 2026 21:43:48 +0200 (CEST) Received: by mail-qv1-xf31.google.com with SMTP id 6a1803df08f44-9196d8543cbso16862726d6.1 for ; Thu, 15 Oct 2026 12:43:48 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792093428; x=1792698228; 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=I2xnkr/NsP46i9rQdxwWnQWmmpo9yYSsaSYJOaVHbgQ=; b=ABNYUkHrR/PRQigHT5o8dZnUCT2xaw2VjQCgxlrNeBKW5VH+YZUJtWyWdtKvU4/O3p nYLBA/IfrIjoR/J0XgOrfX4JpKzHjTAtp0U5VaPAGjEiNZMdFZZ43J1NFuccfDLPf888 1uzzX3mFoqUqFGl+jE+J+VkATSfSMqTalL08749qUbT/dY0wCynviMRu7U+qFSWp0NVy P+jSOUs/VwLbTVye/nnHINoeK4APV7wwmHj3SUc337JS/+BrDwXv+dxOkKbfiyzsiIkY 5EPq6cnAVnEpA/IOgCUt6OjA9PI+OPLIBbjwYzuEsQFKomrkB8CITTQ9gJjAH3hV9m2i FVVQ== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792093428; x=1792698228; 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=I2xnkr/NsP46i9rQdxwWnQWmmpo9yYSsaSYJOaVHbgQ=; b=dDdly82dtSAJt38dRVdoRPm2hWXuKnYkhG7EcHVc9I7DF0Bh8vNmRoTQt9Tr0+Ifag mOInAJUaQsMcvRZ8SUmx1i4CA5E7Nl4ZF9Ow9/vic7euVZOVHrnNXfOJFGDLvjI6UPYl YmoqNTDzSaTrX6b56IYtULtK1wLhzf1HAgHn0Hl5ZsCCGM3Vn/vGgUFvfqUfOBpdFZ8F Isee672Gk73gXvqws3yhPMnyvcNlAFYpxWF5ahTyPqLIcoibXb2fRZhM6sEjWw3z/WoP FD8w+32tzcOjDfYw6SvgD4dNDt3XaMotzlJHDe7tJ7sRennlboLLD2hE4RM5HOnHsSuE w4KQ== X-Gm-Message-State: AFq9FYKpbdCDVAiQBC0riHIU+7H4bPHiT7dT+U6+8I3u8N5QXu7CKA7y fn7WTO5kPGVjk/pTOJ1+Rnr/roIKwKJ6qZBoV1mMTxHmPks4ogOD0O8wM5NxXRJIWBo= X-Gm-Gg: AYBFou3ylnWqERWMbyppKVJz3yGRNiWW9pLuwmkeL0GVPLq6OTJEMYJyIX+45EZs+IS 27IeoU2jyaTeZHfainR+s1mMLwW2VmrM/Lg4/cJb1+FtbpCc8IcJO9f+N54ZjVMWQAq+wGhf3Fu itMGADgb40xNpJGonIg6lgXYWymLvYlxJvqocVkep3fAL8+QAlUoUtXFyTzQ5QPi+Gts1TfwEon 8gcirSqjmIS5kBDNYjwAc8uR3AkDxFWzV0V9kk8pQRATqmGOE09VUmXdHrpAW279pogY4gC5Zje ozhXiuTQ+BzN2nXj80qNsqrpvkoNgeEn53VW0fDNxIsQD0mdvbZ4Q5Hq8JwgmkPxvlYUYDhcWOb qTgbWYsWF5RrIbpk/9s4ltDJJM+HZrAYGIAKzYywUO/3SDb80k1GeO1iJ8U88L760jc5GS2XESE eSfoQmnUK7f+6p+vPRg0w7AB3I+jZCr6fwmG1hBExVPTfgxD0GylABpobLANz1YeE8oBuTm30XD URPIyK5xXd/K8MZZh4dsocLCw== X-Received: by 2002:a05:620a:90ca:10b0:93f:1936:b206 with SMTP id af79cd13be357-93f1936bae3mr75969585a.83.1792093427383; Thu, 15 Oct 2026 12:43:47 -0700 (PDT) Received: from localhost ([144.121.52.163]) by smtp.gmail.com with ESMTPSA id af79cd13be357-93f1a18176esm15370385a.20.2026.10.15.12.43.46 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 15 Oct 2026 12:43:46 -0700 (PDT) From: Ray Strode To: libcamera-devel@lists.libcamera.org Subject: [PATCH 2/4] tests: Validate rendered GPU crop across output sizes Date: Thu, 15 Oct 2026 15:43:33 -0400 Message-ID: <20261015194335.287960-3-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 sensor image when output dimensions or aspect ratios change. The project has no test that inspects rendered pixels, so a projection error can shift the visible image without failing an automated check. This commit renders centered synthetic stripes through the GPU path and compares each output against a centered crop of the native image. It covers horizontal and vertical positioning, four Bayer formats, and four output sizes. Signed-off-by: Ray Strode --- test/meson.build | 16 ++- test/software-isp-gpu-crop.cpp | 203 +++++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 test/software-isp-gpu-crop.cpp diff --git a/test/meson.build b/test/meson.build index 363a2829..644a8cba 100644 --- a/test/meson.build +++ b/test/meson.build @@ -51,81 +51,95 @@ public_tests = [ internal_tests = [ {'name': 'bayer-format', 'sources': ['bayer-format.cpp']}, {'name': 'byte-stream-buffer', 'sources': ['byte-stream-buffer.cpp']}, {'name': 'camera-sensor', 'sources': ['camera-sensor.cpp']}, {'name': 'delayed_controls', 'sources': ['delayed_controls.cpp']}, {'name': 'event', 'sources': ['event.cpp']}, {'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}, + ] +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] + if 'include_directories' in test + includes += test['include_directories'] + endif + exe = executable(test['name'], test['sources'], dependencies : deps, implicit_include_directories : false, link_with : test_libraries, - include_directories : test_includes_internal) + include_directories : includes) test(test['name'], exe, should_fail : test.get('should_fail', false)) endforeach foreach test : internal_non_parallel_tests deps = [libcamera_private] 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_internal) test(test['name'], exe, is_parallel : false, should_fail : test.get('should_fail', false)) endforeach diff --git a/test/software-isp-gpu-crop.cpp b/test/software-isp-gpu-crop.cpp new file mode 100644 index 00000000..8d91c0a4 --- /dev/null +++ b/test/software-isp-gpu-crop.cpp @@ -0,0 +1,203 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include "libcamera/internal/dma_buf_allocator.h" +#include "libcamera/internal/egl.h" +#include "libcamera/internal/framebuffer.h" +#include "libcamera/internal/mapped_framebuffer.h" +#include "libcamera/internal/software_isp/debayer_params.h" +#include "libcamera/internal/software_isp/swstats_cpu.h" + +#include "debayer_egl.h" +#include "test.h" + +using namespace libcamera; + +class GpuCropTest : public Test +{ +protected: + int run() override + { + EGLDisplay display = eGL::probeDisplay(); + if (display == EGL_NO_DISPLAY) + return TestSkip; + utils::scope_exit terminate([&] { eglTerminate(display); }); + + const char *extensions = eglQueryString(display, EGL_EXTENSIONS); + if (!extensions || std::string(extensions).find("EGL_EXT_image_dma_buf_import") == std::string::npos) + return TestSkip; + + DmaBufAllocator allocator(DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap | + DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap | + DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf); + if (!allocator.isValid()) + return TestSkip; + + CameraManager manager; + for (PixelFormat format : { formats::SBGGR8, formats::SGRBG8, + formats::SBGGR10_CSI2P, formats::SGRBG10_CSI2P }) { + for (bool vertical : { true, false }) { + std::vector reference; + Size nativeSize; + for (Size size : { Size(1920, 1080), Size(1280, 720), + Size(640, 480), Size(512, 768) }) { + std::vector image; + if (render(manager, allocator, display, format, size, + vertical, &image, &nativeSize)) + return TestFail; + + if (!checkCenter(image, size, vertical)) { + std::cerr << format << " " << size + << " stripe is not centered" << std::endl; + return TestFail; + } + + if (reference.empty()) { + if (size != nativeSize) + return TestFail; + reference = std::move(image); + continue; + } + + /* Compare against a centered crop of the native render. */ + double scale = std::max(double(size.width) / nativeSize.width, + double(size.height) / nativeSize.height); + double error = 0; + for (unsigned int y = 0; y < size.height; y++) { + int ry = std::lround(nativeSize.height / 2.0 + + (y + 0.5 - size.height / 2.0) / scale - 0.5); + for (unsigned int x = 0; x < size.width; x++) { + int rx = std::lround(nativeSize.width / 2.0 + + (x + 0.5 - size.width / 2.0) / scale - 0.5); + unsigned int actual = (y * size.width + x) * 4; + unsigned int expected = (ry * nativeSize.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 the stripe edges. */ + if (error > 2.0) + return TestFail; + } + } + } + return TestPass; + } + +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 synthetic scene is achromatic, independent of RGB ordering. */ + if (image[(y * size.width + x) * 4 + 1] > 128) { + sum += i + 0.5; + count++; + } + } + if (!count) + return false; + double center = sum / count; + std::cout << (vertical ? "X" : "Y") << " center " << center + << ", expected " << length / 2.0 << std::endl; + return std::abs(center - length / 2.0) <= 1.0; + } + + int render(CameraManager &manager, DmaBufAllocator &allocator, EGLDisplay display, + PixelFormat format, const Size &size, bool vertical, + std::vector *image, Size *nativeSize) + { + auto stats = std::make_unique(manager); + if (!stats->isValid()) + return TestFail; + DebayerEGL 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; + unsigned int frameSize; + std::tie(output.stride, frameSize) = debayer.strideAndFrameSize(output.pixelFormat, size); + output.bufferCount = 1; + *nativeSize = debayer.sizes(format, input.size).max; + if (debayer.configure(input, { output }, false)) + return TestFail; + + std::vector> inputs, outputs; + if (allocator.exportBuffers(1, { input.stride * input.size.height }, &inputs) != 1 || + allocator.exportBuffers(1, { frameSize }, &outputs) != 1) + return TestFail; + MappedFrameBuffer raw(inputs[0].get(), MappedFrameBuffer::MapFlag::Write); + MappedFrameBuffer rgb(outputs[0].get(), MappedFrameBuffer::MapFlag::Read); + if (!raw.isValid() || !rgb.isValid()) + return TestFail; + FrameMetadata &metadata = inputs[0]->_d()->metadata(); + metadata.status = FrameMetadata::FrameSuccess; + metadata.sequence = 0; + metadata.timestamp = 0; + + { + DmaSyncer sync(inputs[0]->planes()[0].fd, DmaSyncer::SyncType::Write); + std::fill(raw.planes()[0].begin(), raw.planes()[0].end(), 0); + /* A bright stripe centered on the sensor, with dark surroundings. */ + 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; + uint8_t value = bright ? 224 : 32; + unsigned int offset = packed ? x / 4 * 5 + x % 4 : x; + /* RAW10 stores the eight high bits before each group's low bits. */ + raw.planes()[0][y * input.stride + offset] = value; + } + } + } + + /* stop() must also release a partially initialized context on failure. */ + utils::scope_exit stop([&] { debayer.stop(); }); + if (debayer.start()) + return TestFail; + bool ready = false; + debayer.outputBufferReady.connect(this, [&](FrameBuffer *) { ready = true; }); + debayer.process(0, inputs[0].get(), outputs[0].get(), DebayerParams{}); + if (!ready || outputs[0]->metadata().status != FrameMetadata::FrameSuccess) + return TestFail; + + DmaSyncer sync(outputs[0]->planes()[0].fd, DmaSyncer::SyncType::Read); + image->resize(size.width * size.height * 4); + for (unsigned int y = 0; y < size.height; y++) + std::copy_n(rgb.planes()[0].data() + y * output.stride, size.width * 4, + image->data() + y * size.width * 4); + return TestPass; + } +}; + +TEST_REGISTER(GpuCropTest) From patchwork Thu Oct 15 19:43:34 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Ray Strode X-Patchwork-Id: 28464 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 1CF04C3358 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 EB3B568E25; Thu, 15 Oct 2026 21:49:46 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="h7eMFUP0"; dkim-atps=neutral Received: from mail-qk1-x72b.google.com (mail-qk1-x72b.google.com [IPv6:2607:f8b0:4864:20::72b]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 7001C68E06 for ; Thu, 15 Oct 2026 21:43:49 +0200 (CEST) Received: by mail-qk1-x72b.google.com with SMTP id af79cd13be357-93e515769d3so485449785a.3 for ; Thu, 15 Oct 2026 12:43:49 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1792093428; x=1792698228; 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=WdaCS6Me9+EDOfggmdbddTpHPzRokVqnY8F5ni21u8Q=; b=h7eMFUP0+ChhB+MGZS/jQHWu0LO8P95+DAmZafANdDdLWHShjSh7umrI8b0i4vdvNc WfEupKsAaq/bGSLNMs1X6YAxGvqC074jKjHyPSSit8+di6vVyux7TaG6Gf/5amAeC7dc tcU9CAy+K3GaachYQBEqzrV7jZb4DEsDujhuZ5kh7yu5EJnm7tLkk27KRYaQs57JFSgg Z0mL/GC1BYjCwCpIkKcnw2s1r2BTyq9ScDR7zQ2lNdvh0Q/tmaq4cJllsBGrOxq+rmYZ OraUWCt+8xdD4NWddukMtefoQtT5KeWwqmW8GJO+LsUN4Zv3i1bhl0c6ye4Xxoq4+aqh DfMg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1792093428; x=1792698228; 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=WdaCS6Me9+EDOfggmdbddTpHPzRokVqnY8F5ni21u8Q=; b=bY5LgIfLbL8UMm+WYWspKF4lC6JNW5OsZzhnL4eBR0oLJZnLu3Ra4oVjXbUgA2+3t+ 6r2N5pjzeLXnpvM1hPffkh7ybzQzGD4CWoALXBIAO42scH205RUdCr54ElMiyA0IFa7V 1neyEVEHzFLmv6/WqSrMzcY4PvG4uLC+xLhu30pIl1AkFJQGt4UEpWLX57swV/ryjSmr G1D/SWDmAlRaeJ3Vl5ZI89eRL4b683kEQQ5EBKBzqSxDEs3dp9GP3DOkWuAYOxutmNGr 23p+bf9ZOxvvLUhDapBlnQfKFSxjVOu8XfVx5QExNpO1ee5zhbX+ICH2X1AGsP97fWHG pMNA== X-Gm-Message-State: AFq9FYKaOBmVmX5YlCaHYkKsQHkJaOBPdiBfX1vxhP7ktl3yxi30Rj0q szfBU3BJmwUNYQvzAVQl/996nPgjzuPGYIhi7w0eKC1L9BowkAG5FtOy/nuea/zTFMg= X-Gm-Gg: AYBFou1GpBWf1E3ckSVmcVE9k0dtiRNC4tJAPA5czgDuv9ty54BO8YlBJs7f3lDB7r5 uXCAZRzWEYC6VBgx6i1VzPSBbpFfnPgpbMDsRSCsweFmqe/zXFfqD4XnXKLMi8qa3mBeSSfWQZe MNOd6nT52b5SySmyZzGVFzrmNrrn8dHj/VlFqH8cQ8fAEuyYAELIsoX9F1saC/4NGy+itxdB2YH 8OYWVzwog+Fve9rAq5iXR50lVO5y7kwtYgd8ruZeyvR37sW/C9gKsNaPiSbccgfh6vKJswjPT53 J9CB1MSAIyWGGCqG002xPDa7xj8/zoqmLOn8xEqXP90QCuEyTOJJIJlmCyWNNiJZxKWZArQ8WSw rVR7y6OGCPofHvCRkiODLFg6f4NzhuRFwZhgi5SuRAY7XhbvWoOhlzP7HVZBVXUNFgua1Z6GUoX E5APCW7t9AWkLuF2Nk1Oi2vE+/pf4UEhph8trVMRrPTDJx43hRNTEzewk+FE1y8Blt8PoQFupA+ 0JC++kMhL9SEvyS8ZYvg1yw+A== X-Received: by 2002:a05:620a:45a8:b0:93f:290:c217 with SMTP id af79cd13be357-93f0290c79emr1115306085a.50.1792093428117; 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.47 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Thu, 15 Oct 2026 12:43:47 -0700 (PDT) From: Ray Strode To: libcamera-devel@lists.libcamera.org Subject: [PATCH 3/4] software-isp: Meter the visible sensor crop Date: Thu, 15 Oct 2026 15:43:34 -0400 Message-ID: <20261015194335.287960-4-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 draws a centered sensor crop into the requested output size, while CPU statistics read the original input buffer. The statistics window still follows the output dimensions, so metering covers a different part of the sensor when output sizes or aspect ratios change. This commit derives the visible crop in sensor coordinates and centers the statistics window on that region. Signed-off-by: Ray Strode --- src/libcamera/software_isp/debayer_egl.cpp | 14 ++++---------- src/libcamera/software_isp/debayer_egl.h | 1 - 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp index 0f515c5e..3335eaf9 100644 --- a/src/libcamera/software_isp/debayer_egl.cpp +++ b/src/libcamera/software_isp/debayer_egl.cpp @@ -295,72 +295,66 @@ int DebayerEGL::configure(const StreamConfiguration &inputCfg, inputConfig_.stride = inputCfg.stride; inputPixelFormat_ = inputCfg.pixelFormat; width_ = inputCfg.size.width; height_ = inputCfg.size.height; if (outputCfgs.size() != 1) { LOG(Debayer, Error) << "Unsupported number of output streams: " << outputCfgs.size(); return -EINVAL; } const StreamConfiguration &outputCfg = outputCfgs[0]; SizeRange outSizeRange = sizes(inputCfg.pixelFormat, inputCfg.size); std::tie(outputConfig_.stride, outputConfig_.frameSize) = strideAndFrameSize(outputCfg.pixelFormat, outputCfg.size); if (!outSizeRange.contains(outputCfg.size) || outputConfig_.stride != outputCfg.stride) { LOG(Debayer, Error) << "Invalid output size/stride: " << "\n " << outputCfg.size << " (" << outSizeRange << ")" << "\n " << outputCfg.stride << " (" << outputConfig_.stride << ")"; return -EINVAL; } outputPixelFormat_ = outputCfg.pixelFormat; outputSize_ = outputCfg.size; nativeOutputSize_ = outSizeRange.max; - window_.x = ((inputCfg.size.width - outputCfg.size.width) / 2) & - ~(inputConfig_.patternSize.width - 1); - window_.y = ((inputCfg.size.height - outputCfg.size.height) / 2) & - ~(inputConfig_.patternSize.height - 1); - window_.width = outputCfg.size.width; - window_.height = outputCfg.size.height; - /* - * Don't pass x,y from window_ since process() already adjusts for it. - * But crop the window to 2/3 of its width and height for speedup. + * The GPU scales the visible sensor crop to outputSize_. Statistics + * consume the original input buffer, so use sensor coordinates here. */ - stats_->setWindow(Rectangle(window_.size())); + const Size visibleSize = nativeOutputSize_.boundedToAspectRatio(outputSize_); + stats_->setWindow(visibleSize.centeredTo(Rectangle(inputCfg.size).center())); inputBufferCount_ = inputCfg.bufferCount; outputBufferCount_ = outputCfg.bufferCount; return 0; } Size DebayerEGL::patternSize(PixelFormat inputFormat) { DebayerEGL::DebayerInputConfig config; if (getInputConfig(inputFormat, config) != 0) return {}; return config.patternSize; } std::vector DebayerEGL::formats(PixelFormat inputFormat) { DebayerEGL::DebayerInputConfig config; if (getInputConfig(inputFormat, config) != 0) return std::vector(); return config.outputFormats; } std::tuple DebayerEGL::strideAndFrameSize(const PixelFormat &outputFormat, const Size &size) { diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h index 30e51a47..52b8aa07 100644 --- a/src/libcamera/software_isp/debayer_egl.h +++ b/src/libcamera/software_isp/debayer_egl.h @@ -84,41 +84,40 @@ private: /* Shader parameters */ float firstRed_x_; float firstRed_y_; GLint attributeVertex_; GLint attributeTexture_; GLint textureUniformStep_; GLint textureUniformSize_; GLint textureUniformStrideFactor_; GLint textureUniformBayerFirstRed_; GLint textureUniformProjMatrix_; GLint textureUniformBayerDataIn_; /* Per-frame AWB gains */ GLint awbUniformDataIn_; /* Represent per-frame CCM as a uniform vector of floats 3 x 3 */ GLint ccmUniformDataIn_; /* Black Level compensation */ GLint blackLevelUniformDataIn_; /* Gamma */ GLint gammaUniformDataIn_; /* Contrast */ GLint contrastExpUniformDataIn_; Size nativeOutputSize_; - Rectangle window_; std::unique_ptr stats_; eGL egl_; uint32_t width_; uint32_t height_; GLint glFormat_; unsigned int bytesPerPixel_; uint32_t shaderStridePixels_; }; } /* namespace libcamera */ 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)