diff --git a/include/libcamera/internal/software_isp/debayer_geometry.h b/include/libcamera/internal/software_isp/debayer_geometry.h
new file mode 100644
index 00000000..8812a3cf
--- /dev/null
+++ b/include/libcamera/internal/software_isp/debayer_geometry.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Red Hat Inc.
+ *
+ * Geometry for scaling and cropping debayered images
+ */
+
+#pragma once
+
+
+#include <libcamera/geometry.h>
+
+namespace libcamera {
+
+/* Return the visible sensor rectangle, before statistics Bayer alignment. */
+inline Rectangle debayerCrop(const Size &inputSize, const Size &nativeSize,
+			     const Size &outputSize)
+{
+	const Size visibleSize = nativeSize.boundedToAspectRatio(outputSize);
+	return visibleSize.centeredTo(Rectangle(inputSize).center());
+}
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/software_isp/meson.build b/include/libcamera/internal/software_isp/meson.build
index df7c3b97..e18597ed 100644
--- a/include/libcamera/internal/software_isp/meson.build
+++ b/include/libcamera/internal/software_isp/meson.build
@@ -1,9 +1,10 @@
 # SPDX-License-Identifier: CC0-1.0
 
 libcamera_internal_headers += files([
     'benchmark.h',
+    'debayer_geometry.h',
     'debayer_params.h',
     'software_isp.h',
     'swisp_stats.h',
     'swstats_cpu.h',
 ])
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 3356070c..5a27a38d 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -1,56 +1,57 @@
 /* SPDX-License-Identifier: LGPL-2.1-or-later */
 /*
  * Copyright (C) 2024, Linaro Ltd.
  *
  * Authors:
  * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
  *
  */
 
 #include "debayer_egl.h"
 
 #include <algorithm>
 #include <assert.h>
 #include <memory>
 #include <span>
 #include <stdlib.h>
 #include <string>
 #include <tuple>
 #include <vector>
 
 #include <libcamera/base/utils.h>
 
 #include <libcamera/formats.h>
 
 #include "libcamera/internal/formats.h"
 #include "libcamera/internal/framebuffer.h"
+#include "libcamera/internal/software_isp/debayer_geometry.h"
 
 #include "../shaders/glsl_shaders.h"
 
 namespace libcamera {
 
 /**
  * \class DebayerEGL
  * \brief Class for debayering using an EGL Shader
  *
  * Implements an EGL shader based debayering solution.
  */
 
 /**
  * \brief Construct a DebayerEGL object
  * \param[in] stats Statistics processing object
  * \param[in] cm The camera manager
  * \param[in] display The EGL display to use
  */
 DebayerEGL::DebayerEGL(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm, EGLDisplay display)
 	: Debayer(cm), stats_(std::move(stats)), egl_(display)
 {
 }
 
 DebayerEGL::~DebayerEGL()
 {
 }
 
 int DebayerEGL::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &config)
 {
 	BayerFormat bayerFormat =
@@ -304,72 +305,65 @@ int DebayerEGL::configure(const StreamConfiguration &inputCfg,
 
 	inputConfig_.stride = inputCfg.stride;
 	inputPixelFormat_ = inputCfg.pixelFormat;
 	width_ = inputCfg.size.width;
 	height_ = inputCfg.size.height;
 
 	if (outputCfgs.size() != 1) {
 		LOG(Debayer, Error)
 			<< "Unsupported number of output streams: "
 			<< outputCfgs.size();
 		return -EINVAL;
 	}
 
 	const StreamConfiguration &outputCfg = outputCfgs[0];
 	SizeRange outSizeRange = sizes(inputCfg.pixelFormat, inputCfg.size);
 	std::tie(outputConfig_.stride, outputConfig_.frameSize) =
 		strideAndFrameSize(outputCfg.pixelFormat, outputCfg.size);
 
 	if (!outSizeRange.contains(outputCfg.size) || outputConfig_.stride != outputCfg.stride) {
 		LOG(Debayer, Error)
 			<< "Invalid output size/stride: "
 			<< "\n  " << outputCfg.size << " (" << outSizeRange << ")"
 			<< "\n  " << outputCfg.stride << " (" << outputConfig_.stride << ")";
 		return -EINVAL;
 	}
 
 	outputPixelFormat_ = outputCfg.pixelFormat;
 	outputSize_ = outputCfg.size;
 	nativeOutputSize_ = outSizeRange.max;
 
-	window_.x = ((inputCfg.size.width - outputCfg.size.width) / 2) &
-		    ~(inputConfig_.patternSize.width - 1);
-	window_.y = ((inputCfg.size.height - outputCfg.size.height) / 2) &
-		    ~(inputConfig_.patternSize.height - 1);
-	window_.width = outputCfg.size.width;
-	window_.height = outputCfg.size.height;
-
 	/*
-	 * Don't pass x,y from window_ since process() already adjusts for it.
-	 * But crop the window to 2/3 of its width and height for speedup.
+	 * The GPU scales the visible sensor crop to outputSize_. Statistics
+	 * consume the original input buffer, so use sensor coordinates here.
 	 */
-	stats_->setWindow(Rectangle(window_.size()));
+	stats_->setWindow(debayerCrop(inputCfg.size, nativeOutputSize_, outputSize_));
 
 	inputBufferCount_ = inputCfg.bufferCount;
 	outputBufferCount_ = outputCfg.bufferCount;
 
 	return 0;
 }
 
 Size DebayerEGL::patternSize(PixelFormat inputFormat)
 {
 	DebayerEGL::DebayerInputConfig config;
 
 	if (getInputConfig(inputFormat, config) != 0)
 		return {};
 
 	return config.patternSize;
 }
 
 std::vector<PixelFormat> DebayerEGL::formats(PixelFormat inputFormat)
 {
 	DebayerEGL::DebayerInputConfig config;
 
 	if (getInputConfig(inputFormat, config) != 0)
 		return std::vector<PixelFormat>();
 
 	return config.outputFormats;
 }
 
 std::tuple<unsigned int, unsigned int>
 DebayerEGL::strideAndFrameSize(const PixelFormat &outputFormat, const Size &size)
 {
diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
index cbbc6a45..6910fa49 100644
--- a/src/libcamera/software_isp/debayer_egl.h
+++ b/src/libcamera/software_isp/debayer_egl.h
@@ -93,41 +93,40 @@ private:
 	float firstRed_y_;
 	GLint attributeVertex_;
 	GLint attributeTexture_;
 	GLint textureUniformStep_;
 	GLint textureUniformSize_;
 	GLint textureUniformStrideFactor_;
 	GLint textureUniformBayerFirstRed_;
 	GLint textureUniformProjMatrix_;
 
 	GLint textureUniformBayerDataIn_;
 
 	/* Per-frame AWB gains */
 	GLint awbUniformDataIn_;
 
 	/* Lens shading correction */
 	GLint textureUniformLsc_;
 
 	/* Represent per-frame CCM as a uniform vector of floats 3 x 3 */
 	GLint ccmUniformDataIn_;
 
 	/* Black Level compensation */
 	GLint blackLevelUniformDataIn_;
 
 	/* Gamma */
 	GLint gammaUniformDataIn_;
 
 	/* Contrast */
 	GLint contrastExpUniformDataIn_;
 
 	Size nativeOutputSize_;
-	Rectangle window_;
 	std::unique_ptr<SwStatsCpu> stats_;
 	eGL egl_;
 	uint32_t width_;
 	uint32_t height_;
 	GLint glFormat_;
 	unsigned int bytesPerPixel_;
 	uint32_t shaderStridePixels_;
 };
 
 } /* namespace libcamera */
