diff --git a/src/libcamera/software_isp/gpu_pipeline_shader_pass_demosiac.cpp b/src/libcamera/software_isp/gpu_pipeline_shader_pass_demosiac.cpp
new file mode 100644
index 000000000..02d9da2f0
--- /dev/null
+++ b/src/libcamera/software_isp/gpu_pipeline_shader_pass_demosiac.cpp
@@ -0,0 +1,357 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Linaro Ltd
+ *
+ * GPU ISP Demosiac pass
+ */
+
+#include <cmath>
+#include <stdint.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/thread.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/controls.h>
+#include <libcamera/formats.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/framebuffer.h"
+#include "libcamera/internal/ipa_manager.h"
+#include "libcamera/internal/software_isp/debayer_params.h"
+
+#include "gpu_pipeline_shader_pass_demosiac.h"
+
+/**
+ * \file software_isp.cpp
+ * \brief Simple software ISP implementation
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(GpuShaderDemosiac)
+
+int GpuIspShaderPassDemosiac::start()
+{
+	return 0;
+}
+
+void GpuIspShaderPassDemosiac::stop()
+{
+}
+
+int GpuIspShaderPassDemosiac::initShaders(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(GpuShaderDemosiac, Error) << "Unsupported output format";
+		return -EINVAL;
+	}
+
+	/* Pixel location parameters */
+	glFormat_ = GL_LUMINANCE;
+	bytesPerPixel_ = 1;
+	shaderStridePixels_ = passInputCfg_.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(GpuShaderDemosiac, 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_ = passInputCfg_.size.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_ = passInputCfg_.size.width;
+		}
+		break;
+	};
+
+	/* TODO: move from here to the end of the method into a helper function in the base class
+	 *       this logic will be common to all pipeline instances
+	 */
+	if (egl_.compileVertexShader(vertexShaderId_, vertexShaderData, vertexShaderDataLen, shaderEnv)) {
+		LOG(GpuShaderDemosiac, Error) << "Compile vertex shader fail";
+		return -ENODEV;
+	}
+	utils::scope_exit vShaderGuard([&] { glDeleteShader(vertexShaderId_); });
+
+	if (egl_.compileFragmentShader(fragmentShaderId_, fragmentShaderData, fragmentShaderDataLen, shaderEnv)) {
+		LOG(GpuShaderDemosiac, Error) << "Compile fragment shader fail";
+		return -ENODEV;
+	}
+	utils::scope_exit fShaderGuard([&] { glDeleteShader(fragmentShaderId_); });
+
+	if (egl_.linkProgram(programId_, vertexShaderId_, fragmentShaderId_)) {
+		LOG(GpuShaderDemosiac, 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(GpuShaderDemosiac, Error) << "Use program error " << err;
+		return -ENODEV;
+	}
+
+	return getShaderVariableLocations();
+}
+
+int GpuIspShaderPassDemosiac::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(GpuShaderDemosiac, 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_;
+
+	/* TODO: trap errors */
+	return 0;
+}
+
+void GpuIspShaderPassDemosiac::setShaderVariableValues(const DebayerParams &params, eGLImage &eglImageIn)
+{
+	/*
+	 * 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)passInputCfg_.size.width,
+			      (GLfloat)passInputCfg_.size.height };
+	GLfloat Step[] = { static_cast<float>(bytesPerPixel_) / (passInputCfg_.stride - 1),
+			   1.0f / (passInputCfg_.size.height - 1) };
+	GLfloat Stride = (GLfloat)passInputCfg_.size.width / (shaderStridePixels_ / bytesPerPixel_);
+	/*
+	 * Scale input to output size, keeping the aspect ratio and preferring
+	 * cropping over black bars.
+	 */
+	GLfloat scale = std::max((GLfloat)passInputCfg_.window.width / passInputCfg_.size.width,
+				 (GLfloat)passInputCfg_.window.height / passInputCfg_.size.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_, eglImageIn.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(GpuShaderDemosiac, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
+			    << " tex_y " << textureUniformBayerDataIn_
+			    << " tex_step " << textureUniformStep_
+			    << " tex_size " << textureUniformSize_
+			    << " stride_factor " << textureUniformStrideFactor_
+			    << " tex_bayer_first_red " << textureUniformBayerFirstRed_;
+
+	LOG(GpuShaderDemosiac, 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(GpuShaderDemosiac, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
+
+	/*
+	 * 0 = Red, 1 = Green, 2 = Blue
+	 */
+	glUniform3f(blackLevelUniformDataIn_, params.blackLevel[0], params.blackLevel[1], params.blackLevel[2]);
+	LOG(GpuShaderDemosiac, Debug) << " blackLevelUniformDataIn_ " << blackLevelUniformDataIn_ << " data " << params.blackLevel;
+
+	/*
+	 * Gamma
+	 */
+	glUniform1f(gammaUniformDataIn_, params.gamma);
+	LOG(GpuShaderDemosiac, Debug) << " gammaUniformDataIn_ " << gammaUniformDataIn_ << " data " << params.gamma;
+
+	/*
+	 * Contrast
+	 */
+	glUniform1f(contrastExpUniformDataIn_, params.contrastExp);
+	LOG(GpuShaderDemosiac, Debug) << " contrastExpUniformDataIn_ " << contrastExpUniformDataIn_ << " data " << params.contrastExp;
+
+	return;
+}
+
+}
diff --git a/src/libcamera/software_isp/gpu_pipeline_shader_pass_demosiac.h b/src/libcamera/software_isp/gpu_pipeline_shader_pass_demosiac.h
new file mode 100644
index 000000000..11bb04c30
--- /dev/null
+++ b/src/libcamera/software_isp/gpu_pipeline_shader_pass_demosiac.h
@@ -0,0 +1,72 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Linaro Ltd
+ *
+ * Authors:
+ * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
+ *
+ * GpuIspIspShaderPass base class
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/object.h>
+#include <libcamera/base/signal.h>
+
+#include <libcamera/geometry.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/egl.h"
+#include "libcamera/internal/software_isp/debayer_params.h"
+
+#include "gpu_pipeline_shader_pass.h"
+
+namespace libcamera {
+
+class FrameBuffer;
+
+class GpuIspShaderPassDemosiac : public GpuIspShaderPass
+{
+public:
+	GpuIspShaderPassDemosiac(eGL& egl) : GpuIspShaderPass(egl) {};
+
+	int start();
+	void stop();
+
+	/* Things that every ISP pipeline pass will need to do */
+	int initShaders(PixelFormat inputFormat, PixelFormat outputFormat);
+	int getShaderVariableLocations(void);
+	void setShaderVariableValues(const DebayerParams &params, eGLImage &eglImageIn);
+	const char *name() const override { return "GpuIspShaderPassDemosiac"; }
+private:
+	/* Shader parameters */
+	float firstRed_x_;
+	float firstRed_y_;
+	GLint attributeVertex_;
+	GLint attributeTexture_;
+	GLint textureUniformStep_;
+	GLint textureUniformSize_;
+	GLint textureUniformStrideFactor_;
+	GLint textureUniformBayerFirstRed_;
+	GLint textureUniformProjMatrix_;
+	GLint textureUniformBayerDataIn_;
+
+	uint32_t shaderStridePixels_;
+
+	/* 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_;
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/software_isp/meson.build b/src/libcamera/software_isp/meson.build
index f8bce5203..3e7d21318 100644
--- a/src/libcamera/software_isp/meson.build
+++ b/src/libcamera/software_isp/meson.build
@@ -33,6 +33,7 @@ if mesa_works
         '../egl.cpp',
         'software_isp_pipeline_gpu.cpp',
         'gpu_pipeline_shader_pass.cpp',
+        'gpu_pipeline_shader_pass_demosiac.cpp',
     ])
     libcamera_deps += [
         libegl,
