diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
index 3ea694d7c..4da335a53 100644
--- a/src/libcamera/egl.cpp
+++ b/src/libcamera/egl.cpp
@@ -153,7 +153,6 @@ int eGL::attachTextureToFBO(eGLImage &eglImage)
 int eGL::createDMABufTexture2D(eGLImage &eglImage, int fd, bool output)
 {
 	EGLint drm_format;
-	int ret;
 
 	ASSERT(tid_ == Thread::currentId());
 
@@ -213,10 +212,7 @@ int eGL::createDMABufTexture2D(eGLImage &eglImage, int fd, bool output)
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
 
-	if (output)
-		ret = attachTextureToFBO(eglImage);
-
-	return ret;
+	return 0;
 }
 
 /**
diff --git a/src/libcamera/software_isp/software_isp_pipeline_gpu.cpp b/src/libcamera/software_isp/software_isp_pipeline_gpu.cpp
index 6f988d672..6f329cf40 100644
--- a/src/libcamera/software_isp/software_isp_pipeline_gpu.cpp
+++ b/src/libcamera/software_isp/software_isp_pipeline_gpu.cpp
@@ -41,7 +41,7 @@ namespace libcamera {
  * \param[in] cm The camera manager
  */
 SoftwareIspPipelineGpu::SoftwareIspPipelineGpu(std::unique_ptr<SwStatsCpu> stats, const CameraManager &cm)
-	: SoftwareIspPipeline(cm), stats_(std::move(stats))
+	: SoftwareIspPipeline(cm), stats_(std::move(stats)), gpuIspShaderPassDemosiac_(egl_)
 {
 }
 
@@ -109,186 +109,13 @@ int SoftwareIspPipelineGpu::getOutputConfig(PixelFormat outputFormat, DebayerOut
 	return -EINVAL;
 }
 
