[RFC,v8,6/6] libcamera: software_isp: debayer_egl: Add LSC support
diff mbox series

Message ID 20260716222515.251464-7-mzamazal@redhat.com
State Superseded
Headers show
Series
  • LSC for SoftISP simple pipeline
Related show

Commit Message

Milan Zamazal July 16, 2026, 10:25 p.m. UTC
From: Xander Pronk <xander.c.pronk@gmail.com>

Add support for passing the LSC table from debayerParams to the shaders.

The LSC table values are floats, we must add `type' parameter to
createTexture2D to support this.  Moreover, we use 16-bit internal
format for the values, primarily because this works for me, unlike
32-bit floats.

When LSC is disabled by the runtime control, we provide a constant 1.0
correction table, i.e. a no-op table.  This is preferred to having a
runtime flag, to not disturb GPU computation with runtime switches,
assuming that disabling LSC in runtime is not a common use case.

Co-developed-by: Rick ten Wolde <rick_libcamera@wolde.info>
Signed-off-by: Rick ten Wolde <rick_libcamera@wolde.info>
Signed-off-by: Xander Pronk <xander.c.pronk@gmail.com>
Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
---
 include/libcamera/internal/egl.h              |  6 +++-
 .../internal/software_isp/debayer_params.h    |  2 ++
 src/ipa/simple/algorithms/lsc.cpp             | 11 ++++---
 src/libcamera/egl.cpp                         | 10 +++++--
 src/libcamera/software_isp/debayer.cpp        |  5 ++++
 src/libcamera/software_isp/debayer_egl.cpp    | 29 +++++++++++++++++--
 src/libcamera/software_isp/debayer_egl.h      |  6 ++++
 7 files changed, 60 insertions(+), 9 deletions(-)

Comments

Bryan O'Donoghue July 20, 2026, 10:56 a.m. UTC | #1
On 16/07/2026 23:25, Milan Zamazal wrote:
> From: Xander Pronk <xander.c.pronk@gmail.com>
> 
> Add support for passing the LSC table from debayerParams to the shaders.
> 
> The LSC table values are floats, we must add `type' parameter to
> createTexture2D to support this.  Moreover, we use 16-bit internal
> format for the values, primarily because this works for me, unlike
> 32-bit floats.
> 
> When LSC is disabled by the runtime control, we provide a constant 1.0
> correction table, i.e. a no-op table.  This is preferred to having a
> runtime flag, to not disturb GPU computation with runtime switches,
> assuming that disabling LSC in runtime is not a common use case.
> 
> Co-developed-by: Rick ten Wolde <rick_libcamera@wolde.info>
> Signed-off-by: Rick ten Wolde <rick_libcamera@wolde.info>
> Signed-off-by: Xander Pronk <xander.c.pronk@gmail.com>
> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
> ---
>   include/libcamera/internal/egl.h              |  6 +++-
>   .../internal/software_isp/debayer_params.h    |  2 ++
>   src/ipa/simple/algorithms/lsc.cpp             | 11 ++++---
>   src/libcamera/egl.cpp                         | 10 +++++--
>   src/libcamera/software_isp/debayer.cpp        |  5 ++++
>   src/libcamera/software_isp/debayer_egl.cpp    | 29 +++++++++++++++++--
>   src/libcamera/software_isp/debayer_egl.h      |  6 ++++
>   7 files changed, 60 insertions(+), 9 deletions(-)
> 
> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
> index 030c813a5..37da2b18a 100644
> --- a/include/libcamera/internal/egl.h
> +++ b/include/libcamera/internal/egl.h
> @@ -107,7 +107,11 @@ public:
> 
>   	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
>   	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
> -	void createTexture2D(eGLImage &eglImage, void *data, GLint filter);
> +	void createTexture2D(eGLImage &eglImage,
> +			     GLint internalFormat,
> +			     GLenum type,
> +			     const void *data,
> +			     GLint filter);
> 
>   	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
>   	void makeCurrent();
> diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
> index e3e01bc04..76825e6c8 100644
> --- a/include/libcamera/internal/software_isp/debayer_params.h
> +++ b/include/libcamera/internal/software_isp/debayer_params.h
> @@ -29,6 +29,8 @@ struct DebayerParams {
> 
>   	static constexpr unsigned int kLscGridSize = 16;
>   	static constexpr unsigned int kLscValuesPerCell = 3;
> +	static constexpr unsigned int kLscBytesPerCell =
> +		kLscValuesPerCell * sizeof(float);
>   	using LscLookupTable =
>   		std::array<float, kLscGridSize * kLscGridSize * kLscValuesPerCell>;
>   	LscLookupTable lscLut{};
> diff --git a/src/ipa/simple/algorithms/lsc.cpp b/src/ipa/simple/algorithms/lsc.cpp
> index c531937c7..51c5b63d5 100644
> --- a/src/ipa/simple/algorithms/lsc.cpp
> +++ b/src/ipa/simple/algorithms/lsc.cpp
> @@ -46,13 +46,16 @@ void Lsc::prepare([[maybe_unused]] IPAContext &context,
>   		  IPAFrameContext &frameContext,
>   		  DebayerParams *params)
>   {
> -	params->lscEnabled = frameContext.lsc.enabled;
> -
>   	unsigned int ct = frameContext.awb.colourTemperature;
>   	constexpr unsigned int minTemperatureChange = 100;
> 
> -	if (!frameContext.lsc.enabled ||
> -	    utils::abs_diff(ct, lastAppliedCt_) < minTemperatureChange)
> +	if (!frameContext.lsc.enabled) {
> +		params->lscLut.fill(1.0);
> +		lastAppliedCt_ = 0;
> +		return;
> +	}
> +
> +	if (utils::abs_diff(ct, lastAppliedCt_) < minTemperatureChange)
>   		return;
> 
>   	const lsc::Components<uint8_t> &set = lscAlgo_.interpolateComponents(ct);
> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
> index aeac5d313..c2a158ce7 100644
> --- a/src/libcamera/egl.cpp
> +++ b/src/libcamera/egl.cpp
> @@ -241,6 +241,8 @@ int eGL::createOutputDMABufTexture2D(eGLImage &eglImage, int fd)
>   /**
>    * \brief Create a 2D texture from a memory buffer
>    * \param[in,out] eglImage EGL image to associate with the texture
> + * \param[in] internalFormat OpenGL internal storage format (e.g., GL_RGB8, GL_RGBA8)
> + * \param[in] type OpenGL pixel data type (e.g., GL_UNSIGNED_BYTE, GL_FLOAT)
>    * \param[in] data Pointer to pixel data, or nullptr for uninitialised texture
>    * \param[in] filter GL texture filter setting
>    *
> @@ -249,7 +251,11 @@ int eGL::createOutputDMABufTexture2D(eGLImage &eglImage, int fd)
>    * is useful for uploading static data like lookup tables or uniform color
>    * matrices to the GPU.
>    */
> -void eGL::createTexture2D(eGLImage &eglImage, void *data, GLint filter)
> +void eGL::createTexture2D(eGLImage &eglImage,
> +			  GLint internalFormat,
> +			  GLenum type,
> +			  const void *data,
> +			  GLint filter)
>   {
>   	ASSERT(tid_ == Thread::currentId());
> 
> @@ -257,7 +263,7 @@ void eGL::createTexture2D(eGLImage &eglImage, void *data, GLint filter)
>   	glBindTexture(GL_TEXTURE_2D, eglImage.texture_);
> 
>   	// Generate texture, bind, associate image to texture, configure, unbind
> -	glTexImage2D(GL_TEXTURE_2D, 0, eglImage.format_, eglImage.width_, eglImage.height_, 0, eglImage.format_, GL_UNSIGNED_BYTE, data);
> +	glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, eglImage.width_, eglImage.height_, 0, eglImage.format_, type, data);
> 
>   	// Nearest filtering
>   	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
> diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
> index 5ae9f032f..70c0e3336 100644
> --- a/src/libcamera/software_isp/debayer.cpp
> +++ b/src/libcamera/software_isp/debayer.cpp
> @@ -53,6 +53,11 @@ namespace libcamera {
>    * \brief Number of pixel values per each of the lens shading grid areas
>    */
> 
> +/**
> + * \var DebayerParams::kLscBytesPerCell
> + * \brief Number of bytes per each of the lens shading grid areas
> + */
> +
>   /**
>    * \typedef DebayerParams::LscLookupTable
>    * \brief Lookup table for lens shading correction
> diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
> index 562e9fc7a..0fdf0e6e8 100644
> --- a/src/libcamera/software_isp/debayer_egl.cpp
> +++ b/src/libcamera/software_isp/debayer_egl.cpp
> @@ -128,6 +128,8 @@ int DebayerEGL::getShaderVariableLocations(void)
>   	textureUniformBayerFirstRed_ = glGetUniformLocation(programId_, "tex_bayer_first_red");
>   	textureUniformProjMatrix_ = glGetUniformLocation(programId_, "proj_matrix");
> 
> +	textureUniformLsc_ = glGetUniformLocation(programId_, "lsc_tex");
> +
>   	LOG(Debayer, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
>   			    << " tex_y " << textureUniformBayerDataIn_
>   			    << " awb " << awbUniformDataIn_
> @@ -139,7 +141,8 @@ int DebayerEGL::getShaderVariableLocations(void)
>   			    << " tex_size " << textureUniformSize_
>   			    << " stride_factor " << textureUniformStrideFactor_
>   			    << " tex_bayer_first_red " << textureUniformBayerFirstRed_
> -			    << " proj_matrix " << textureUniformProjMatrix_;
> +			    << " proj_matrix " << textureUniformProjMatrix_
> +			    << " tex_lsc " << textureUniformLsc_;
>   	return 0;
>   }
> 
> @@ -156,6 +159,9 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
>   	/* Specify GL_OES_EGL_image_external */
>   	egl_.pushEnv(shaderEnv, "#extension GL_OES_EGL_image_external: enable");
> 
> +	if (lscEnabled_)
> +		egl_.pushEnv(shaderEnv, "#define APPLY_LSC");
> +
>   	/*
>   	 * Tell shaders how to re-order output taking account of how the pixels
>   	 * are actually stored by EGL.
> @@ -348,6 +354,18 @@ int DebayerEGL::configure(const StreamConfiguration &inputCfg,
>   	 */
>   	stats_->setWindow(Rectangle(window_.size()));
> 
> +	if (lscEnabled_) {
> +		constexpr unsigned int gridSize = DebayerParams::kLscGridSize;
> +		const unsigned int stride = gridSize * DebayerParams::kLscBytesPerCell;
> +		eglImageLscLookup_ =
> +			std::make_unique<eGLImage>(GL_RGB,
> +						   gridSize,
> +						   gridSize,
> +						   stride,
> +						   GL_TEXTURE2,
> +						   2);
> +	}
> +
>   	return 0;
>   }
> 
> @@ -483,6 +501,12 @@ void DebayerEGL::setShaderVariableValues(const DebayerParams &params)
>   	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_TRUE, params.combinedMatrix.data().data());
>   	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
> 
> +	if (lscEnabled_) {
> +		egl_.createTexture2D(*eglImageLscLookup_, GL_RGB16F, GL_FLOAT,
> +				     params.lscLut.data(), GL_LINEAR);

As we are discussing in the multi-pass thread, GL_RGB16F is dependent on 
the optional GL_EXT_color_buffer_half_float

https://registry.khronos.org/OpenGL/extensions/EXT/EXT_color_buffer_half_float.txt

I think it should be pretty easy to disjunct on that as a flag in the 
base class - a flag I plan to add for SSBO stats but also perfectly 
happy for you to add.

Half float linear filtering requires OES_texture_half_float_linear

glGetString(GL_EXTENSIONS) | grep OES_texture_half_float_linear | 
debayer_egl{ bool _has_OES_texture_half_float_linear;} or whatever

if (_has_OES_texture_half_float_linear)
	GL_RGB16F;

Could we repreent the LSC data as uint8 natively ? I mean without 
storing it all as float and then converting to uint8 on each LSC 
application cycle ?

> +		glUniform1i(textureUniformLsc_, eglImageLscLookup_->texture_unit_uniform_id_);
> +	}
> +
>   	/*
>   	 * 0 = Red, 1 = Green, 2 = Blue
>   	 */
> @@ -530,7 +554,8 @@ int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const Debaye
>   			LOG(Debayer, Error) << "mmap-ing buffer(s) failed";
>   			return -ENODEV;
>   		}
> -		egl_.createTexture2D(*eglImageBayerIn_, inMapped->value().planes()[0].data(), GL_NEAREST);
> +		egl_.createTexture2D(*eglImageBayerIn_, glFormat_, GL_UNSIGNED_BYTE,
> +				     inMapped->value().planes()[0].data(), GL_NEAREST);
>   	}
> 
>   	/* Generate the output render framebuffer as render to texture */
> diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
> index 82ed1305d..f3913d564 100644
> --- a/src/libcamera/software_isp/debayer_egl.h
> +++ b/src/libcamera/software_isp/debayer_egl.h
> @@ -78,7 +78,10 @@ private:
>   	std::unique_ptr<eGLImage> eglImageBayerIn_;
>   	std::unique_ptr<eGLImage> eglImageBayerOut_;
> 
> +	/* LSC lookup table */
> +	std::unique_ptr<eGLImage> eglImageLscLookup_;
>   	bool lscEnabled_;
> +
>   	/* Shader parameters */
>   	float firstRed_x_;
>   	float firstRed_y_;
> @@ -95,6 +98,9 @@ private:
>   	/* Per-frame AWB gains */
>   	GLint awbUniformDataIn_;
> 
> +	/* Lens shading correction */
> +	GLint textureUniformLsc_;
> +
>   	/* Represent per-frame CCM as a uniform vector of floats 3 x 3 */
>   	GLint ccmUniformDataIn_;
> 
> --
> 2.55.0
>
Bryan O'Donoghue July 20, 2026, 11:07 a.m. UTC | #2
On 16/07/2026 23:25, Milan Zamazal wrote:
> @@ -483,6 +501,12 @@ void DebayerEGL::setShaderVariableValues(const DebayerParams &params)
>   	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_TRUE, params.combinedMatrix.data().data());
>   	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
> 
> +	if (lscEnabled_) {
> +		egl_.createTexture2D(*eglImageLscLookup_, GL_RGB16F, GL_FLOAT,
> +				     params.lscLut.data(), GL_LINEAR);
> +		glUniform1i(textureUniformLsc_, eglImageLscLookup_->texture_unit_uniform_id_);
> +	}
> +

