[v2] libcamera: software_isp: Add R10 and R10_CSI2P monochrome formats
diff mbox series

Message ID 20261003155838.91284-1-alain.cousinie@laposte.net
State New
Headers show
Series
  • [v2] libcamera: software_isp: Add R10 and R10_CSI2P monochrome formats
Related show

Commit Message

Alain Cousinie Oct. 3, 2026, 3:58 p.m. UTC
Implement support for R10 and R10_CSI2P monochrome packed and
unpacked formats in the software ISP and shaders.

Signed-off-by: Alain Cousinié <alain.cousinie@laposte.net>
---
This patch was tested on an HP 14-eu0xxx laptop with the og0va1b camera module (testing on og0ve1b is currently in progress) using qcam.

 src/libcamera/shaders/meson.build             |  2 +
 src/libcamera/shaders/raw_mono_1x_packed.frag | 50 +++++++++++++++++++
 src/libcamera/shaders/raw_mono_unpacked.frag  | 47 +++++++++++++++++
 src/libcamera/software_isp/debayer_egl.cpp    | 42 ++++++++++++++++
 4 files changed, 141 insertions(+)
 create mode 100644 src/libcamera/shaders/raw_mono_1x_packed.frag
 create mode 100644 src/libcamera/shaders/raw_mono_unpacked.frag
---
--
2.55.0

Comments

Milan Zamazal Oct. 5, 2026, 11:58 a.m. UTC | #1
Hi Alain,

thank you for the patch.

Alain Cousinié <alain.cousinie@laposte.net> writes:

> Implement support for R10 and R10_CSI2P monochrome packed and
> unpacked formats in the software ISP and shaders.
>
> Signed-off-by: Alain Cousinié <alain.cousinie@laposte.net>
> ---
> This patch was tested on an HP 14-eu0xxx laptop with the og0va1b camera module (testing on og0ve1b is currently in progress) using qcam.
>
>  src/libcamera/shaders/meson.build             |  2 +
>  src/libcamera/shaders/raw_mono_1x_packed.frag | 50 +++++++++++++++++++
>  src/libcamera/shaders/raw_mono_unpacked.frag  | 47 +++++++++++++++++
>  src/libcamera/software_isp/debayer_egl.cpp    | 42 ++++++++++++++++
>  4 files changed, 141 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..983834cc8
> --- /dev/null
> +++ b/src/libcamera/shaders/raw_mono_1x_packed.frag
> @@ -0,0 +1,50 @@
> +/* SPDX-License-Identifier: BSD-2-Clause */
> +/*
> + * Copyright (C) 2026, Alain Cousinié
> + * raw_mono_1x_packed.frag - Fragment shader for packed monochrome formats (R10_CSI2P)

We don't put file names into the headers any more, just the description:

  Fragment shader for packed monochrome formats (R10_CSI2P)

The same for the other shader.

> + */
> +
> +#ifdef GL_ES
> +precision highp float;
> +#endif
> +
> +#define BPP		1.25
> +
> +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);
> +}
> +
> +void main(void)
> +{
> +	vec2 pixel_coords = floor(textureOut * tex_size);
> +
> +	vec2 center_pixel = pixel_coords + vec2(0.5);
> +	vec2 center_bytes;
> +	center_bytes.y = center_pixel.y;
> +	center_bytes.x = floor(BPP * center_pixel.x);
> +
> +	center_bytes = center_bytes / tex_size;
> +
> +	/* Linear sampling: GPU merges byte transitions. */
> +	float C = texture2D(tex_y, center_bytes).r;
> +	vec3 rgb = vec3(C, C, C);

No need to subtract black level?

(There should be no need to duplicate all the processing once the
multi-shaders effort is finished but this is not going to happen soon.)

> +
> +	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..f8a0c14ae
> --- /dev/null
> +++ b/src/libcamera/shaders/raw_mono_unpacked.frag
> @@ -0,0 +1,47 @@
> +/* SPDX-License-Identifier: BSD-2-Clause */
> +/*
> + * Copyright (C) 2026, Alain Cousinié
> + * raw_mono_unpacked.frag - Fragment shader for raw unpacked monochrome formats (R10)
> + */
> +
> +#extension GL_EXT_texture_rg : enable
> +
> +#ifdef GL_ES
> +precision highp float;
> +#endif
> +
> +varying vec2 textureOut;
> +
> +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)
> +{
> +	/* GL_RG format sampling (16 bits):
> +	 * The .r component contains the 8 MSBs,
> +	 * The .g component contains the remaining LSBs.
> +	 */
> +	vec4 texel = texture2D(tex_y, textureOut);
> +	float C = texel.r + texel.g / 256.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_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
> index 300822f9d..2a57fb7d4 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 = bayerFormat.bitDepth; /* Dynamic (10 bits for this sensor) */
> +		config.patternSize.width = isPacked ? 4 : 1;
> +		config.patternSize.height = 1; /* No 2x2 pattern in monochrome */
> +		config.outputFormats = outputFormats;
> +		return 0;
> +	}
> +
>  	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10) &&
>  	    bayerFormat.packing == BayerFormat::Packing::None &&
>  	    isStandardBayerOrder(bayerFormat.order)) {
> @@ -166,6 +177,11 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
>  	shaderStridePixels_ = inputConfig_.stride;
>
>  	switch (inputFormat) {
> +	/* Monochrome cases: No color phase, proper initialization to 0 for R10 and R10_CSI2P */
> +	case libcamera::formats::R10:
> +		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 +213,25 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
>
>  	/* Shader selection */
>  	switch (inputFormat) {
> +	case libcamera::formats::R10: {
> +		/* Raw 10-bit Monochrome Case (Handles both Packed R10_CSI2P and Unpacked R10 sharing the same value) */
> +		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;
> +			/* Use configured input stride instead of theoretical width */
> +			glFormat_ = GL_LUMINANCE;
> +			bytesPerPixel_ = 1;
> +			shaderStridePixels_ = inputConfig_.stride;
> +		}
> +		break;
> +	}
>  	case libcamera::formats::SBGGR8:
>  	case libcamera::formats::SGBRG8:
>  	case libcamera::formats::SGRBG8:
> @@ -516,6 +551,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);

Why is this needed here?  Isn't GL_NEAREST used in EGL debayering already?

> +		}
> +
>  		if (egl_.createInputDMABufTexture2D(*eglImageIn, input->planes()[0].fd.get()) == 0)
>  			return eglImageIn;
>
> --
> 2.55.0
Hans de Goede Oct. 12, 2026, 3:10 p.m. UTC | #2
Hi Alaine,

