[2/2] libcamera: shaders: Snap unpacked Bayer sampling to texel centres
diff mbox series

Message ID 20260830115120.255815-2-christian@themurphys.eu
State New
Headers show
Series
  • [1/2] qcam: viewfinder_gl: Take the stride into account for unpacked Bayer
Related show

Commit Message

Christian Murphy Aug. 30, 2026, 11:51 a.m. UTC
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 <christian@themurphys.eu>
---
 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(-)

Patch
diff mbox series

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);