[v3] libcamera: software_isp: Add support for raw monochrome formats
diff mbox series

Message ID 81d74e44-11c7-4ffd-8044-dd54fa368a8c@laposte.net
State New
Headers show
Series
  • [v3] libcamera: software_isp: Add support for raw monochrome formats
Related show

Commit Message

Alain Cousinie Oct. 6, 2026, 11:41 a.m. UTC
From bfa7f6d2a6a3ac71b88f812c51e5ca8d993c92c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alain=20Cousini=C3=A9?= <alain.cousinie@laposte.net>
Date: Thu, 1 Oct 2026 16:31:05 +0200
Subject: [PATCH v3] libcamera: software_isp: Add support for raw monochrome formats
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Implement support for monochrome packed and unpacked formats in the software ISP and shaders.

Signed-off-by: Alain CousiniƩ <alain.cousinie@laposte.net>
---

Notes:
    Changes since v2:
    - Added comprehensive support and processing pipelines for R8 (8-bit) and R12 (12-bit) monochrome formats.
    - Synchronized implementation with the latest upstream libcamera codebase.
    - Enforced strict monochrome processing inside swstats_cpu.cpp (patternSize, sumShift, and ySkipMask alignment).
    - Added robust, alignment-safe CPU debayering routines (debayer_Mono and debayerP_Mono) with manual byte reconstruction.
    - Updated GPU rendering pipelines (EGL and shaders) to completely eliminate runtime branching and properly handle McGuire vertex shader inputs.
    
    This patch was tested on an HP 14-eu0xxx laptop with the og0va1b camera module (testing on og0ve1b is currently in progress) using qcam with CPU and GPU mode.

 src/libcamera/shaders/meson.build             |  2 +
 src/libcamera/shaders/raw_mono_1x_packed.frag | 75 +++++++++++++++
 src/libcamera/shaders/raw_mono_unpacked.frag  | 58 +++++++++++
 src/libcamera/software_isp/debayer_cpu.cpp    | 96 +++++++++++++++++++
 src/libcamera/software_isp/debayer_cpu.h      |  5 +
 src/libcamera/software_isp/debayer_egl.cpp    | 52 ++++++++++
 src/libcamera/software_isp/swstats_cpu.cpp    | 32 +++++++
 7 files changed, 320 insertions(+)
 create mode 100644 src/libcamera/shaders/raw_mono_1x_packed.frag
 create mode 100644 src/libcamera/shaders/raw_mono_unpacked.frag

Patch
diff mbox series

diff --git a/src/libcamera/shaders/meson.build b/src/libcamera/shaders/meson.build
index c409ff9b0..546053051 100644
--- a/src/libcamera/shaders/meson.build
+++ b/src/libcamera/shaders/meson.build
@@ -7,6 +7,8 @@  shader_files = files([
     'bayer_unpacked.frag',
     'bayer_unpacked.vert',
     'identity.vert',
+    'raw_mono_1x_packed.frag',
+    'raw_mono_unpacked.frag',
 ])
 
 # Generate header from shaders