On 3-Oct-26 17:58, Alain Cousinié wrote:
> Implement support for R10 and R10_CSI2P monochrome packed and
> unpacked formats in the software ISP and shaders.
> 
> Signed-off-by: Alain Cousinié <alain.cousinie@laposte.net>

First of all thank you for your work on this!

Some more remarks on top of Milan's remark.

1. I'm not sure how this can even work at all, you do not seem to add any
   support for mono formats to src/libcamera/software_isp/swstats_cpu.cpp
   so I would expect the stats_->configure() call in the swisp to fail ?

2. Likewise to get the IPA to work well and not try to do AWB on r/g/b
   sums which I don't expect the swstats code to fill in the monochrome case,
   you should pass some mono flag to the IPA at some point and make the IPA
   load uncalibrated-mono.yaml instead of uncalibrated.yaml and disable some
   algos like awb which do not make sense for a mono sensor (they can be
   disabled in the yaml file).

3. It would be really nice to also have a debayer_cpu.cpp implentation of this,
   we've always expected the 2 (cpu vs gpu) to diverge over time, e.g. LSC will
   likely be a GPU only thing. But this seems something which should be easily
   doable on CPU re-usig the existing code to populate a single combined
   "gamma" lookup which does black-level substract + gamma + contrast all in
   a singe lookup. This is more of a nice to have, where as I would consider
   1. and 2. must have-s.

Regards,

Hans




