From patchwork Sun Aug 30 11:51:20 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Christian Murphy X-Patchwork-Id: 28149 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 C012DC3260 for ; Sun, 30 Aug 2026 12:10:20 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 35367684B9; Sun, 30 Aug 2026 14:10:19 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=themurphys.eu header.i=christian@themurphys.eu header.b="jPEliv3p"; dkim-atps=neutral Received: from mout.kundenserver.de (mout.kundenserver.de [212.227.17.10]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id D9C5565F9F for ; Sun, 30 Aug 2026 13:51:32 +0200 (CEST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=themurphys.eu; s=s1-ionos; t=1788090691; x=1788695491; i=christian@themurphys.eu; bh=8enTWDY8bVkNwJB+z8gAgkMpRoTcVOk/0ZLkMoOAygo=; h=X-UI-Sender-Class:From:To:Cc:Subject:Date:Message-ID:In-Reply-To: References:MIME-Version:Content-Transfer-Encoding:cc: content-transfer-encoding:content-type:date:from:message-id: mime-version:reply-to:subject:to; b=jPEliv3pkuxm07t56L9sv/zE3/q94PY8dYsjhTCuhfafQZKXbpTpHSviPjN+VTyh TkyylhocijIaAUGE20jn4DDPwno6cj3nLMDRxcPGY54tNcg7Rj9IL+tVlI3ACNNOH sYMsumpGU2clK1i0/NlXG9j97jVpt4oQtq1Qb18i1bxsvUz8c6l7bQ3OGqrFrWUgD c91c8PCwH1WDPcY5VLEsUN5ulbIlqQDA+Yzza4MAiZfsCuVfyGYai2SH7Xcgy1Pgv zQ/TWzOIitTc3QWh+hITkO/jd2sU9ZlXhnIhKGOj8ehvUDNIyLR71F+k6rGKQZUN1 dVN8pwjiPKB/x0jPig== X-UI-Sender-Class: 55c96926-9e95-11ee-ae09-1f7a4046a0f6 Received: from client.hidden.invalid by mrelayeu.kundenserver.de (mreue106 [213.165.67.119]) with ESMTPSA (Nemesis) id 1N0WLC-1wdjyJ1vSm-00w7UE; Sun, 30 Aug 2026 13:51:31 +0200 From: Christian Murphy To: libcamera-devel@lists.libcamera.org Cc: Bryan O'Donoghue , Milan Zamazal , Hans de Goede , Kieran Bingham , Laurent Pinchart , Robert Mader , =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= , Christian Murphy Subject: [PATCH 2/2] libcamera: shaders: Snap unpacked Bayer sampling to texel centres Date: Sun, 30 Aug 2026 12:51:20 +0100 Message-ID: <20260830115120.255815-2-christian@themurphys.eu> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260830115120.255815-1-christian@themurphys.eu> References: <20260830115120.255815-1-christian@themurphys.eu> MIME-Version: 1.0 X-Provags-ID: V03:K1:skisLAD9S+DtEtiTqoI3319KBql5GW/EY8bHc0mnRrTl9OTbaTP Q5WJpviXyIKIFMOUGSTB8bvtPZcyvBH1yBlvuKv3LHHO/F4o84U0nN7tK9n/6SD/2tCx2P+ H8RcPtUOE+Ppn7h77uYDdvh42Uv48u59rw+ebyCCkV32Y9EG2Wmdfu3yHydeKehWESgSTfs 7BnMjj5/DIhMRtzviDB1g== X-Spam-Flag: NO UI-OutboundReport: notjunk:1; M01:P0:YrEg2edZyG0=; GhxP0uOvPmvJYpvIa+V1JqMSu+j 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 7JShEX0aw== X-Mailman-Approved-At: Sun, 30 Aug 2026 14:10:16 +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" The bayer_unpacked fragment shader derives the Bayer parity of the sampled pixel from one interpolated varying, in pixel units, while the texture unit selects the nearest texel from another, in texture units. At native size every sample lands on a texel centre and the two agree. When the render target is smaller than the source, as in the software ISP GPU debayer and qcam's scaled viewfinder, sample positions land on texel boundaries on a periodic subset of output rows and columns, and rounding differences make the parity and the fetched texel disagree by one. Such rows debayer with the wrong row parity, raising R and B over G: regular magenta lines. On an imx471 (1928x1088 SRGGB10 through the Intel IPU7 ISYS, debayered by Mesa 26.1 on Intel Arc B390 graphics) scaled to 640x480, output row y samples source row (2y + 1) * 17 / 15, an exact texel boundary for y = 7 (mod 15). Captures showed one magenta line every 15 output rows, at those offsets. Select the source pixel with a single floor() in pixel space, as bayer_1x_packed.frag already does, and derive the parity, the centre fetch and the neighbour fetches from that one index, snapped to texel centres. The two computations can then no longer disagree. The vertex shader now only forwards the source position in pixels; the center, xCoord and yCoord varyings and tex_step are dropped from the unpacked programs, and stride_factor and tex_bayer_first_red move to the fragment shader. DebayerEGL and qcam already set those uniforms, so no C++ changes are needed beyond a comment. At native size the sampled texels are unchanged. Measured with the mean of R + B - 2G per output row, in 8-bit output levels: at 640x480 the mean excess of the y = 7 (mod 15) rows drops from +13.1 (about 5% of full scale), with all other phases within +/-0.05, to 0.00, within the spread of the other phases. 480x320, 640x360, 1280x720 and the native output size of 1924x1088 stay clean and the field of view is unchanged. Assisted by Claude Code (claude-fable-5). Signed-off-by: Christian Murphy --- src/libcamera/shaders/bayer_unpacked.frag | 21 ++++++++++++---- src/libcamera/shaders/bayer_unpacked.vert | 28 +++------------------- src/libcamera/software_isp/debayer_egl.cpp | 10 ++++---- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/libcamera/shaders/bayer_unpacked.frag b/src/libcamera/shaders/bayer_unpacked.frag index 10c5e941b..455d1fe78 100644 --- a/src/libcamera/shaders/bayer_unpacked.frag +++ b/src/libcamera/shaders/bayer_unpacked.frag @@ -21,9 +21,10 @@ precision highp float; /** Monochrome RGBA or GL_LUMINANCE Bayer encoded texture.*/ uniform sampler2D tex_y; -varying vec4 center; -varying vec4 yCoord; -varying vec4 xCoord; +uniform vec2 tex_size; +uniform float stride_factor; +uniform vec2 tex_bayer_first_red; +varying vec2 pixelPos; uniform vec3 awb; uniform mat3 ccm; uniform vec3 blacklevel; @@ -52,11 +53,23 @@ void main(void) { #define fetch(x, y) texture2D(tex_y, vec2(x, y)).r #endif + /* + * Derive the parity and every fetch coordinate from one floor() of the + * pixel position, snapped to texel centres. Computed separately they + * can round to different texels when downscaling lands a sample on a + * texel boundary. + */ + vec2 texStep = vec2(stride_factor / tex_size.x, 1.0 / tex_size.y); + vec2 pix = floor(pixelPos); + vec2 center = (pix + 0.5) * texStep; + vec4 xCoord = center.x + vec4(-2.0, -1.0, 1.0, 2.0) * texStep.x; + vec4 yCoord = center.y + vec4(-2.0, -1.0, 1.0, 2.0) * texStep.y; + float C = fetch(center.x, center.y); // ( 0, 0) const vec4 kC = vec4( 4.0, 6.0, 5.0, 5.0) / 8.0; // Determine which of four types of pixels we are on. - vec2 alternate = mod(floor(center.zw), 2.0); + vec2 alternate = mod(pix + tex_bayer_first_red, 2.0); vec4 Dvec = vec4( fetch(xCoord[1], yCoord[1]), // (-1,-1) diff --git a/src/libcamera/shaders/bayer_unpacked.vert b/src/libcamera/shaders/bayer_unpacked.vert index 423dde0fa..be1268b62 100644 --- a/src/libcamera/shaders/bayer_unpacked.vert +++ b/src/libcamera/shaders/bayer_unpacked.vert @@ -22,34 +22,12 @@ attribute vec2 textureIn; uniform mat4 proj_matrix; uniform vec2 tex_size; /* The texture size in pixels */ -uniform vec2 tex_step; -/** Pixel position of the first red pixel in the */ -/** Bayer pattern. [{0,1}, {0, 1}]*/ -uniform vec2 tex_bayer_first_red; - -/** .xy = Pixel being sampled in the fragment shader on the range [0, 1] - .zw = ...on the range [0, sourceSize], offset by firstRed */ -varying vec4 center; - -/** center.x + (-2/w, -1/w, 1/w, 2/w); These are the x-positions */ -/** of the adjacent pixels.*/ -varying vec4 xCoord; - -/** center.y + (-2/h, -1/h, 1/h, 2/h); These are the y-positions */ -/** of the adjacent pixels.*/ -varying vec4 yCoord; - -uniform float stride_factor; +/** Position of the pixel being sampled, in image pixels. */ +varying vec2 pixelPos; void main(void) { - center.xy = vec2(textureIn.x * stride_factor, textureIn.y); - center.zw = textureIn * tex_size + tex_bayer_first_red; - - xCoord = center.x + vec4(-2.0 * tex_step.x, - -tex_step.x, tex_step.x, 2.0 * tex_step.x); - yCoord = center.y + vec4(-2.0 * tex_step.y, - -tex_step.y, tex_step.y, 2.0 * tex_step.y); + pixelPos = textureIn * tex_size; gl_Position = proj_matrix * vertexIn; } diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp index 97aa03793..6db07c284 100644 --- a/src/libcamera/software_isp/debayer_egl.cpp +++ b/src/libcamera/software_isp/debayer_egl.cpp @@ -432,11 +432,11 @@ void DebayerEGL::setShaderVariableValues(eGLImage &eglImageIn, const DebayerPara /* * These values are: - * firstRed = tex_bayer_first_red - bayer_8.vert - * imgSize = tex_size - bayer_8.vert - * step = tex_step - bayer_8.vert - * Stride = stride_factor identity.vert - * textureUniformProjMatri = No scaling + * firstRed = tex_bayer_first_red - bayer_unpacked.frag, bayer_1x_packed.frag + * imgSize = tex_size - bayer_unpacked.vert, bayer_unpacked.frag, bayer_1x_packed.frag + * step = tex_step - bayer_1x_packed.frag + * Stride = stride_factor - identity.vert, bayer_unpacked.frag + * projMatrix = proj_matrix - identity.vert, bayer_unpacked.vert */ glUniform2fv(textureUniformBayerFirstRed_, 1, firstRed); glUniform2fv(textureUniformSize_, 1, imgSize);