From patchwork Sun Nov 15 17:54:36 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Firas Sharary X-Patchwork-Id: 28909 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 E7231BDE17 for ; Sun, 15 Nov 2026 17:58:10 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 7E6CA6A832; Sun, 15 Nov 2026 18:58:08 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=gmail.com header.i=@gmail.com header.b="XgksVIQ1"; dkim-atps=neutral Received: from mail-wm1-x335.google.com (mail-wm1-x335.google.com [IPv6:2a00:1450:4864:20::335]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 36EDF6A819 for ; Sun, 15 Nov 2026 18:54:41 +0100 (CET) Received: by mail-wm1-x335.google.com with SMTP id 5b1f17b1804b1-4a210470b49so21291905e9.2 for ; Sun, 15 Nov 2026 09:54:41 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20260707; t=1794765281; x=1795370081; darn=lists.libcamera.org; h=content-transfer-encoding:mime-version:message-id:date:subject:to :from:from:to:cc:subject:date:message-id:reply-to:content-type; bh=7iq+Tv1wVOnzcqi0XGNugP1lBASw2GpJI63Z+1WpyDQ=; b=XgksVIQ1Nl5VVskHEUw7bQFF7zy3YH62GG6gEDtSdVKP13IRLnObp0lRpOHhBGkG/T gVNO8cgDtnb9haanj4FKzRQIHVk0+a9eIDHXM8nUtE0bXYJK+AhHQm/2kPdNlMPWIS9l FY1DSy854KQfgyvHGVqQeRQR+VBQnN8tNr3rc5/j9RhcxLeK2uiqf9r8nt114HDwRNBV hG0B7QJOx+vG7CPI3R0fIblCSfnFuFdxzezkoKMsqjL0dhMt4J5tKpaTzuntjAWrpL+S G9S2xNjWkjbOGYcsWAXRJTddxPT6kQCoYDvGNWxJLQmsNurrBW6f8NZPfPNO8HyMf0rw PqUg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1794765281; x=1795370081; h=content-transfer-encoding:mime-version: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=7iq+Tv1wVOnzcqi0XGNugP1lBASw2GpJI63Z+1WpyDQ=; b=1q1YzrbzPKR+Q0sDizkRGJks1AXSOoPmsAAn6FeqpGJy+sJLWCnCn7kHgbIaCfz0u/ bEIw7N5z2YSkE2Cne302Eteemkw6NV/BIfx1zUbl0fIdjD3/l9QPpBU2XMv18YtGOM5e XeYVCY6vPJ4MuYeFZBel0UycYlpwhAYRN0VTB6pi8tfZiaafbWBQLilPSuYOP9R0k7E5 9bAki2PoELPGw4h0JQHdDObpqtD9WgU+xhNQivjaH3i14tLKRgn9Xx+d9LfkPRWyHlom qKrWRQwi8YZDZ4fB/ziqtFqQEosamOZzuFOH5S/7EUN8XLhqKC+VmooyvMU7FWERKl/m KkRQ== X-Gm-Message-State: APuU4Z9g1t7eGHrjv/zU/KrA2Xr1BAfPn+e7dN5XA9kqXkfRGMv87nEk kSPHIW7sxd2UhtVPsAOShVIx0Rk6P/rTyN05xVNdwmPYdtmXSdxt6kXN209JJbf/PP5Mgw== X-Gm-Gg: AYBFou2afOQVFcij+8r5VQJfqdx3GSCbslsfjg/RNMIfh6XvDVhQ71VMqIE4rv9sTxn s9iqvQESViFvgePleC0x3Vw7MWbQYu27rkznCjBKvqp1bEbmJxrM9I1QmA/HuZ4iTQkXK3ZjLfx UDBLawGUv+k2Md6XvmMXYJ8YYzOQLugS5nfNp3tP96Vf3/fPqWoDubt34ik74wp0VcYN6BMBFbI LmL/V/8TdoOR2k8dRjVAXrgscL35nsqrIM1dWtPXjND1HctUjlrpjo9vaNZhSqBObzjZS9zV8Wt oHhOt4CNFDk3J0mDHb5qdBM/ED2TrxvIaWeHJWvsjQ8ROiaSIN1waXqyf+jJM09lkXQ+/HZxVd2 zuI0b7dxzi9TOjSORCDuMWAjTQR/KyD/RL9IbFZXxnqtxy2p9ZduiLBObFxYIOedN66XAWDNgzV KUz2O5w1B6PEtIPMYHdBfHj23kvjn5GU9hiXpyQ9XhFakq5Off4OpxEL2N76q0XH90L7kpB0OOj Pdr2cx11kLecTlhyCNLZ0wY8A1ZydVYIeHsl1RUbL+kf7QGioPp/VVLEq970zmbeUnqbIVR8p30 xO9EAoAMQKhTvVdi7GEmX8eB1DA4P2UNqpunEIgeyg65bmw= X-Received: by 2002:a05:600c:3e06:b0:4bb:6947:3e1d with SMTP id 5b1f17b1804b1-4bb6947426bmr61651565e9.20.1794765280512; Sun, 15 Nov 2026 09:54:40 -0800 (PST) Received: from firas-surface.home ([2a06:c701:9f9f:6800:4b81:a189:67c3:a27d]) by smtp.gmail.com with ESMTPSA id 5b1f17b1804b1-4bb68c00b10sm56827605e9.0.2026.11.15.09.54.39 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Sun, 15 Nov 2026 09:54:40 -0800 (PST) From: Firas Sharary To: libcamera-devel@lists.libcamera.org Subject: [PATCH] libcamera: shaders: bayer_unpacked: Sample at texel centres Date: Sun, 15 Nov 2026 19:54:36 +0200 Message-ID: <20261115175436.8673-1-firassharary3@gmail.com> X-Mailer: git-send-email 2.47.3 MIME-Version: 1.0 X-Mailman-Approved-At: Sun, 15 Nov 2026 18:58:06 +0100 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" When the software ISP scales with the GPU debayer, the fragment shader samples the Bayer texture at the interpolated position and derives the Bayer phase of that position from floor(center.zw). The two can disagree: the sampler resolves nearest filtering in fixed point, so a position just below a texel boundary may return the next texel, while floor() still selects the previous one. The pixel is then demosaiced with the phase of one texel and the data of its neighbour. With a colour correction matrix loaded this shows up as isolated magenta columns and rows, one pixel wide, whose position depends on the scaling ratio. On an OV5693 read at 2592x1944 through an Intel IPU6 (Tiger Lake, Iris Xe, Mesa 25.0.7) a 1920x1080 stream has them at x=731, x=1211 and y=731, and a 2560x1440 stream at y=1051. In those lines the green channel is zero. Compute the texel from floor(center.zw) once and sample at its centre, so the phase and the data always come from the same texel. While at it, handle the neighbours that fall outside the image. They were clamped to the edge texel, which has a different Bayer colour than the pixel asked for and tinted the first row and column. Use the neighbour at the same distance on the other side instead. Signed-off-by: Firas Sharary --- Tested with v0.8.0 on a Surface Pro 7+; bayer_unpacked.frag is unchanged between v0.8.0 and master. Captures at 1280x720, 1920x1080 and 2560x1440 from the 2592x1944 mode no longer show the lines or the tinted border. bayer_1x_packed.frag derives its coordinates differently and I could not test it: this machine's IPU6 does not deliver packed frames at all (see "media: ipu6: packed raw capture never completes a frame on Tiger Lake" on linux-media). src/libcamera/shaders/bayer_unpacked.frag | 60 +++++++++++++++++------ 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/src/libcamera/shaders/bayer_unpacked.frag b/src/libcamera/shaders/bayer_unpacked.frag index be2f89d..f06022b 100644 --- a/src/libcamera/shaders/bayer_unpacked.frag +++ b/src/libcamera/shaders/bayer_unpacked.frag @@ -24,6 +24,10 @@ uniform sampler2D tex_y; varying vec4 center; varying vec4 yCoord; varying vec4 xCoord; +uniform vec2 tex_size; +uniform vec2 tex_step; +uniform vec2 tex_bayer_first_red; +uniform float stride_factor; uniform vec3 awb; uniform mat3 ccm; uniform vec3 blacklevel; @@ -56,17 +60,45 @@ void main(void) { #define fetch(x, y) texture2D(tex_y, vec2(x, y)).r #endif - float C = fetch(center.x, center.y); // ( 0, 0) + /* + * Sample at the centre of the source pixel that floor(center.zw) selects, + * not at the interpolated position. When the output is scaled the + * interpolated position can lie so close to a texel boundary that the + * sampler, which resolves nearest filtering in fixed point, returns the + * neighbouring texel. The pixel is then demosaiced with the Bayer phase + * of one texel and the data of another, which shows up as isolated + * magenta rows and columns in the output. + */ + vec2 pos = floor(center.zw); + vec2 snapped = (pos - tex_bayer_first_red + 0.5) / tex_size; + vec2 c = vec2(snapped.x * stride_factor, snapped.y); + vec4 dx = vec4(-2.0, -1.0, 1.0, 2.0) * tex_step.x; + vec4 dy = vec4(-2.0, -1.0, 1.0, 2.0) * tex_step.y; + vec4 xC = c.x + dx; + vec4 yC = c.y + dy; + + /* + * Neighbours beyond the edge of the image would be clamped to the edge + * texel, which has a different Bayer colour than the pixel asked for and + * tints the outermost row and column. Use the neighbour at the same + * distance on the other side instead, which has the right colour. + */ + xC = mix(xC, c.x - dx, vec4(lessThan(xC, vec4(0.0))) + + vec4(greaterThan(xC, vec4(stride_factor)))); + yC = mix(yC, c.y - dy, vec4(lessThan(yC, vec4(0.0))) + + vec4(greaterThan(yC, vec4(1.0)))); + + float C = fetch(c.x, c.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(pos, 2.0); vec4 Dvec = vec4( - fetch(xCoord[1], yCoord[1]), // (-1,-1) - fetch(xCoord[1], yCoord[2]), // (-1, 1) - fetch(xCoord[2], yCoord[1]), // ( 1,-1) - fetch(xCoord[2], yCoord[2])); // ( 1, 1) + fetch(xC[1], yC[1]), // (-1,-1) + fetch(xC[1], yC[2]), // (-1, 1) + fetch(xC[2], yC[1]), // ( 1,-1) + fetch(xC[2], yC[2])); // ( 1, 1) vec4 PATTERN = (kC.xyz * C).xyzz; @@ -77,16 +109,16 @@ void main(void) { Dvec.x += Dvec.y; vec4 value = vec4( - fetch(center.x, yCoord[0]), // ( 0,-2) - fetch(center.x, yCoord[1]), // ( 0,-1) - fetch(xCoord[0], center.y), // (-2, 0) - fetch(xCoord[1], center.y)); // (-1, 0) + fetch(c.x, yC[0]), // ( 0,-2) + fetch(c.x, yC[1]), // ( 0,-1) + fetch(xC[0], c.y), // (-2, 0) + fetch(xC[1], c.y)); // (-1, 0) vec4 temp = vec4( - fetch(center.x, yCoord[3]), // ( 0, 2) - fetch(center.x, yCoord[2]), // ( 0, 1) - fetch(xCoord[3], center.y), // ( 2, 0) - fetch(xCoord[2], center.y)); // ( 1, 0) + fetch(c.x, yC[3]), // ( 0, 2) + fetch(c.x, yC[2]), // ( 0, 1) + fetch(xC[3], c.y), // ( 2, 0) + fetch(xC[2], c.y)); // ( 1, 0) // Even the simplest compilers should be able to constant-fold these to // avoid the division.