> ---
> This patch was tested on an HP 14-eu0xxx laptop with the og0va1b camera module (testing on og0ve1b is currently in progress) using qcam.
> 
>  src/libcamera/shaders/meson.build             |  2 +
>  src/libcamera/shaders/raw_mono_1x_packed.frag | 50 +++++++++++++++++++
>  src/libcamera/shaders/raw_mono_unpacked.frag  | 47 +++++++++++++++++
>  src/libcamera/software_isp/debayer_egl.cpp    | 42 ++++++++++++++++
>  4 files changed, 141 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..983834cc8
> --- /dev/null
> +++ b/src/libcamera/shaders/raw_mono_1x_packed.frag
> @@ -0,0 +1,50 @@
> +/* SPDX-License-Identifier: BSD-2-Clause */
> +/*
> + * Copyright (C) 2026, Alain Cousinié
> + * raw_mono_1x_packed.frag - Fragment shader for packed monochrome formats (R10_CSI2P)
> + */
> +
> +#ifdef GL_ES
> +precision highp float;
> +#endif
> +
> +#define BPP		1.25
> +
> +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);
> +}
> +
> +void main(void)
> +{
> +	vec2 pixel_coords = floor(textureOut * tex_size);
> +
> +	vec2 center_pixel = pixel_coords + vec2(0.5);
> +	vec2 center_bytes;
> +	center_bytes.y = center_pixel.y;
> +	center_bytes.x = floor(BPP * center_pixel.x);
> +
> +	center_bytes = center_bytes / tex_size;
> +
> +	/* Linear sampling: GPU merges byte transitions. */
> +	float C = texture2D(tex_y, center_bytes).r;
> +	vec3 rgb = vec3(C, C, C);
> +
> +	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..f8a0c14ae
> --- /dev/null
> +++ b/src/libcamera/shaders/raw_mono_unpacked.frag
> @@ -0,0 +1,47 @@
> +/* SPDX-License-Identifier: BSD-2-Clause */
> +/*
> + * Copyright (C) 2026, Alain Cousinié
> + * raw_mono_unpacked.frag - Fragment shader for raw unpacked monochrome formats (R10)
> + */
> +
> +#extension GL_EXT_texture_rg : enable
> +
> +#ifdef GL_ES
> +precision highp float;
> +#endif
> +
> +varying vec2 textureOut;
> +
> +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)
> +{
> +	/* GL_RG format sampling (16 bits):
> +	 * The .r component contains the 8 MSBs,
> +	 * The .g component contains the remaining LSBs.
> +	 */
> +	vec4 texel = texture2D(tex_y, textureOut);
> +	float C = texel.r + texel.g / 256.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_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
> index 300822f9d..2a57fb7d4 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 = bayerFormat.bitDepth; /* Dynamic (10 bits for this sensor) */
> +		config.patternSize.width = isPacked ? 4 : 1;
> +		config.patternSize.height = 1; /* No 2x2 pattern in monochrome */
> +		config.outputFormats = outputFormats;
> +		return 0;
> +	}
> +
>  	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10) &&
>  	    bayerFormat.packing == BayerFormat::Packing::None &&
>  	    isStandardBayerOrder(bayerFormat.order)) {
> @@ -166,6 +177,11 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
>  	shaderStridePixels_ = inputConfig_.stride;
> 
>  	switch (inputFormat) {
> +	/* Monochrome cases: No color phase, proper initialization to 0 for R10 and R10_CSI2P */
> +	case libcamera::formats::R10:
> +		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 +213,25 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
> 
>  	/* Shader selection */
>  	switch (inputFormat) {
> +	case libcamera::formats::R10: {
> +		/* Raw 10-bit Monochrome Case (Handles both Packed R10_CSI2P and Unpacked R10 sharing the same value) */
> +		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;
> +			/* Use configured input stride instead of theoretical width */
> +			glFormat_ = GL_LUMINANCE;
> +			bytesPerPixel_ = 1;
> +			shaderStridePixels_ = inputConfig_.stride;
> +		}
> +		break;
> +	}
>  	case libcamera::formats::SBGGR8:
>  	case libcamera::formats::SGBRG8:
>  	case libcamera::formats::SGRBG8:
> @@ -516,6 +551,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;
> 
> --
> 2.55.0
Hans de Goede Oct. 12, 2026, 3:12 p.m. UTC | #3
Hi,

On 12-Oct-26 17:10, johannes.goede@oss.qualcomm.com wrote:
> Hi Alaine,
> 
> On 3-Oct-26 17:58, Alain Cousinié wrote:
>> Implement support for R10 and R10_CSI2P monochrome packed and
>> unpacked formats in the software ISP and shaders.
>>
>> Signed-off-by: Alain Cousinié <alain.cousinie@laposte.net>
> 
> First of all thank you for your work on this!
> 
> Some more remarks on top of Milan's remark.
> 
> 1. I'm not sure how this can even work at all, you do not seem to add any
>    support for mono formats to src/libcamera/software_isp/swstats_cpu.cpp
>    so I would expect the stats_->configure() call in the swisp to fail ?
> 
> 2. Likewise to get the IPA to work well and not try to do AWB on r/g/b
>    sums which I don't expect the swstats code to fill in the monochrome case,
>    you should pass some mono flag to the IPA at some point and make the IPA
>    load uncalibrated-mono.yaml instead of uncalibrated.yaml and disable some
>    algos like awb which do not make sense for a mono sensor (they can be
>    disabled in the yaml file).
> 
> 3. It would be really nice to also have a debayer_cpu.cpp implentation of this,
>    we've always expected the 2 (cpu vs gpu) to diverge over time, e.g. LSC will
>    likely be a GPU only thing. But this seems something which should be easily
>    doable on CPU re-usig the existing code to populate a single combined
>    "gamma" lookup which does black-level substract + gamma + contrast all in
>    a singe lookup. This is more of a nice to have, where as I would consider
>    1. and 2. must have-s.

Oops, I just see there is a newer version of this series addressing most, if not
all, of the above. Nevermind.

Regards,

Hans



>> ---
>> This patch was tested on an HP 14-eu0xxx laptop with the og0va1b camera module (testing on og0ve1b is currently in progress) using qcam.
>>
>>  src/libcamera/shaders/meson.build             |  2 +
>>  src/libcamera/shaders/raw_mono_1x_packed.frag | 50 +++++++++++++++++++
>>  src/libcamera/shaders/raw_mono_unpacked.frag  | 47 +++++++++++++++++
>>  src/libcamera/software_isp/debayer_egl.cpp    | 42 ++++++++++++++++
>>  4 files changed, 141 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..983834cc8
>> --- /dev/null
>> +++ b/src/libcamera/shaders/raw_mono_1x_packed.frag
>> @@ -0,0 +1,50 @@
>> +/* SPDX-License-Identifier: BSD-2-Clause */
>> +/*
>> + * Copyright (C) 2026, Alain Cousinié
>> + * raw_mono_1x_packed.frag - Fragment shader for packed monochrome formats (R10_CSI2P)
>> + */
>> +
>> +#ifdef GL_ES
>> +precision highp float;
>> +#endif
>> +
>> +#define BPP		1.25
>> +
>> +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);
>> +}
>> +
>> +void main(void)
>> +{
>> +	vec2 pixel_coords = floor(textureOut * tex_size);
>> +
>> +	vec2 center_pixel = pixel_coords + vec2(0.5);
>> +	vec2 center_bytes;
>> +	center_bytes.y = center_pixel.y;
>> +	center_bytes.x = floor(BPP * center_pixel.x);
>> +
>> +	center_bytes = center_bytes / tex_size;
>> +
>> +	/* Linear sampling: GPU merges byte transitions. */
>> +	float C = texture2D(tex_y, center_bytes).r;
>> +	vec3 rgb = vec3(C, C, C);
>> +
>> +	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..f8a0c14ae
>> --- /dev/null
>> +++ b/src/libcamera/shaders/raw_mono_unpacked.frag
>> @@ -0,0 +1,47 @@
>> +/* SPDX-License-Identifier: BSD-2-Clause */
>> +/*
>> + * Copyright (C) 2026, Alain Cousinié
>> + * raw_mono_unpacked.frag - Fragment shader for raw unpacked monochrome formats (R10)
>> + */
>> +
>> +#extension GL_EXT_texture_rg : enable
>> +
>> +#ifdef GL_ES
>> +precision highp float;
>> +#endif
>> +
>> +varying vec2 textureOut;
>> +
>> +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)
>> +{
>> +	/* GL_RG format sampling (16 bits):
>> +	 * The .r component contains the 8 MSBs,
>> +	 * The .g component contains the remaining LSBs.
>> +	 */
>> +	vec4 texel = texture2D(tex_y, textureOut);
>> +	float C = texel.r + texel.g / 256.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_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
>> index 300822f9d..2a57fb7d4 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 = bayerFormat.bitDepth; /* Dynamic (10 bits for this sensor) */
>> +		config.patternSize.width = isPacked ? 4 : 1;
>> +		config.patternSize.height = 1; /* No 2x2 pattern in monochrome */
>> +		config.outputFormats = outputFormats;
>> +		return 0;
>> +	}
>> +
>>  	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10) &&
>>  	    bayerFormat.packing == BayerFormat::Packing::None &&
>>  	    isStandardBayerOrder(bayerFormat.order)) {
>> @@ -166,6 +177,11 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
>>  	shaderStridePixels_ = inputConfig_.stride;
>>
>>  	switch (inputFormat) {
>> +	/* Monochrome cases: No color phase, proper initialization to 0 for R10 and R10_CSI2P */
>> +	case libcamera::formats::R10:
>> +		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 +213,25 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
>>
>>  	/* Shader selection */
>>  	switch (inputFormat) {
>> +	case libcamera::formats::R10: {
>> +		/* Raw 10-bit Monochrome Case (Handles both Packed R10_CSI2P and Unpacked R10 sharing the same value) */
>> +		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;
>> +			/* Use configured input stride instead of theoretical width */
>> +			glFormat_ = GL_LUMINANCE;
>> +			bytesPerPixel_ = 1;
>> +			shaderStridePixels_ = inputConfig_.stride;
>> +		}
>> +		break;
>> +	}
>>  	case libcamera::formats::SBGGR8:
>>  	case libcamera::formats::SGBRG8:
>>  	case libcamera::formats::SGRBG8:
>> @@ -516,6 +551,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;
>>
>> --
>> 2.55.0
>

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..983834cc8
--- /dev/null
+++ b/src/libcamera/shaders/raw_mono_1x_packed.frag
@@ -0,0 +1,50 @@ 
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2026, Alain Cousinié
+ * raw_mono_1x_packed.frag - Fragment shader for packed monochrome formats (R10_CSI2P)
+ */
+
+#ifdef GL_ES
+precision highp float;
+#endif
+
+#define BPP		1.25
+
+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);
+}
+
+void main(void)
+{
+	vec2 pixel_coords = floor(textureOut * tex_size);
+
+	vec2 center_pixel = pixel_coords + vec2(0.5);
+	vec2 center_bytes;
+	center_bytes.y = center_pixel.y;
+	center_bytes.x = floor(BPP * center_pixel.x);
+
+	center_bytes = center_bytes / tex_size;
+
+	/* Linear sampling: GPU merges byte transitions. */
+	float C = texture2D(tex_y, center_bytes).r;
+	vec3 rgb = vec3(C, C, C);
+
+	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..f8a0c14ae
--- /dev/null
+++ b/src/libcamera/shaders/raw_mono_unpacked.frag
@@ -0,0 +1,47 @@ 
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2026, Alain Cousinié
+ * raw_mono_unpacked.frag - Fragment shader for raw unpacked monochrome formats (R10)
+ */
+
+#extension GL_EXT_texture_rg : enable
+
+#ifdef GL_ES
+precision highp float;
+#endif
+
+varying vec2 textureOut;
+
+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)
+{
+	/* GL_RG format sampling (16 bits):
+	 * The .r component contains the 8 MSBs,
+	 * The .g component contains the remaining LSBs.
+	 */
+	vec4 texel = texture2D(tex_y, textureOut);
+	float C = texel.r + texel.g / 256.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_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 300822f9d..2a57fb7d4 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 = bayerFormat.bitDepth; /* Dynamic (10 bits for this sensor) */
+		config.patternSize.width = isPacked ? 4 : 1;
+		config.patternSize.height = 1; /* No 2x2 pattern in monochrome */
+		config.outputFormats = outputFormats;
+		return 0;
+	}
+
 	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10) &&
 	    bayerFormat.packing == BayerFormat::Packing::None &&
 	    isStandardBayerOrder(bayerFormat.order)) {
@@ -166,6 +177,11 @@  int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
 	shaderStridePixels_ = inputConfig_.stride;

 	switch (inputFormat) {
+	/* Monochrome cases: No color phase, proper initialization to 0 for R10 and R10_CSI2P */
+	case libcamera::formats::R10:
+		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 +213,25 @@  int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm

 	/* Shader selection */
 	switch (inputFormat) {
+	case libcamera::formats::R10: {
+		/* Raw 10-bit Monochrome Case (Handles both Packed R10_CSI2P and Unpacked R10 sharing the same value) */
+		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;
+			/* Use configured input stride instead of theoretical width */
+			glFormat_ = GL_LUMINANCE;
+			bytesPerPixel_ = 1;
+			shaderStridePixels_ = inputConfig_.stride;
+		}
+		break;
+	}
 	case libcamera::formats::SBGGR8:
 	case libcamera::formats::SGBRG8:
 	case libcamera::formats::SGRBG8:
@@ -516,6 +551,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;