new file mode 100644
@@ -0,0 +1,228 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Linaro Ltd
+ *
+ * Authors:
+ * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
+ *
+ * GpuIspShaderPassBlcNormalise base class
+ */
+
+#include <stdint.h>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/object.h>
+#include <libcamera/base/signal.h>
+
+#include <libcamera/geometry.h>
+#include <libcamera/formats.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/egl.h"
+#include "libcamera/internal/software_isp/debayer_params.h"
+
+#include "gpu_pipeline_shader_pass_blc_normalise.h"
+
+/**
+ * \file software_isp.cpp
+ * \brief Simple software ISP implementation
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(GpuShaderBlc)
+
+int GpuIspShaderPassBlcNormalise::start()
+{
+ return 0;
+}
+
+void GpuIspShaderPassBlcNormalise::stop()
+{
+}
+
+int GpuIspShaderPassBlcNormalise::selectShaders(struct ShaderConfig &shaderCfg, [[maybe_unused]]PixelFormat &inputFormat, [[maybe_unused]]PixelFormat &outputFormat)
+{
+ /* Pixel location parameters */
+ glFormat_ = GL_LUMINANCE;
+ bytesPerPixel_ = 1;
+ shaderStridePixels_ = passInputCfg_.stride;
+
+ /* Shader selection */
+ switch (inputFormat) {
+ case libcamera::formats::SBGGR8:
+ case libcamera::formats::SGBRG8:
+ case libcamera::formats::SGRBG8:
+ case libcamera::formats::SRGGB8:
+ shaderCfg.fragmentShaderData = bayer_unpacked_to_blc_glr16f_frag;
+ shaderCfg.fragmentShaderDataLen = bayer_unpacked_to_blc_glr16f_frag_len;
+ shaderCfg.vertexShaderData = bayer_unpacked_vert;
+ shaderCfg.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(shaderCfg.shaderEnv, "#define RAW10P");
+ if (BayerFormat::fromPixelFormat(inputFormat).packing == BayerFormat::Packing::None) {
+ shaderCfg.fragmentShaderData = bayer_unpacked_to_blc_glr16f_frag;
+ shaderCfg.fragmentShaderDataLen = bayer_unpacked_to_blc_glr16f_frag_len;
+ shaderCfg.vertexShaderData = bayer_unpacked_vert;
+ shaderCfg.vertexShaderDataLen = bayer_unpacked_vert_len;
+ glFormat_ = GL_RG;
+ bytesPerPixel_ = 2;
+ } else {
+ shaderCfg.fragmentShaderData = bayer_1x_packed_to_blc_glr16f_frag;
+ shaderCfg.fragmentShaderDataLen = bayer_1x_packed_to_blc_glr16f_frag_len;
+ shaderCfg.vertexShaderData = identity_vert;
+ shaderCfg.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(shaderCfg.shaderEnv, "#define RAW12P");
+ if (BayerFormat::fromPixelFormat(inputFormat).packing == BayerFormat::Packing::None) {
+ shaderCfg.fragmentShaderData = bayer_unpacked_to_blc_glr16f_frag;
+ shaderCfg.fragmentShaderDataLen = bayer_unpacked_to_blc_glr16f_frag_len;
+ shaderCfg.vertexShaderData = bayer_unpacked_vert;
+ shaderCfg.vertexShaderDataLen = bayer_unpacked_vert_len;
+ glFormat_ = GL_RG;
+ bytesPerPixel_ = 2;
+ } else {
+ shaderCfg.fragmentShaderData = bayer_1x_packed_to_blc_glr16f_frag;
+ shaderCfg.fragmentShaderDataLen = bayer_1x_packed_to_blc_glr16f_frag_len;
+ shaderCfg.vertexShaderData = identity_vert;
+ shaderCfg.vertexShaderDataLen = identity_vert_len;
+ shaderStridePixels_ = passInputCfg_.size.width;
+ }
+ break;
+ };
+
+ return 0;
+}
+
+int GpuIspShaderPassBlcNormalise::getShaderVariableLocations(void)
+{
+ attributeVertex_ = glGetAttribLocation(programId_, "vertexIn");
+ attributeTexture_ = glGetAttribLocation(programId_, "textureIn");
+ textureUniformBayerDataIn_ = glGetUniformLocation(programId_, "tex_y");
+ 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(GpuShaderBlc, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
+ << " tex_y " << textureUniformBayerDataIn_
+ << " tex_step " << textureUniformStep_
+ << " tex_size " << textureUniformSize_
+ << " stride_factor " << textureUniformStrideFactor_
+ << " tex_bayer_first_red " << textureUniformBayerFirstRed_
+ << " proj_matrix " << textureUniformProjMatrix_;
+
+ /* TODO: trap errors */
+ return 0;
+}
+
+void GpuIspShaderPassBlcNormalise::setShaderVariableValues(const DebayerParams ¶ms, 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(GpuShaderBlc, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
+ << " tex_y " << textureUniformBayerDataIn_
+ << " tex_step " << textureUniformStep_
+ << " tex_size " << textureUniformSize_
+ << " stride_factor " << textureUniformStrideFactor_
+ << " tex_bayer_first_red " << textureUniformBayerFirstRed_;
+
+ LOG(GpuShaderBlc, 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_;
+
+ /*
+ * 0 = Red, 1 = Green, 2 = Blue
+ */
+ glUniform3f(blackLevelUniformDataIn_, params.blackLevel[0], params.blackLevel[1], params.blackLevel[2]);
+ LOG(GpuShaderBlc, Debug) << " blackLevelUniformDataIn_ " << blackLevelUniformDataIn_ << " data " << params.blackLevel;
+
+ return;
+}
+
+} /* namespace libcamera */
new file mode 100644
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2026, Linaro Ltd
+ *
+ * Authors:
+ * Bryan O'Donoghue <bryan.odonoghue@linaro.org>
+ *
+ * GpuIspPipelineShaderPass 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 GpuIspShaderPassBlcNormalise : public GpuIspShaderPass
+{
+public:
+ GpuIspShaderPassBlcNormalise(eGL& egl) : GpuIspShaderPass(egl) {};
+
+ int start();
+ void stop();
+
+ /* Things that every ISP pipeline pass will need to do */
+ int getShaderVariableLocations(void);
+ void setShaderVariableValues(const DebayerParams ¶ms, eGLImage &eglImageIn);
+ int selectShaders(struct ShaderConfig &shaderCfg, PixelFormat &inputFormat, PixelFormat &outputFormat);
+ const char *name() const override { return "GpuIspShaderPassBlcNormalise"; }
+
+private:
+ /* Shader parameters */
+ GLint textureUniformStep_;
+ GLint textureUniformSize_;
+
+ /* Black Level compensation */
+ GLint blackLevelUniformDataIn_;
+
+};
+
+} /* namespace libcamera */
@@ -34,6 +34,7 @@ if mesa_works
'software_isp_pipeline_gpu.cpp',
'gpu_pipeline_shader_pass.cpp',
'gpu_pipeline_shader_pass_demosiac.cpp',
+ 'gpu_pipeline_shader_pass_blc_normalise.cpp',
])
libcamera_deps += [
libegl,
Make GpuIspShaderPassBlcNormalise which takes existing logic to select fragment/vertex shaders based on the incoming CSI2 packign but, select new shaders whose task in life is to apply our blacklevel normalisation step and output a normalised 16 bit float. The unpacked 16f will then be fed into the existing unpacked demosiac shader. This patch compiles but doesn't do anything useful yet. Signed-off-by: Bryan O'Donoghue <bryan.odonoghue@linaro.org> --- ...gpu_pipeline_shader_pass_blc_normalise.cpp | 228 ++++++++++++++++++ .../gpu_pipeline_shader_pass_blc_normalise.h | 55 +++++ src/libcamera/software_isp/meson.build | 1 + 3 files changed, 284 insertions(+) create mode 100644 src/libcamera/software_isp/gpu_pipeline_shader_pass_blc_normalise.cpp create mode 100644 src/libcamera/software_isp/gpu_pipeline_shader_pass_blc_normalise.h