diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index c042d3864..0ccae473f 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -388,16 +388,33 @@ void DebayerEGL::setShaderVariableValues(eGLImage &eglImageIn, const DebayerPara
 	/*
 	 * 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] = {