-int SoftwareIspPipelineGpu::getShaderVariableLocations(void)
-{
-	attributeVertex_ = glGetAttribLocation(programId_, "vertexIn");
-	attributeTexture_ = glGetAttribLocation(programId_, "textureIn");
-
-	textureUniformBayerDataIn_ = glGetUniformLocation(programId_, "tex_y");
-	ccmUniformDataIn_ = glGetUniformLocation(programId_, "ccm");
-	blackLevelUniformDataIn_ = glGetUniformLocation(programId_, "blacklevel");
-	gammaUniformDataIn_ = glGetUniformLocation(programId_, "gamma");
-	contrastExpUniformDataIn_ = glGetUniformLocation(programId_, "contrastExp");
-
-	textureUniformStep_ = glGetUniformLocation(programId_, "tex_step");
-	textureUniformSize_ = glGetUniformLocation(programId_, "tex_size");
-	textureUniformStrideFactor_ = glGetUniformLocation(programId_, "stride_factor");
-	textureUniformBayerFirstRed_ = glGetUniformLocation(programId_, "tex_bayer_first_red");
-	textureUniformProjMatrix_ = glGetUniformLocation(programId_, "proj_matrix");
-
-	LOG(Debayer, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
-			    << " tex_y " << textureUniformBayerDataIn_
-			    << " ccm " << ccmUniformDataIn_
-			    << " blacklevel " << blackLevelUniformDataIn_
-			    << " gamma " << gammaUniformDataIn_
-			    << " contrastExp " << contrastExpUniformDataIn_
-			    << " tex_step " << textureUniformStep_
-			    << " tex_size " << textureUniformSize_
-			    << " stride_factor " << textureUniformStrideFactor_
-			    << " tex_bayer_first_red " << textureUniformBayerFirstRed_
-			    << " proj_matrix " << textureUniformProjMatrix_;
-	return 0;
-}
-
 int SoftwareIspPipelineGpu::initBayerShaders(PixelFormat inputFormat, PixelFormat outputFormat)
 {
-	std::vector<std::string> shaderEnv;
-	unsigned int fragmentShaderDataLen = 0;
-	const unsigned char *fragmentShaderData = 0;
-	unsigned int vertexShaderDataLen = 0;
-	const unsigned char *vertexShaderData = 0;
-	GLenum err;
-
-	/* Target gles 100 glsl requires "#version x" as first directive in shader */
-	egl_.pushEnv(shaderEnv, "#version 100");
-
-	/* Specify GL_OES_EGL_image_external */
-	egl_.pushEnv(shaderEnv, "#extension GL_OES_EGL_image_external: enable");
-
-	/*
-	 * Tell shaders how to re-order output taking account of how the pixels
-	 * are actually stored by EGL.
-	 */
-	switch (outputFormat) {
-	case formats::ARGB8888:
-	case formats::XRGB8888:
-		break;
-	case formats::ABGR8888:
-	case formats::XBGR8888:
-		egl_.pushEnv(shaderEnv, "#define SWAP_BLUE");
-		break;
-	default:
-		LOG(Debayer, Error) << "Unsupported output format";
-		return -EINVAL;
-	}
-
-	/* Pixel location parameters */
-	glFormat_ = GL_LUMINANCE;
-	bytesPerPixel_ = 1;
-	shaderStridePixels_ = inputConfig_.stride;
-
-	switch (inputFormat) {
-	case libcamera::formats::SBGGR8:
-	case libcamera::formats::SBGGR10_CSI2P:
-	case libcamera::formats::SBGGR12_CSI2P:
-		firstRed_x_ = 1.0;
-		firstRed_y_ = 1.0;
-		break;
-	case libcamera::formats::SGBRG8:
-	case libcamera::formats::SGBRG10_CSI2P:
-	case libcamera::formats::SGBRG12_CSI2P:
-		firstRed_x_ = 0.0;
-		firstRed_y_ = 1.0;
-		break;
-	case libcamera::formats::SGRBG8:
-	case libcamera::formats::SGRBG10_CSI2P:
-	case libcamera::formats::SGRBG12_CSI2P:
-		firstRed_x_ = 1.0;
-		firstRed_y_ = 0.0;
-		break;
-	case libcamera::formats::SRGGB8:
-	case libcamera::formats::SRGGB10_CSI2P:
-	case libcamera::formats::SRGGB12_CSI2P:
-		firstRed_x_ = 0.0;
-		firstRed_y_ = 0.0;
-		break;
-	default:
-		LOG(Debayer, Error) << "Unsupported input format";
-		return -EINVAL;
-	};
-
-	/* Shader selection */
-	switch (inputFormat) {
-	case libcamera::formats::SBGGR8:
-	case libcamera::formats::SGBRG8:
-	case libcamera::formats::SGRBG8:
-	case libcamera::formats::SRGGB8:
-		fragmentShaderData = bayer_unpacked_frag;
-		fragmentShaderDataLen = bayer_unpacked_frag_len;
-		vertexShaderData = bayer_unpacked_vert;
-		vertexShaderDataLen = bayer_unpacked_vert_len;
-		break;
-	case libcamera::formats::SBGGR10_CSI2P:
-	case libcamera::formats::SGBRG10_CSI2P:
-	case libcamera::formats::SGRBG10_CSI2P:
-	case libcamera::formats::SRGGB10_CSI2P:
-		egl_.pushEnv(shaderEnv, "#define RAW10P");
-		if (BayerFormat::fromPixelFormat(inputFormat).packing == BayerFormat::Packing::None) {
-			fragmentShaderData = bayer_unpacked_frag;
-			fragmentShaderDataLen = bayer_unpacked_frag_len;
-			vertexShaderData = bayer_unpacked_vert;
-			vertexShaderDataLen = bayer_unpacked_vert_len;
-			glFormat_ = GL_RG;
-			bytesPerPixel_ = 2;
-		} else {
-			fragmentShaderData = bayer_1x_packed_frag;
-			fragmentShaderDataLen = bayer_1x_packed_frag_len;
-			vertexShaderData = identity_vert;
-			vertexShaderDataLen = identity_vert_len;
-			shaderStridePixels_ = width_;
-		}
-		break;
-	case libcamera::formats::SBGGR12_CSI2P:
-	case libcamera::formats::SGBRG12_CSI2P:
-	case libcamera::formats::SGRBG12_CSI2P:
-	case libcamera::formats::SRGGB12_CSI2P:
-		egl_.pushEnv(shaderEnv, "#define RAW12P");
-		if (BayerFormat::fromPixelFormat(inputFormat).packing == BayerFormat::Packing::None) {
-			fragmentShaderData = bayer_unpacked_frag;
-			fragmentShaderDataLen = bayer_unpacked_frag_len;
-			vertexShaderData = bayer_unpacked_vert;
-			vertexShaderDataLen = bayer_unpacked_vert_len;
-			glFormat_ = GL_RG;
-			bytesPerPixel_ = 2;
-		} else {
-			fragmentShaderData = bayer_1x_packed_frag;
-			fragmentShaderDataLen = bayer_1x_packed_frag_len;
-			vertexShaderData = identity_vert;
-			vertexShaderDataLen = identity_vert_len;
-			shaderStridePixels_ = width_;
-		}
-		break;
-	};
-
-	if (egl_.compileVertexShader(vertexShaderId_, vertexShaderData, vertexShaderDataLen, shaderEnv)) {
-		LOG(Debayer, Error) << "Compile vertex shader fail";
-		return -ENODEV;
-	}
-	utils::scope_exit vShaderGuard([&] { glDeleteShader(vertexShaderId_); });
-
-	if (egl_.compileFragmentShader(fragmentShaderId_, fragmentShaderData, fragmentShaderDataLen, shaderEnv)) {
-		LOG(Debayer, Error) << "Compile fragment shader fail";
-		return -ENODEV;
-	}
-	utils::scope_exit fShaderGuard([&] { glDeleteShader(fragmentShaderId_); });
+	int ret;
 
-	if (egl_.linkProgram(programId_, vertexShaderId_, fragmentShaderId_)) {
-		LOG(Debayer, Error) << "Linking program fail";
-		return -ENODEV;
-	}
-
-	egl_.dumpShaderSource(vertexShaderId_);
-	egl_.dumpShaderSource(fragmentShaderId_);
-
-	/* Ensure we set the programId_ */
-	egl_.useProgram(programId_);
-	err = glGetError();
-	if (err != GL_NO_ERROR) {
-		LOG(Debayer, Error) << "Use program error " << err;
-		return -ENODEV;
-	}
+	ret = gpuIspShaderPassDemosiac_.initShaders(inputFormat, outputFormat);
 
-	return getShaderVariableLocations();
+	return ret;
 }
 
 int SoftwareIspPipelineGpu::configure(const StreamConfiguration &inputCfg,
@@ -351,6 +178,12 @@ int SoftwareIspPipelineGpu::configure(const StreamConfiguration &inputCfg,
 	 */
 	stats_->setWindow(Rectangle(window_.size()));
 
+	/* Configure for one pass */
+	PassConfig rawSensorIn = { inputCfg.size, inputConfig_.stride, inputPixelFormat_, window_ };
+	PassConfig rgbaOut = { outputCfg.size, outputConfig_.stride, outputPixelFormat_, Rectangle(outputSize_) };
+
+	gpuIspShaderPassDemosiac_.configure(rawSensorIn, rgbaOut);
+
 	return 0;
 }
 
@@ -388,132 +221,10 @@ SoftwareIspPipelineGpu::strideAndFrameSize(const PixelFormat &outputFormat, cons
 	return std::make_tuple(stride, stride * size.height);
 }
 
-void SoftwareIspPipelineGpu::setShaderVariableValues(const DebayerParams &params)
-{
-	/*
-	 * Raw Bayer 8-bit, and packed raw Bayer 10-bit/12-bit formats
-	 * are stored in a GL_LUMINANCE texture. The texture width is
-	 * equal to the stride.
-	 */
-	GLfloat firstRed[] = { firstRed_x_, firstRed_y_ };
-	GLfloat imgSize[] = { (GLfloat)width_,
-			      (GLfloat)height_ };
-	GLfloat Step[] = { static_cast<float>(bytesPerPixel_) / (inputConfig_.stride - 1),
-			   1.0f / (height_ - 1) };
-	GLfloat Stride = (GLfloat)width_ / (shaderStridePixels_ / bytesPerPixel_);
-	/*
-	 * Scale input to output size, keeping the aspect ratio and preferring
-	 * cropping over black bars.
-	 */
-	GLfloat scale = std::max((GLfloat)window_.width / width_,
-				 (GLfloat)window_.height / height_);
-	GLfloat trans = -(1.0f - scale);
-	GLfloat projMatrix[] = {
-		scale, 0, 0, 0,
-		0, scale, 0, 0,
-		0, 0, 1, 0,
-		trans, trans, 0, 1
-	};
-	/* Static const coordinates */
-	static const GLfloat vcoordinates[4][2] = {
-		{ -1.0f, -1.0f },
-		{ -1.0f, +1.0f },
-		{ +1.0f, +1.0f },
-		{ +1.0f, -1.0f },
-	};
-	static const GLfloat tcoordinates[4][2] = {
-		{ 0.0f, 0.0f },
-		{ 0.0f, 1.0f },
-		{ 1.0f, 1.0f },
-		{ 1.0f, 0.0f },
-	};
-
-	/* vertexIn - bayer_8.vert */
-	glEnableVertexAttribArray(attributeVertex_);
-	glVertexAttribPointer(attributeVertex_, 2, GL_FLOAT, GL_TRUE,
-			      2 * sizeof(GLfloat), vcoordinates);
-
-	/* textureIn - bayer_8.vert */
-	glEnableVertexAttribArray(attributeTexture_);
-	glVertexAttribPointer(attributeTexture_, 2, GL_FLOAT, GL_TRUE,
-			      2 * sizeof(GLfloat), tcoordinates);
-
-	/*
-	 * Set the sampler2D to the respective texture unit for each texutre
-	 * To simultaneously sample multiple textures we need to use multiple
-	 * texture units
-	 */
-	glUniform1i(textureUniformBayerDataIn_, eglImageBayerIn_->texture_unit_uniform_id_);
-
-	/*
-	 * 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
-	 */
-	glUniform2fv(textureUniformBayerFirstRed_, 1, firstRed);
-	glUniform2fv(textureUniformSize_, 1, imgSize);
-	glUniform2fv(textureUniformStep_, 1, Step);
-	glUniform1f(textureUniformStrideFactor_, Stride);
-	glUniformMatrix4fv(textureUniformProjMatrix_, 1, GL_FALSE, projMatrix);
-
-	LOG(Debayer, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
-			    << " tex_y " << textureUniformBayerDataIn_
-			    << " tex_step " << textureUniformStep_
-			    << " tex_size " << textureUniformSize_
-			    << " stride_factor " << textureUniformStrideFactor_
-			    << " tex_bayer_first_red " << textureUniformBayerFirstRed_;
-
-	LOG(Debayer, Debug) << "textureUniformY_ = 0 "
-			    << " firstRed.x " << firstRed[0]
-			    << " firstRed.y " << firstRed[1]
-			    << " textureUniformSize_.width " << imgSize[0]
-			    << " textureUniformSize_.height " << imgSize[1]
-			    << " textureUniformStep_.x " << Step[0]
-			    << " textureUniformStep_.y " << Step[1]
-			    << " textureUniformStrideFactor_ " << Stride
-			    << " textureUniformProjMatrix_ " << textureUniformProjMatrix_;
-
-	GLfloat ccm[9] = {
-		params.combinedMatrix[0][0],
-		params.combinedMatrix[0][1],
-		params.combinedMatrix[0][2],
-		params.combinedMatrix[1][0],
-		params.combinedMatrix[1][1],
-		params.combinedMatrix[1][2],
-		params.combinedMatrix[2][0],
-		params.combinedMatrix[2][1],
-		params.combinedMatrix[2][2],
-	};
-	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_FALSE, ccm);
-	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
-
-	/*
-	 * 0 = Red, 1 = Green, 2 = Blue
-	 */
-	glUniform3f(blackLevelUniformDataIn_, params.blackLevel[0], params.blackLevel[1], params.blackLevel[2]);
-	LOG(Debayer, Debug) << " blackLevelUniformDataIn_ " << blackLevelUniformDataIn_ << " data " << params.blackLevel;
-
-	/*
-	 * Gamma
-	 */
-	glUniform1f(gammaUniformDataIn_, params.gamma);
-	LOG(Debayer, Debug) << " gammaUniformDataIn_ " << gammaUniformDataIn_ << " data " << params.gamma;
-
-	/*
-	 * Contrast
-	 */
-	glUniform1f(contrastExpUniformDataIn_, params.contrastExp);
-	LOG(Debayer, Debug) << " contrastExpUniformDataIn_ " << contrastExpUniformDataIn_ << " data " << params.contrastExp;
-
-	return;
-}
-
 int SoftwareIspPipelineGpu::processGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *inMapped, std::optional<DmaSyncer> *inDmaSyncer)
 {
 	bool dmabuf_import_succeeded = false;
+	int pipelineResult = 0;
 
 	/* eGL context switch */
 	egl_.makeCurrent();
@@ -540,20 +251,17 @@ int SoftwareIspPipelineGpu::processGPU(FrameBuffer *input, FrameBuffer *output,
 	/* Generate the output render framebuffer as render to texture */
 	egl_.createOutputDMABufTexture2D(*eglImageRGBAOut_, output->planes()[0].fd.get());
 
-	setShaderVariableValues(params);
-	glViewport(0, 0, width_, height_);
-	glClear(GL_COLOR_BUFFER_BIT);
-	glDrawArrays(GL_TRIANGLE_FAN, 0, DEBAYER_OPENGL_COORDS);
+	pipelineResult = gpuIspShaderPassDemosiac_.process(*eglImageBayerIn_, *eglImageRGBAOut_, width_, height_, params);
 
 	GLenum err = glGetError();
 	if (err != GL_NO_ERROR) {
 		LOG(eGL, Error) << "Drawing scene fail " << err;
-		return -ENODEV;
+		pipelineResult = -ENODEV;
 	} else {
 		egl_.flushOutput();
 	}
 
-	return 0;
+	return pipelineResult;
 }
 
 void SoftwareIspPipelineGpu::process(uint32_t frame, FrameBuffer *input, FrameBuffer *output, const DebayerParams &params)
@@ -623,7 +331,7 @@ int SoftwareIspPipelineGpu::start()
 		return -EINVAL;
 
 	/* Raw bayer input as texture */
-	eglImageBayerIn_ = std::make_unique<eGLImage>(glFormat_, inputConfig_.stride / bytesPerPixel_, height_, inputConfig_.stride, GL_TEXTURE0, 0);
+	eglImageBayerIn_ = std::make_unique<eGLImage>(gpuIspShaderPassDemosiac_.glFormat_, inputConfig_.stride / gpuIspShaderPassDemosiac_.getBytesPerPixel(), height_, inputConfig_.stride, GL_TEXTURE0, 0);
 
 	/* Texture we will render to */
 	eglImageRGBAOut_ = std::make_unique<eGLImage>(GL_RGBA, outputSize_.width, outputSize_.height, outputConfig_.stride, GL_TEXTURE1, 1);
@@ -636,8 +344,7 @@ void SoftwareIspPipelineGpu::stop()
 	eglImageRGBAOut_.reset();
 	eglImageBayerIn_.reset();
 
-	if (programId_)
-		glDeleteProgram(programId_);
+	gpuIspShaderPassDemosiac_.stop();
 }
 
 SizeRange SoftwareIspPipelineGpu::sizes(PixelFormat inputFormat, const Size &inputSize)
diff --git a/src/libcamera/software_isp/software_isp_pipeline_gpu.h b/src/libcamera/software_isp/software_isp_pipeline_gpu.h
index f0515d889..6a9df2f04 100644
--- a/src/libcamera/software_isp/software_isp_pipeline_gpu.h
+++ b/src/libcamera/software_isp/software_isp_pipeline_gpu.h
@@ -23,16 +23,12 @@
 #include "libcamera/internal/software_isp/benchmark.h"
 #include "libcamera/internal/software_isp/swstats_cpu.h"
 
-#include <EGL/egl.h>
-#include <EGL/eglext.h>
-#include <GLES3/gl32.h>
-
 #include "software_isp_pipeline.h"
+#include "gpu_pipeline_shader_pass_demosiac.h"
 
 namespace libcamera {
 
 #define DEBAYER_EGL_MIN_SIMPLE_RGB_GAIN_TEXTURE_UNITS 4
-#define DEBAYER_OPENGL_COORDS 4
 
 class CameraManager;
 
@@ -68,48 +64,17 @@ private:
 	int processGPU(FrameBuffer *input, FrameBuffer *output, const DebayerParams &params, std::optional<MappedFrameBuffer> *mappedInputBuffer, std::optional<DmaSyncer> *inputBufferDmaSyncer);
 	void configureTexture(GLuint &texture);
 
-	/* Shader program identifiers */
-	GLuint vertexShaderId_ = 0;
-	GLuint fragmentShaderId_ = 0;
-	GLuint programId_ = 0;
-
 	/* Pointer to object representing input texture */
 	std::unique_ptr<eGLImage> eglImageBayerIn_;
 	std::unique_ptr<eGLImage> eglImageRGBAOut_;
 
-	/* Shader parameters */
-	float firstRed_x_;
-	float firstRed_y_;
-	GLint attributeVertex_;
-	GLint attributeTexture_;
-	GLint textureUniformStep_;
-	GLint textureUniformSize_;
-	GLint textureUniformStrideFactor_;
-	GLint textureUniformBayerFirstRed_;
-	GLint textureUniformProjMatrix_;
-
-	GLint textureUniformBayerDataIn_;
-
-	/* 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_;
-
 	Rectangle window_;
 	std::unique_ptr<SwStatsCpu> stats_;
 	eGL egl_;
 	uint32_t width_;
 	uint32_t height_;
-	GLint glFormat_;
-	unsigned int bytesPerPixel_;
-	uint32_t shaderStridePixels_;
+
+	GpuIspShaderPassDemosiac gpuIspShaderPassDemosiac_;
 };
 
 } /* namespace libcamera */
