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

Message ID 81d74e44-11c7-4ffd-8044-dd54fa368a8c@laposte.net
State Superseded
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

Comments

Alain Cousinie Oct. 6, 2026, 4:24 p.m. UTC | #1
On 06/10/2026 à 15:11, Barnabás Pőcze Wrote :
> 
> I think we should ideally remove this condition by having separate
> functions, like it is done for color formats. Same below. Any reason
> it was done this way? E.g. debayer{8,10,12,etc.}_MONO_BGR888.
> 
[...]
> > Generally camelCase is preferred.
>
[...]> 
> Does it make sense to use the color functions? I'd imagine that this
> will calculate incorrect statistics. I'm not sure how `sum_` could
> be made sense, but for `yHistogram` I imagine the ideal thing to do
> is to take every pixel with a weight of 1. But if you use the color
> variants, then that's not going to happen.
> 

Hi,
Thanks for the review and the feedback!

Those are very good points. Separating the functions, switching to camelCase, 
and looking into dedicated monochrome statistics functions make total sense 
to align with libcamera's architecture.

I will wait a bit to see if there are any other comments from the list 
before implementing these changes into a v4 patch series.

Best regards,
Alain
Milan Zamazal Oct. 7, 2026, 4:57 p.m. UTC | #2
Alain Cousinie <alain.cousinie@laposte.net> writes:

>>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.

Please see my comments on v2.

>     
>     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
>
> 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);

This is difficult to understand.  I suppose it converts 12-bit to
10-bit.  Please explain in the commit message why doing the 12->10 bit
conversion and add a comment here what the computation does.  Also, is
there perhaps a more efficient way to compute this, especially
considering index can have only two values?

> +
> +	gray_val = (g * 4.0) + lsb_bits;
> +#else

Is this part Mono 10 encoding?  Worth adding a comment.

> +	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;

The last two code lines are the same as in the MONO_RAW12 branch, so
maybe they are better to move out of the #ifdef (if the lines should
really be the same).

> +#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);

The values are the same, apply_contrast can be called just once, on C
(the same in the other shader).

> +	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)

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)