Create texture should happen in configure() and we should use 
egl_.updateTexture2D - if and only if the LSC algorithm has called for 
new data - else it is sufficient to create and populate the texture and 
then reuse the texture_id on subsequent passes.

I guess ? LSC doesn't need to be updated for each frame - even if it 
does updateTexture2D should be used - as its role in life is to update a 
GPU texture we already have instead of nuking and replacing.

---
bod
Milan Zamazal July 20, 2026, 1:35 p.m. UTC | #3
Bryan O'Donoghue <bod.linux@nxsw.ie> writes:

> On 16/07/2026 23:25, Milan Zamazal wrote:
>> From: Xander Pronk <xander.c.pronk@gmail.com>
>> 
>
>> Add support for passing the LSC table from debayerParams to the shaders.
>> 
>> The LSC table values are floats, we must add `type' parameter to
>> createTexture2D to support this.  Moreover, we use 16-bit internal
>> format for the values, primarily because this works for me, unlike
>> 32-bit floats.
>> 
>> When LSC is disabled by the runtime control, we provide a constant 1.0
>> correction table, i.e. a no-op table.  This is preferred to having a
>> runtime flag, to not disturb GPU computation with runtime switches,
>> assuming that disabling LSC in runtime is not a common use case.
>> 
>> Co-developed-by: Rick ten Wolde <rick_libcamera@wolde.info>
>> Signed-off-by: Rick ten Wolde <rick_libcamera@wolde.info>
>> Signed-off-by: Xander Pronk <xander.c.pronk@gmail.com>
>> Signed-off-by: Milan Zamazal <mzamazal@redhat.com>
>> ---
>>   include/libcamera/internal/egl.h              |  6 +++-
>>   .../internal/software_isp/debayer_params.h    |  2 ++
>>   src/ipa/simple/algorithms/lsc.cpp             | 11 ++++---
>>   src/libcamera/egl.cpp                         | 10 +++++--
>>   src/libcamera/software_isp/debayer.cpp        |  5 ++++
>>   src/libcamera/software_isp/debayer_egl.cpp    | 29 +++++++++++++++++--
>>   src/libcamera/software_isp/debayer_egl.h      |  6 ++++
>>   7 files changed, 60 insertions(+), 9 deletions(-)
>> 
>> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
>> index 030c813a5..37da2b18a 100644
>> --- a/include/libcamera/internal/egl.h
>> +++ b/include/libcamera/internal/egl.h
>> @@ -107,7 +107,11 @@ public:
>> 
>>   	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
>>   	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
>> -	void createTexture2D(eGLImage &eglImage, void *data, GLint filter);
>> +	void createTexture2D(eGLImage &eglImage,
>> +			     GLint internalFormat,
>> +			     GLenum type,
>> +			     const void *data,
>> +			     GLint filter);
>> 
>>   	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
>>   	void makeCurrent();
>> diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
>> index e3e01bc04..76825e6c8 100644
>> --- a/include/libcamera/internal/software_isp/debayer_params.h
>> +++ b/include/libcamera/internal/software_isp/debayer_params.h
>> @@ -29,6 +29,8 @@ struct DebayerParams {
>> 
>>   	static constexpr unsigned int kLscGridSize = 16;
>>   	static constexpr unsigned int kLscValuesPerCell = 3;
>> +	static constexpr unsigned int kLscBytesPerCell =
>> +		kLscValuesPerCell * sizeof(float);
>>   	using LscLookupTable =
>>   		std::array<float, kLscGridSize * kLscGridSize * kLscValuesPerCell>;
>>   	LscLookupTable lscLut{};
>> diff --git a/src/ipa/simple/algorithms/lsc.cpp b/src/ipa/simple/algorithms/lsc.cpp
>> index c531937c7..51c5b63d5 100644
>> --- a/src/ipa/simple/algorithms/lsc.cpp
>> +++ b/src/ipa/simple/algorithms/lsc.cpp
>> @@ -46,13 +46,16 @@ void Lsc::prepare([[maybe_unused]] IPAContext &context,
>>   		  IPAFrameContext &frameContext,
>>   		  DebayerParams *params)
>>   {
>> -	params->lscEnabled = frameContext.lsc.enabled;
>> -
>>   	unsigned int ct = frameContext.awb.colourTemperature;
>>   	constexpr unsigned int minTemperatureChange = 100;
>> 
>> -	if (!frameContext.lsc.enabled ||
>> -	    utils::abs_diff(ct, lastAppliedCt_) < minTemperatureChange)
>> +	if (!frameContext.lsc.enabled) {
>> +		params->lscLut.fill(1.0);
>> +		lastAppliedCt_ = 0;
>> +		return;
>> +	}
>> +
>> +	if (utils::abs_diff(ct, lastAppliedCt_) < minTemperatureChange)
>>   		return;
>> 
>>   	const lsc::Components<uint8_t> &set = lscAlgo_.interpolateComponents(ct);
>> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
>> index aeac5d313..c2a158ce7 100644
>> --- a/src/libcamera/egl.cpp
>> +++ b/src/libcamera/egl.cpp
>> @@ -241,6 +241,8 @@ int eGL::createOutputDMABufTexture2D(eGLImage &eglImage, int fd)
>>   /**
>>    * \brief Create a 2D texture from a memory buffer
>>    * \param[in,out] eglImage EGL image to associate with the texture
>> + * \param[in] internalFormat OpenGL internal storage format (e.g., GL_RGB8, GL_RGBA8)
>> + * \param[in] type OpenGL pixel data type (e.g., GL_UNSIGNED_BYTE, GL_FLOAT)
>>    * \param[in] data Pointer to pixel data, or nullptr for uninitialised texture
>>    * \param[in] filter GL texture filter setting
>>    *
>> @@ -249,7 +251,11 @@ int eGL::createOutputDMABufTexture2D(eGLImage &eglImage, int fd)
>>    * is useful for uploading static data like lookup tables or uniform color
>>    * matrices to the GPU.
>>    */
>> -void eGL::createTexture2D(eGLImage &eglImage, void *data, GLint filter)
>> +void eGL::createTexture2D(eGLImage &eglImage,
>> +			  GLint internalFormat,
>> +			  GLenum type,
>> +			  const void *data,
>> +			  GLint filter)
>>   {
>>   	ASSERT(tid_ == Thread::currentId());
>> 
>> @@ -257,7 +263,7 @@ void eGL::createTexture2D(eGLImage &eglImage, void *data, GLint filter)
>>   	glBindTexture(GL_TEXTURE_2D, eglImage.texture_);
>> 
>>   	// Generate texture, bind, associate image to texture, configure, unbind
>> -	glTexImage2D(GL_TEXTURE_2D, 0, eglImage.format_, eglImage.width_, eglImage.height_, 0, eglImage.format_, GL_UNSIGNED_BYTE, data);
>> +	glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, eglImage.width_, eglImage.height_, 0, eglImage.format_, type, data);
>> 
>>   	// Nearest filtering
>>   	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
>> diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
>> index 5ae9f032f..70c0e3336 100644
>> --- a/src/libcamera/software_isp/debayer.cpp
>> +++ b/src/libcamera/software_isp/debayer.cpp
>> @@ -53,6 +53,11 @@ namespace libcamera {
>>    * \brief Number of pixel values per each of the lens shading grid areas
>>    */
>> 
>> +/**
>> + * \var DebayerParams::kLscBytesPerCell
>> + * \brief Number of bytes per each of the lens shading grid areas
>> + */
>> +
>>   /**
>>    * \typedef DebayerParams::LscLookupTable
>>    * \brief Lookup table for lens shading correction
>> diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
>> index 562e9fc7a..0fdf0e6e8 100644
>> --- a/src/libcamera/software_isp/debayer_egl.cpp
>> +++ b/src/libcamera/software_isp/debayer_egl.cpp
>> @@ -128,6 +128,8 @@ int DebayerEGL::getShaderVariableLocations(void)
>>   	textureUniformBayerFirstRed_ = glGetUniformLocation(programId_, "tex_bayer_first_red");
>>   	textureUniformProjMatrix_ = glGetUniformLocation(programId_, "proj_matrix");
>> 
>> +	textureUniformLsc_ = glGetUniformLocation(programId_, "lsc_tex");
>> +
>>   	LOG(Debayer, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
>>   			    << " tex_y " << textureUniformBayerDataIn_
>>   			    << " awb " << awbUniformDataIn_
>> @@ -139,7 +141,8 @@ int DebayerEGL::getShaderVariableLocations(void)
>>   			    << " tex_size " << textureUniformSize_
>>   			    << " stride_factor " << textureUniformStrideFactor_
>>   			    << " tex_bayer_first_red " << textureUniformBayerFirstRed_
>> -			    << " proj_matrix " << textureUniformProjMatrix_;
>> +			    << " proj_matrix " << textureUniformProjMatrix_
>> +			    << " tex_lsc " << textureUniformLsc_;
>>   	return 0;
>>   }
>> 
>> @@ -156,6 +159,9 @@ int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
>>   	/* Specify GL_OES_EGL_image_external */
>>   	egl_.pushEnv(shaderEnv, "#extension GL_OES_EGL_image_external: enable");
>> 
>> +	if (lscEnabled_)
>> +		egl_.pushEnv(shaderEnv, "#define APPLY_LSC");
>> +
>>   	/*
>>   	 * Tell shaders how to re-order output taking account of how the pixels
>>   	 * are actually stored by EGL.
>> @@ -348,6 +354,18 @@ int DebayerEGL::configure(const StreamConfiguration &inputCfg,
>>   	 */
>>   	stats_->setWindow(Rectangle(window_.size()));
>> 
>> +	if (lscEnabled_) {
>> +		constexpr unsigned int gridSize = DebayerParams::kLscGridSize;
>> +		const unsigned int stride = gridSize * DebayerParams::kLscBytesPerCell;
>> +		eglImageLscLookup_ =
>> +			std::make_unique<eGLImage>(GL_RGB,
>> +						   gridSize,
>> +						   gridSize,
>> +						   stride,
>> +						   GL_TEXTURE2,
>> +						   2);
>> +	}
>> +
>>   	return 0;
>>   }
>> 
>> @@ -483,6 +501,12 @@ void DebayerEGL::setShaderVariableValues(const DebayerParams &params)
>>   	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_TRUE, params.combinedMatrix.data().data());
>>   	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
>> 
>> +	if (lscEnabled_) {
>> +		egl_.createTexture2D(*eglImageLscLookup_, GL_RGB16F, GL_FLOAT,
>> +				     params.lscLut.data(), GL_LINEAR);
>
> As we are discussing in the multi-pass thread, GL_RGB16F is dependent on 
> the optional GL_EXT_color_buffer_half_float
>
> https://registry.khronos.org/OpenGL/extensions/EXT/EXT_color_buffer_half_float.txt
>
> I think it should be pretty easy to disjunct on that as a flag in the 
> base class - a flag I plan to add for SSBO stats but also perfectly 
> happy for you to add.
>
> Half float linear filtering requires OES_texture_half_float_linear
>
> glGetString(GL_EXTENSIONS) | grep OES_texture_half_float_linear | 
> debayer_egl{ bool _has_OES_texture_half_float_linear;} or whatever
>
> if (_has_OES_texture_half_float_linear)
> 	GL_RGB16F;
>
> Could we repreent the LSC data as uint8 natively ? I mean without 
> storing it all as float and then converting to uint8 on each LSC 
> application cycle ?

We could.  `float' would be a nice and clear interface.  But I'd prefer
not to deal with conditional support, especially if we had to handle the
unsupported case some way.  Since we opt for UQ<2,6> with libipa in the
algorithm part, we can pass that to debayering.  It may become a
limitation in the future, but for now, why not, it's not that difficult
to change once/if needed.

