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.
