From patchwork Tue Oct 6 11:41:14 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Alain Cousinie X-Patchwork-Id: 28412 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id AAF2FC32F4 for ; Tue, 6 Oct 2026 11:41:17 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 69C7568B50; Tue, 6 Oct 2026 13:41:16 +0200 (CEST) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (2048-bit key; unprotected) header.d=laposte.net header.i=@laposte.net header.b="pT+UUiCk"; dkim-atps=neutral Received: from smtp-outgoing-1901.laposte.net (smtp-outgoing-1901.laposte.net [160.92.124.105]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 30C9768A7A for ; Tue, 6 Oct 2026 13:41:15 +0200 (CEST) X-mail-filterd: {"version":"1.9.7", "queueID":"4hzZ8V3lbmz1GBvm", "contextId": "dce52725-9707-4efe-850f-51fc2c6d4d7b"} Received: from outgoing-mail.laposte.net (localhost.localdomain [127.0.0.1]) by mlpnf0114.laposte.net (SMTP Server) with ESMTP id 4hzZ8V3lbmz1GBvm for ; Tue, 6 Oct 2026 13:41:14 +0200 (CEST) X-mail-filterd: {"version":"1.9.7", "queueID":"4hzZ8V2Wf2z1GBvc", "contextId": "0a4dcc26-5e28-49dc-bfbb-4e0a75aa1e17"} X-lpn-mailing: LEGIT X-lpn-spamrating: 40 X-lpn-spamlevel: not-spam Received: from [192.168.1.2] (m-dj.hd.free.fr [82.67.82.126]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mlpnf0114.laposte.net (SMTP Server) with ESMTPSA id 4hzZ8V2Wf2z1GBvc for ; Tue, 6 Oct 2026 13:41:14 +0200 (CEST) Message-ID: <81d74e44-11c7-4ffd-8044-dd54fa368a8c@laposte.net> Date: Tue, 6 Oct 2026 13:41:14 +0200 MIME-Version: 1.0 User-Agent: Mozilla Thunderbird Content-Language: fr To: libcamera-devel@lists.libcamera.org From: Alain Cousinie Subject: [PATCH v3] libcamera: software_isp: Add support for raw monochrome formats DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=laposte.net; s=lpn-wlmd; t=1791286874; bh=j0E+b0/uIJEdJ4fGmealgg5rUyh06jtV06+yjrJezGs=; h=Message-ID:Date:MIME-Version:Content-Language:To:From:Subject:Content-Type:Content-Transfer-Encoding; b=pT+UUiCkQk4YurBBtSYb14d1HoZOuXdoXZbr+q5o4mkEfDTZhgZSnnnQ3ZN9hasSWcr3ARVdru1QsMJcM2im/BSnHNz5AkFqfUiBNWdnzSUIYzMJiGV8pJsHYF9BteTgWIbNGlfD+SuLPavKoSCspYF7REItpLvSfVs84+DfP2mizVNYQDi8V8hr/2dEjqYNOvUz6adEtnCuBc/876Pc/BzEy6E5thi8kO0dKW9NDJq7b32X5InqUx7ecMCP5o5Hxv7G7VmTX5RKeuWDS+hvim0mzUSVsozARDqOWwOZ0JMH4o++6+gekWF0iW5xofnab22UgYmBk2HPhBLLufgh5Q==; X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" From bfa7f6d2a6a3ac71b88f812c51e5ca8d993c92c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alain=20Cousini=C3=A9?= 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Ʃ --- 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 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 +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(high_byte) << 8) | low_byte; + uint16_t pixel_val = native_val >> shift; + + STORE_PIXEL(pixel_val, pixel_val, pixel_val) + } +} + +template +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 void debayer12P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[]); + /* CSI-2 packed and unpacked raw mono */ + template + void debayer_Mono(uint8_t *dst, const uint8_t *src[]); + template + 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(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)