diff --git a/src/libcamera/shaders/raw_mono_1x_packed.frag b/src/libcamera/shaders/raw_mono_1x_packed.frag
new file mode 100644
index 000000000..8461b9bc0
--- /dev/null
+++ b/src/libcamera/shaders/raw_mono_1x_packed.frag
@@ -0,0 +1,75 @@ 
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2026, Alain CousiniƩ
+ * raw_mono_1x_packed.frag - Fragment shader for packed monochrome formats (R10_CSI2P & R12_CSI2P)
+ */
+
+#ifdef GL_ES
+precision highp float;
+#endif
+
+varying vec2 textureOut;
+
+uniform vec2 tex_size;
+uniform sampler2D tex_y;
+uniform float gamma;
+uniform float contrastExp;
+
+float apply_contrast(float value)
+{
+	if (value < 0.5)
+		return 0.5 * pow(value / 0.5, contrastExp);
+	else
+		return 1.0 - 0.5 * pow((1.0 - value) / 0.5, contrastExp);
+}
+
+float read_byte(float pixel_x, float pixel_y)
+{
+	vec2 uv = vec2(pixel_x + 0.5, pixel_y + 0.5) / tex_size;
+	return floor(texture2D(tex_y, uv).r * 255.0 + 0.5);
+}
+
+void main(void)
+{
+	vec2 pixel_coords = floor(textureOut * tex_size);
+	float px = pixel_coords.x;
+	float py = pixel_coords.y;
+
+	float gray_val = 0.0;
+
+#ifdef MONO_RAW12
+	int index = int(mod(px, 2.0));
+	float block_start = floor(px / 2.0) * 3.0;
+
+	float g = read_byte(block_start + float(index), py);
+	float lsb = read_byte(block_start + 2.0, py);
+
+	int lsb_shift = (index == 0) ? 2 : 6;
+	float lsb_bits = mod(floor(lsb / pow(2.0, float(lsb_shift))), 4.0);
+
+	gray_val = (g * 4.0) + lsb_bits;
+#else
+	int index = int(mod(px, 4.0));
+	float block_start = floor(px / 4.0) * 5.0;
+
+	float g = read_byte(block_start + float(index), py);
+	float lsb = read_byte(block_start + 4.0, py);
+
+	int lsb_shift = index * 2;
+	float lsb_bits = mod(floor(lsb / pow(2.0, float(lsb_shift))), 4.0); // & 0x03
+
+	gray_val = (g * 4.0) + lsb_bits;
+#endif
+
+	float C = gray_val / 1023.0;
+	vec3 rgb = vec3(C, C, C);
+
+	/* Generic rendering processing */
+	rgb = clamp(rgb, 0.0, 1.0);
+	rgb.r = apply_contrast(rgb.r);
+	rgb.g = apply_contrast(rgb.g);
+	rgb.b = apply_contrast(rgb.b);
+	rgb = pow(rgb, vec3(gamma));
+
+	gl_FragColor = vec4(rgb, 1.0);
+}
diff --git a/src/libcamera/shaders/raw_mono_unpacked.frag b/src/libcamera/shaders/raw_mono_unpacked.frag
new file mode 100644
index 000000000..cb1ef2890
--- /dev/null
+++ b/src/libcamera/shaders/raw_mono_unpacked.frag
@@ -0,0 +1,58 @@ 
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2026, Alain CousiniƩ
+ * raw_mono_unpacked.frag - Fragment shader for raw unpacked monochrome formats (R8, R10, R12)
+ */
+
+#extension GL_EXT_texture_rg : enable
+
+#ifdef GL_ES
+precision highp float;
+#endif
+
+varying vec4            center;
+varying vec4            xCoord;
+varying vec4            yCoord;
+
+uniform sampler2D tex_y;
+uniform float gamma;
+uniform float contrastExp;
+
+float apply_contrast(float value)
+{
+	if (value < 0.5)
+		return 0.5 * pow(value / 0.5, contrastExp);
+	else
+		return 1.0 - 0.5 * pow((1.0 - value) / 0.5, contrastExp);
+}
+
+void main(void)
+{
+	vec4 texel = texture2D(tex_y, center.xy);
+
+	float byte_low  = floor(texel.r * 255.0 + 0.5);
+	float byte_high = floor(texel.g * 255.0 + 0.5);
+
+	float native_val = (byte_high * 256.0) + byte_low;
+
+	float gray_bits = 0.0;
+
+#ifdef MONO_RAW12
+	gray_bits = floor(native_val / 4.0);
+#else
+	gray_bits = native_val;
+#endif
+
+	float C = gray_bits / 1023.0;
+
+	vec3 rgb = vec3(C, C, C);
+
+	/* Generic rendering processing */
+	rgb = clamp(rgb, 0.0, 1.0);
+	rgb.r = apply_contrast(rgb.r);
+	rgb.g = apply_contrast(rgb.g);
+	rgb.b = apply_contrast(rgb.b);
+	rgb = pow(rgb, vec3(gamma));
+
+	gl_FragColor = vec4(rgb, 1.0);
+}
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
index ce8b3c647..887b1a6a0 100644
--- a/src/libcamera/software_isp/debayer_cpu.cpp
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -424,6 +424,72 @@  void DebayerCpu::debayer12P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[])
 	}
 }
 