>> +		glUniform1i(textureUniformLsc_, eglImageLscLookup_->texture_unit_uniform_id_);
>> +	}
>> +
>>   	/*
>>   	 * 0 = Red, 1 = Green, 2 = Blue
>>   	 */
>> @@ -530,7 +554,8 @@ int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const Debaye
>>   			LOG(Debayer, Error) << "mmap-ing buffer(s) failed";
>>   			return -ENODEV;
>>   		}
>> -		egl_.createTexture2D(*eglImageBayerIn_, inMapped->value().planes()[0].data(), GL_NEAREST);
>> +		egl_.createTexture2D(*eglImageBayerIn_, glFormat_, GL_UNSIGNED_BYTE,
>> +				     inMapped->value().planes()[0].data(), GL_NEAREST);
>>   	}
>> 
>>   	/* Generate the output render framebuffer as render to texture */
>> diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
>> index 82ed1305d..f3913d564 100644
>> --- a/src/libcamera/software_isp/debayer_egl.h
>> +++ b/src/libcamera/software_isp/debayer_egl.h
>> @@ -78,7 +78,10 @@ private:
>>   	std::unique_ptr<eGLImage> eglImageBayerIn_;
>>   	std::unique_ptr<eGLImage> eglImageBayerOut_;
>> 
>> +	/* LSC lookup table */
>> +	std::unique_ptr<eGLImage> eglImageLscLookup_;
>>   	bool lscEnabled_;
>> +
>>   	/* Shader parameters */
>>   	float firstRed_x_;
>>   	float firstRed_y_;
>> @@ -95,6 +98,9 @@ private:
>>   	/* Per-frame AWB gains */
>>   	GLint awbUniformDataIn_;
>> 
>> +	/* Lens shading correction */
>> +	GLint textureUniformLsc_;
>> +
>>   	/* Represent per-frame CCM as a uniform vector of floats 3 x 3 */
>>   	GLint ccmUniformDataIn_;
>> 
>> --
>> 2.55.0
>>
Milan Zamazal July 20, 2026, 1:38 p.m. UTC | #4
Bryan O'Donoghue <bod.linux@nxsw.ie> writes:

> On 16/07/2026 23:25, Milan Zamazal wrote:
>> @@ -483,6 +501,12 @@ void DebayerEGL::setShaderVariableValues(const DebayerParams &params)
>>   	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_TRUE, params.combinedMatrix.data().data());
>>   	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
>> 
>> +	if (lscEnabled_) {
>> +		egl_.createTexture2D(*eglImageLscLookup_, GL_RGB16F, GL_FLOAT,
>> +				     params.lscLut.data(), GL_LINEAR);
>> +		glUniform1i(textureUniformLsc_, eglImageLscLookup_->texture_unit_uniform_id_);
>> +	}
>> +
>
> Create texture should happen in configure() and we should use 
> egl_.updateTexture2D - if and only if the LSC algorithm has called for 
> new data - else it is sufficient to create and populate the texture and 
> then reuse the texture_id on subsequent passes.
>
> I guess ? LSC doesn't need to be updated for each frame - even if it 
> does updateTexture2D should be used - as its role in life is to update a 
> GPU texture we already have instead of nuking and replacing.

OK, we have it now so let's use it.  I'll do something about it in v10.
Bryan O'Donoghue July 20, 2026, 7:47 p.m. UTC | #5
On 20/07/2026 14:35, Milan Zamazal wrote:
>>> @@ -483,6 +501,12 @@ void DebayerEGL::setShaderVariableValues(const DebayerParams &params)
>>>    	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_TRUE, params.combinedMatrix.data().data());
>>>    	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
>>>
>>> +	if (lscEnabled_) {
>>> +		egl_.createTexture2D(*eglImageLscLookup_, GL_RGB16F, GL_FLOAT,
>>> +				     params.lscLut.data(), GL_LINEAR);
>> As we are discussing in the multi-pass thread, GL_RGB16F is dependent on
>> the optional GL_EXT_color_buffer_half_float
>>
>> https://registry.khronos.org/OpenGL/extensions/EXT/ 
>> EXT_color_buffer_half_float.txt
>>
>> I think it should be pretty easy to disjunct on that as a flag in the
>> base class - a flag I plan to add for SSBO stats but also perfectly
>> happy for you to add.
>>
>> Half float linear filtering requires OES_texture_half_float_linear
>>
>> glGetString(GL_EXTENSIONS) | grep OES_texture_half_float_linear |
>> debayer_egl{ bool _has_OES_texture_half_float_linear;} or whatever
>>
>> if (_has_OES_texture_half_float_linear)
>> 	GL_RGB16F;
>>
>> Could we repreent the LSC data as uint8 natively ? I mean without
>> storing it all as float and then converting to uint8 on each LSC
>> application cycle ?
> We could.  `float' would be a nice and clear interface.  But I'd prefer
> not to deal with conditional support, especially if we had to handle the
> unsupported case some way.  Since we opt for UQ<2,6> with libipa in the
> algorithm part, we can pass that to debayering.  It may become a
> limitation in the future, but for now, why not, it's not that difficult
> to change once/if needed.

Right so that would fit into eight bits as a regular 
GL_RGB/GL_UNSIGNED_BYTE.

So long as you do the normalisation in the shader, that should work.

---
bod

Patch
diff mbox series

diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
index 030c813a5..37da2b18a 100644
--- a/include/libcamera/internal/egl.h
+++ b/include/libcamera/internal/egl.h
@@ -107,7 +107,11 @@  public:
 
 	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
 	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
-	void createTexture2D(eGLImage &eglImage, void *data, GLint filter);
+	void createTexture2D(eGLImage &eglImage,
+			     GLint internalFormat,
+			     GLenum type,
+			     const void *data,
+			     GLint filter);
 
 	void pushEnv(std::vector<std::string> &shaderEnv, const char *str);
 	void makeCurrent();
diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
index e3e01bc04..76825e6c8 100644
--- a/include/libcamera/internal/software_isp/debayer_params.h
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -29,6 +29,8 @@  struct DebayerParams {
 
 	static constexpr unsigned int kLscGridSize = 16;
 	static constexpr unsigned int kLscValuesPerCell = 3;
+	static constexpr unsigned int kLscBytesPerCell =
+		kLscValuesPerCell * sizeof(float);
 	using LscLookupTable =
 		std::array<float, kLscGridSize * kLscGridSize * kLscValuesPerCell>;
 	LscLookupTable lscLut{};
diff --git a/src/ipa/simple/algorithms/lsc.cpp b/src/ipa/simple/algorithms/lsc.cpp
index c531937c7..51c5b63d5 100644
--- a/src/ipa/simple/algorithms/lsc.cpp
+++ b/src/ipa/simple/algorithms/lsc.cpp
@@ -46,13 +46,16 @@  void Lsc::prepare([[maybe_unused]] IPAContext &context,
 		  IPAFrameContext &frameContext,
 		  DebayerParams *params)
 {
-	params->lscEnabled = frameContext.lsc.enabled;
-
 	unsigned int ct = frameContext.awb.colourTemperature;
 	constexpr unsigned int minTemperatureChange = 100;
 
-	if (!frameContext.lsc.enabled ||
-	    utils::abs_diff(ct, lastAppliedCt_) < minTemperatureChange)
+	if (!frameContext.lsc.enabled) {
+		params->lscLut.fill(1.0);
+		lastAppliedCt_ = 0;
+		return;
+	}
+
+	if (utils::abs_diff(ct, lastAppliedCt_) < minTemperatureChange)
 		return;
 
 	const lsc::Components<uint8_t> &set = lscAlgo_.interpolateComponents(ct);
diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
index aeac5d313..c2a158ce7 100644
--- a/src/libcamera/egl.cpp
+++ b/src/libcamera/egl.cpp
@@ -241,6 +241,8 @@  int eGL::createOutputDMABufTexture2D(eGLImage &eglImage, int fd)
 /**
  * \brief Create a 2D texture from a memory buffer
  * \param[in,out] eglImage EGL image to associate with the texture
+ * \param[in] internalFormat OpenGL internal storage format (e.g., GL_RGB8, GL_RGBA8)
+ * \param[in] type OpenGL pixel data type (e.g., GL_UNSIGNED_BYTE, GL_FLOAT)
  * \param[in] data Pointer to pixel data, or nullptr for uninitialised texture
  * \param[in] filter GL texture filter setting
  *
@@ -249,7 +251,11 @@  int eGL::createOutputDMABufTexture2D(eGLImage &eglImage, int fd)
  * is useful for uploading static data like lookup tables or uniform color
  * matrices to the GPU.
  */
-void eGL::createTexture2D(eGLImage &eglImage, void *data, GLint filter)
+void eGL::createTexture2D(eGLImage &eglImage,
+			  GLint internalFormat,
+			  GLenum type,
+			  const void *data,
+			  GLint filter)
 {
 	ASSERT(tid_ == Thread::currentId());
 
@@ -257,7 +263,7 @@  void eGL::createTexture2D(eGLImage &eglImage, void *data, GLint filter)
 	glBindTexture(GL_TEXTURE_2D, eglImage.texture_);
 
 	// Generate texture, bind, associate image to texture, configure, unbind
-	glTexImage2D(GL_TEXTURE_2D, 0, eglImage.format_, eglImage.width_, eglImage.height_, 0, eglImage.format_, GL_UNSIGNED_BYTE, data);
+	glTexImage2D(GL_TEXTURE_2D, 0, internalFormat, eglImage.width_, eglImage.height_, 0, eglImage.format_, type, data);
 
 	// Nearest filtering
 	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, filter);
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
index 5ae9f032f..70c0e3336 100644
--- a/src/libcamera/software_isp/debayer.cpp
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -53,6 +53,11 @@  namespace libcamera {
  * \brief Number of pixel values per each of the lens shading grid areas
  */
 
+/**
+ * \var DebayerParams::kLscBytesPerCell
+ * \brief Number of bytes per each of the lens shading grid areas
+ */
+
 /**
  * \typedef DebayerParams::LscLookupTable
  * \brief Lookup table for lens shading correction
diff --git a/src/libcamera/software_isp/debayer_egl.cpp b/src/libcamera/software_isp/debayer_egl.cpp
index 562e9fc7a..0fdf0e6e8 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -128,6 +128,8 @@  int DebayerEGL::getShaderVariableLocations(void)
 	textureUniformBayerFirstRed_ = glGetUniformLocation(programId_, "tex_bayer_first_red");
 	textureUniformProjMatrix_ = glGetUniformLocation(programId_, "proj_matrix");
 
+	textureUniformLsc_ = glGetUniformLocation(programId_, "lsc_tex");
+
 	LOG(Debayer, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
 			    << " tex_y " << textureUniformBayerDataIn_
 			    << " awb " << awbUniformDataIn_
@@ -139,7 +141,8 @@  int DebayerEGL::getShaderVariableLocations(void)
 			    << " tex_size " << textureUniformSize_
 			    << " stride_factor " << textureUniformStrideFactor_
 			    << " tex_bayer_first_red " << textureUniformBayerFirstRed_
-			    << " proj_matrix " << textureUniformProjMatrix_;
+			    << " proj_matrix " << textureUniformProjMatrix_
+			    << " tex_lsc " << textureUniformLsc_;
 	return 0;
 }
 
@@ -156,6 +159,9 @@  int DebayerEGL::initBayerShaders(PixelFormat inputFormat, PixelFormat outputForm
 	/* Specify GL_OES_EGL_image_external */
 	egl_.pushEnv(shaderEnv, "#extension GL_OES_EGL_image_external: enable");
 
+	if (lscEnabled_)
+		egl_.pushEnv(shaderEnv, "#define APPLY_LSC");
+
 	/*
 	 * Tell shaders how to re-order output taking account of how the pixels
 	 * are actually stored by EGL.
@@ -348,6 +354,18 @@  int DebayerEGL::configure(const StreamConfiguration &inputCfg,
 	 */
 	stats_->setWindow(Rectangle(window_.size()));
 
+	if (lscEnabled_) {
+		constexpr unsigned int gridSize = DebayerParams::kLscGridSize;
+		const unsigned int stride = gridSize * DebayerParams::kLscBytesPerCell;
+		eglImageLscLookup_ =
+			std::make_unique<eGLImage>(GL_RGB,
+						   gridSize,
+						   gridSize,
+						   stride,
+						   GL_TEXTURE2,
+						   2);
+	}
+
 	return 0;
 }
 
@@ -483,6 +501,12 @@  void DebayerEGL::setShaderVariableValues(const DebayerParams &params)
 	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_TRUE, params.combinedMatrix.data().data());
 	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
 
+	if (lscEnabled_) {
+		egl_.createTexture2D(*eglImageLscLookup_, GL_RGB16F, GL_FLOAT,
+				     params.lscLut.data(), GL_LINEAR);
+		glUniform1i(textureUniformLsc_, eglImageLscLookup_->texture_unit_uniform_id_);
+	}
+
 	/*
 	 * 0 = Red, 1 = Green, 2 = Blue
 	 */
@@ -530,7 +554,8 @@  int DebayerEGL::debayerGPU(FrameBuffer *input, FrameBuffer *output, const Debaye
 			LOG(Debayer, Error) << "mmap-ing buffer(s) failed";
 			return -ENODEV;
 		}
-		egl_.createTexture2D(*eglImageBayerIn_, inMapped->value().planes()[0].data(), GL_NEAREST);
+		egl_.createTexture2D(*eglImageBayerIn_, glFormat_, GL_UNSIGNED_BYTE,
+				     inMapped->value().planes()[0].data(), GL_NEAREST);
 	}
 
 	/* Generate the output render framebuffer as render to texture */
diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
index 82ed1305d..f3913d564 100644
--- a/src/libcamera/software_isp/debayer_egl.h
+++ b/src/libcamera/software_isp/debayer_egl.h
@@ -78,7 +78,10 @@  private:
 	std::unique_ptr<eGLImage> eglImageBayerIn_;
 	std::unique_ptr<eGLImage> eglImageBayerOut_;
 
+	/* LSC lookup table */
+	std::unique_ptr<eGLImage> eglImageLscLookup_;
 	bool lscEnabled_;
+
 	/* Shader parameters */
 	float firstRed_x_;
 	float firstRed_y_;
@@ -95,6 +98,9 @@  private:
 	/* Per-frame AWB gains */
 	GLint awbUniformDataIn_;
 
+	/* Lens shading correction */
+	GLint textureUniformLsc_;
+
 	/* Represent per-frame CCM as a uniform vector of floats 3 x 3 */
 	GLint ccmUniformDataIn_;