+template<bool addAlphaByte, bool ccmEnabled>
+void DebayerCpu::debayer_Mono(uint8_t *dst, const uint8_t *src[])
+{
+	if (inputConfig_.bpp == 8) {
+		const uint8_t *cur = src[1] + xShift_;
+
+		for (int x = 0; x < (int)window_.width;) {
+			uint16_t pixel_val = *cur++;
+
+			STORE_PIXEL(pixel_val, pixel_val, pixel_val)
+		}
+		return;
+	}
+
+	const uint8_t *cur = src[1] + (xShift_ * 2);
+	const int shift = (inputConfig_.bpp == 10) ? 6 : 2;
+	for (int x = 0; x < (int)window_.width;) {
+		uint8_t low_byte = *cur++;
+		uint8_t high_byte = *cur++;
+
+		uint16_t native_val = (static_cast<uint16_t>(high_byte) << 8) | low_byte;
+		uint16_t pixel_val = native_val >> shift;
+
+		STORE_PIXEL(pixel_val, pixel_val, pixel_val)
+	}
+}
+
+template<bool addAlphaByte, bool ccmEnabled>
+void DebayerCpu::debayerP_Mono(uint8_t *dst, const uint8_t *src[])
+{
+	const uint8_t *cur = src[1];
+
+	if (inputConfig_.bpp == 10) {
+		for (int x = 0; x < (int)window_.width;) {
+			uint16_t g0 = *cur++;
+			uint16_t g1 = *cur++;
+			uint16_t g2 = *cur++;
+			uint16_t g3 = *cur++;
+			uint8_t lsb = *cur++;
+
+			uint16_t gray0 = (g0 << 2) | (lsb & 0x03);
+			uint16_t gray1 = (g1 << 2) | ((lsb >> 2) & 0x03);
+			uint16_t gray2 = (g2 << 2) | ((lsb >> 4) & 0x03);
+			uint16_t gray3 = (g3 << 2) | ((lsb >> 6) & 0x03);
+
+			STORE_PIXEL(gray0, gray0, gray0)
+			STORE_PIXEL(gray1, gray1, gray1)
+			STORE_PIXEL(gray2, gray2, gray2)
+			STORE_PIXEL(gray3, gray3, gray3)
+		}
+		return;
+	}
+	/* 12 bits Packed */
+	for (int x = 0; x < (int)window_.width;) {
+		uint16_t g0 = *cur++;
+		uint16_t g1 = *cur++;
+		uint8_t lsb = *cur++;
+
+		uint16_t gray0 = (g0 << 2) | ((lsb >> 2) & 0x03);
+		uint16_t gray1 = (g1 << 2) | ((lsb >> 6) & 0x03);
+
+		STORE_PIXEL(gray0, gray0, gray0)
+		STORE_PIXEL(gray1, gray1, gray1)
+	}
+}
+
 /*
  * Setup the Debayer object according to the passed in parameters.
  * Return 0 on success, a negative errno value on failure
@@ -439,6 +505,21 @@  int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
 						   formats::BGR888,
 						   formats::XBGR8888,
 						   formats::ABGR8888 };
+	if (bayerFormat.order == BayerFormat::Order::MONO) {
+		if (bayerFormat.packing == BayerFormat::Packing::None) {
+			config.bpp = (bayerFormat.bitDepth + 7) & ~7;
+			config.patternSize.width = 2;
+			config.patternSize.height = 2;
+		}
+		else if (bayerFormat.packing == BayerFormat::Packing::CSI2) {
+			config.bpp = bayerFormat.bitDepth;
+			config.patternSize.width = (bayerFormat.bitDepth == 10) ? 4 : 2;
+			config.patternSize.height = 2;
+		}
+
+		config.outputFormats = outputFormats;
+		return 0;
+	}
 
 	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10 || bayerFormat.bitDepth == 12) &&
 	    bayerFormat.packing == BayerFormat::Packing::None &&
@@ -525,6 +606,21 @@  int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat,
 		return -EINVAL;
 	};
 
+	if (bayerFormat.order == BayerFormat::Order::MONO) {
+		if (outputFormat == formats::XRGB8888 || outputFormat == formats::ARGB8888 ||
+		    outputFormat == formats::XBGR8888 || outputFormat == formats::ABGR8888) {
+			addAlphaByte = true;
+		}
+		if (bayerFormat.packing == BayerFormat::Packing::None) {
+			SET_DEBAYER_METHODS(debayer_Mono, debayer_Mono)
+			return 0;
+		} else if (bayerFormat.packing == BayerFormat::Packing::CSI2) {
+			SET_DEBAYER_METHODS(debayerP_Mono, debayerP_Mono)
+			return 0;
+		}
+		return invalidFmt();
+	}
+
 	switch (outputFormat) {
 	case formats::XRGB8888:
 	case formats::ARGB8888:
diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
index 2c88c9e1a..909798fa4 100644
--- a/src/libcamera/software_isp/debayer_cpu.h
+++ b/src/libcamera/software_isp/debayer_cpu.h
@@ -119,6 +119,11 @@  private:
 	void debayer12P_GBGB_BGR888(uint8_t *dst, const uint8_t *src[]);
 	template<bool addAlphaByte, bool ccmEnabled>
 	void debayer12P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[]);
+	/* CSI-2 packed and unpacked raw mono */
+	template<bool addAlphaByte, bool ccmEnabled>
+	void debayer_Mono(uint8_t *dst, const uint8_t *src[]);
+	template<bool addAlphaByte, bool ccmEnabled>
+	void debayerP_Mono(uint8_t *dst, const uint8_t *src[]);
 
 	static int getInputConfig(PixelFormat inputFormat, DebayerInputConfig &config);
 	int setupStandardBayerOrder(BayerFormat::Order order);
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 300822f9d..616884885 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -61,6 +61,17 @@  int DebayerEGL::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &conf
 						   formats::XBGR8888,
 						   formats::ABGR8888 };
 
+	/* Handle Monochrome case using bayerFormat */
+	if (bayerFormat.order == BayerFormat::Order::MONO) {
+		bool isPacked = (bayerFormat.packing == BayerFormat::Packing::CSI2);
+
+		config.bpp = isPacked ? bayerFormat.bitDepth : ((bayerFormat.bitDepth + 7) & ~7);
+		config.patternSize.width = isPacked ? 4 : 2;
+		config.patternSize.height = 2;
+		config.outputFormats = outputFormats;
+		return 0;
+	}
+
 	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10) &&
 	    bayerFormat.packing == BayerFormat::Packing::None &&
 	    isStandardBayerOrder(bayerFormat.order)) {
@@ -166,6 +177,13 @@  int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
 	shaderStridePixels_ = inputConfig_.stride;
 
 	switch (inputFormat) {
+	/* Monochrome cases: No color: phase, proper initialization to 0 for Rx and Rx_CSI2P */
+	case libcamera::formats::R8:
+	case libcamera::formats::R10:
+	case libcamera::formats::R12:
+		firstRed_x_ = 0.0;
+		firstRed_y_ = 0.0;
+		break;
 	case libcamera::formats::SBGGR8:
 	case libcamera::formats::SBGGR10_CSI2P:
 	case libcamera::formats::SBGGR12_CSI2P:
@@ -197,6 +215,33 @@  int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
 
 	/* Shader selection */
 	switch (inputFormat) {
+	case libcamera::formats::R8: {
+		fragmentShaderData = raw_mono_unpacked_frag;
+		vertexShaderData = bayer_unpacked_vert;
+		glFormat_ = GL_LUMINANCE;
+		bytesPerPixel_ = 1;
+		break;
+	}
+	case libcamera::formats::R12:
+		egl_.pushEnv(shaderEnv, "#define MONO_RAW12");
+		[[fallthrough]];
+	case libcamera::formats::R10: {
+		BayerFormat bayerFormat = BayerFormat::fromPixelFormat(inputFormat);
+
+		if (bayerFormat.packing == BayerFormat::Packing::None) {
+			fragmentShaderData = raw_mono_unpacked_frag;
+			vertexShaderData = bayer_unpacked_vert;
+			glFormat_ = GL_RG;
+			bytesPerPixel_ = 2;
+		} else {
+			fragmentShaderData = raw_mono_1x_packed_frag;
+			vertexShaderData = identity_vert;
+			glFormat_ = GL_LUMINANCE;
+			bytesPerPixel_ = 1;
+			shaderStridePixels_ = inputConfig_.stride;
+		}
+		break;
+	}
 	case libcamera::formats::SBGGR8:
 	case libcamera::formats::SGBRG8:
 	case libcamera::formats::SGRBG8:
@@ -516,6 +561,13 @@  eGLImage *DebayerEGL::getCachedInputFrameBuffer(FrameBuffer *input, std::optiona
 		eglImageInCache_.emplace_back(fd, std::make_unique<eGLImage>(glFormat_, inputConfig_.stride / bytesPerPixel_, height_, inputConfig_.stride, GL_TEXTURE0, 0));
 		eglImageIn = eglImageInCache_.back().second.get();
 
+		/* Force GL_NEAREST to preserve LSBs of packed monochrome format */
+		BayerFormat bayerFormat = BayerFormat::fromPixelFormat(inputPixelFormat_);
+		if (bayerFormat.order == BayerFormat::Order::MONO && bayerFormat.packing == BayerFormat::Packing::CSI2) {
+			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+			glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+		}
+
 		if (egl_.createInputDMABufTexture2D(*eglImageIn, input->planes()[0].fd.get()) == 0)
 			return eglImageIn;
 
diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
index 7fb77ce7d..e3090ec1f 100644
--- a/src/libcamera/software_isp/swstats_cpu.cpp
+++ b/src/libcamera/software_isp/swstats_cpu.cpp
@@ -494,6 +494,38 @@  int SwStatsCpu::configure(const StreamConfiguration &inputCfg, unsigned int stat
 
 	uint8_t bitDepth = bayerFormat.bitDepth;
 
+	if (bayerFormat.order == BayerFormat::Order::MONO) {
+		if (bayerFormat.packing == BayerFormat::Packing::None) {
+			patternSize_.width = 2;
+			patternSize_.height = 2;
+
+			if (bitDepth == 8) {
+				stats0_ = &SwStatsCpu::statsBGGR8Line0;
+				sumShift_ = 0;
+			} else if (bitDepth == 10) {
+				stats0_ = &SwStatsCpu::statsBGGR10Line0;
+				sumShift_ = 2;
+			} else { /* 12 bits */
+				stats0_ = &SwStatsCpu::statsBGGR12Line0;
+				sumShift_ = 4;
+			}
+		}
+		else if (bayerFormat.packing == BayerFormat::Packing::CSI2) {
+			patternSize_.width = (bitDepth == 10) ? 4 : 2;
+			patternSize_.height = 2;
+
+			stats0_ = (bitDepth == 10) ? &SwStatsCpu::statsBGGR10PLine0 : &SwStatsCpu::statsBGGR12PLine0;
+			sumShift_ = 0;
+		}
+
+		ySkipMask_ = 0x02; /* Skip every 3th and 4th line */
+		xShift_ = 0;
+		swapLines_ = false;
+		processFrame_ = &SwStatsCpu::processBayerFrame2;
+
+		return 0;
+	}
+
 	if ((bitDepth == 10 || bitDepth == 12) &&
 	    bayerFormat.packing == BayerFormat::Packing::CSI2) {
 		if (bitDepth == 10)