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

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

Commit Message

Milan Zamazal Aug. 5, 2026, 6:35 p.m. UTC
From: Xander Pronk <xander.c.pronk@gmail.com>

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

We use 8-bit texture with the original UQ<2, 6> values to avoid trouble
with floats.  32-bit floats don't work in my environment and 16-bit
floats may not be supported in all OpenGL environments.

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.

The LSC texture is created once and then updated.  The texture creation
cannot be done in `configure', because of a different thread.

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              |  8 +++-
 .../internal/software_isp/debayer_params.h    |  2 +
 src/libcamera/egl.cpp                         | 14 +++++--
 src/libcamera/software_isp/debayer.cpp        |  5 +++
 src/libcamera/software_isp/debayer_egl.cpp    | 42 ++++++++++++++++++-
 src/libcamera/software_isp/debayer_egl.h      |  8 ++++
 6 files changed, 71 insertions(+), 8 deletions(-)

Comments

Barnabás Pőcze Aug. 6, 2026, 10:59 a.m. UTC | #1
2026. 08. 05. 20:35 keltezéssel, Milan Zamazal írta:
> From: Xander Pronk <xander.c.pronk@gmail.com>
> 
> Add support for passing the LSC table from debayerParams to the shaders.
> 
> We use 8-bit texture with the original UQ<2, 6> values to avoid trouble
> with floats.  32-bit floats don't work in my environment and 16-bit
> floats may not be supported in all OpenGL environments.
> 
> 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.
> 
> The LSC texture is created once and then updated.  The texture creation
> cannot be done in `configure', because of a different thread.
> 
> 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              |  8 +++-
>   .../internal/software_isp/debayer_params.h    |  2 +
>   src/libcamera/egl.cpp                         | 14 +++++--
>   src/libcamera/software_isp/debayer.cpp        |  5 +++
>   src/libcamera/software_isp/debayer_egl.cpp    | 42 ++++++++++++++++++-
>   src/libcamera/software_isp/debayer_egl.h      |  8 ++++
>   6 files changed, 71 insertions(+), 8 deletions(-)
> 
> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
> index 790ec4b22..b80d65877 100644
> --- a/include/libcamera/internal/egl.h
> +++ b/include/libcamera/internal/egl.h
> @@ -107,8 +107,12 @@ public:
>   
>   	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
>   	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
> -	void createTexture2D(eGLImage &eglImage, void *data, GLint filter);
> -	void updateTexture2D(eGLImage &eglImage, void *data);
> +	void createTexture2D(eGLImage &eglImage,
> +			     GLint internalFormat,
> +			     GLenum type,
> +			     const void *data,
> +			     GLint filter);
> +	void updateTexture2D(eGLImage &eglImage, const void *data);
>   	void createOutputTexture2D(eGLImage &eglImage);
>   
>   	int attachTextureToFBO(eGLImage &eglImage);
> diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
> index 93dcf42e6..ae51c3271 100644
> --- a/include/libcamera/internal/software_isp/debayer_params.h
> +++ b/include/libcamera/internal/software_isp/debayer_params.h
> @@ -33,6 +33,8 @@ struct DebayerParams {
>   	 **/
>   	static constexpr unsigned int kLscGridSize = 17;
>   	static constexpr unsigned int kLscValuesPerCell = 4;
> +	static constexpr unsigned int kLscBytesPerCell =
> +		kLscValuesPerCell * sizeof(uint8_t);
>   	using LscLookupTable =
>   		std::array<uint8_t, kLscGridSize * kLscGridSize * kLscValuesPerCell>;
>   	LscLookupTable lscLut{};
> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
> index 32e7e0e9c..cbae3a664 100644
> --- a/src/libcamera/egl.cpp
> +++ b/src/libcamera/egl.cpp
> @@ -277,6 +277,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
>    *
> @@ -285,14 +287,18 @@ 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());
>   
>   	activateBindTexture(eglImage);
>   
>   	// 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);
> @@ -338,7 +344,7 @@ EGLDisplay eGL::probeDisplay()
>    *
>    * Updates a 2D texture in VRAM.
>    */
> -void eGL::updateTexture2D(eGLImage &eglImage, void *data)
> +void eGL::updateTexture2D(eGLImage &eglImage, const void *data)

I feel like it might be nice to separate the `eGL` class changes.


>   {
>   	ASSERT(tid_ == Thread::currentId());
>   
> @@ -357,7 +363,7 @@ void eGL::updateTexture2D(eGLImage &eglImage, void *data)
>    */
>   void eGL::createOutputTexture2D(eGLImage &eglImage)
>   {
> -	createTexture2D(eglImage, NULL, GL_NEAREST);
> +	createTexture2D(eglImage, eglImage.format_, GL_UNSIGNED_BYTE, NULL, GL_NEAREST);
>   	attachTextureToFBO(eglImage);
>   }
>   
> [...]
> diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
> index a64dc925a..ac0de23a0 100644
> --- a/src/libcamera/software_isp/debayer_egl.h
> +++ b/src/libcamera/software_isp/debayer_egl.h
> @@ -83,7 +83,12 @@ private:
>   	unsigned int inputBufferCount_;
>   	unsigned int outputBufferCount_;
>   
> +	/* LSC lookup table */
> +	std::unique_ptr<eGLImage> eglImageLscLookup_;
>   	bool lscEnabled_;
> +	bool lscTextureCreated_ = false;

I believe the unique_ptr/optional could be used instead of an extra bool
if you move the construction into `setShaderVariableValues()`?


> +	uint64_t lscLutVersion_ = 0;

Shouldn't this be reset in `start()` / `stop()`? And should it possibly be an `std::optional`?
Especially given that `DebayerParams::lscLutVersion` does not seem to be reset anywhere?


> +
>   	/* Shader parameters */
>   	float firstRed_x_;
>   	float firstRed_y_;
> @@ -100,6 +105,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_;
>
Milan Zamazal Aug. 6, 2026, 4:02 p.m. UTC | #2
Hi Barnabás,

thank you for review.

Barnabás Pőcze <barnabas.pocze@ideasonboard.com> writes:

> 2026. 08. 05. 20:35 keltezéssel, Milan Zamazal írta:
>> From: Xander Pronk <xander.c.pronk@gmail.com>
>> Add support for passing the LSC table from debayerParams to the shaders.
>> We use 8-bit texture with the original UQ<2, 6> values to avoid trouble
>> with floats.  32-bit floats don't work in my environment and 16-bit
>> floats may not be supported in all OpenGL environments.
>> 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.
>> The LSC texture is created once and then updated.  The texture creation
>> cannot be done in `configure', because of a different thread.
>> 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              |  8 +++-
>>   .../internal/software_isp/debayer_params.h    |  2 +
>>   src/libcamera/egl.cpp                         | 14 +++++--
>>   src/libcamera/software_isp/debayer.cpp        |  5 +++
>>   src/libcamera/software_isp/debayer_egl.cpp    | 42 ++++++++++++++++++-
>>   src/libcamera/software_isp/debayer_egl.h      |  8 ++++
>>   6 files changed, 71 insertions(+), 8 deletions(-)
>> diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
>> index 790ec4b22..b80d65877 100644
>> --- a/include/libcamera/internal/egl.h
>> +++ b/include/libcamera/internal/egl.h
>> @@ -107,8 +107,12 @@ public:
>>     	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
>>   	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
>> -	void createTexture2D(eGLImage &eglImage, void *data, GLint filter);
>> -	void updateTexture2D(eGLImage &eglImage, void *data);
>> +	void createTexture2D(eGLImage &eglImage,
>> +			     GLint internalFormat,
>> +			     GLenum type,
>> +			     const void *data,
>> +			     GLint filter);
>> +	void updateTexture2D(eGLImage &eglImage, const void *data);
>>   	void createOutputTexture2D(eGLImage &eglImage);
>>     	int attachTextureToFBO(eGLImage &eglImage);
>> diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
>> index 93dcf42e6..ae51c3271 100644
>> --- a/include/libcamera/internal/software_isp/debayer_params.h
>> +++ b/include/libcamera/internal/software_isp/debayer_params.h
>> @@ -33,6 +33,8 @@ struct DebayerParams {
>>   	 **/
>>   	static constexpr unsigned int kLscGridSize = 17;
>>   	static constexpr unsigned int kLscValuesPerCell = 4;
>> +	static constexpr unsigned int kLscBytesPerCell =
>> +		kLscValuesPerCell * sizeof(uint8_t);
>>   	using LscLookupTable =
>>   		std::array<uint8_t, kLscGridSize * kLscGridSize * kLscValuesPerCell>;
>>   	LscLookupTable lscLut{};
>> diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
>> index 32e7e0e9c..cbae3a664 100644
>> --- a/src/libcamera/egl.cpp
>> +++ b/src/libcamera/egl.cpp
>> @@ -277,6 +277,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
>>    *
>> @@ -285,14 +287,18 @@ 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());
>>     	activateBindTexture(eglImage);
>>     	// 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);
>> @@ -338,7 +344,7 @@ EGLDisplay eGL::probeDisplay()
>>    *
>>    * Updates a 2D texture in VRAM.
>>    */
>> -void eGL::updateTexture2D(eGLImage &eglImage, void *data)
>> +void eGL::updateTexture2D(eGLImage &eglImage, const void *data)
>
> I feel like it might be nice to separate the `eGL` class changes.

I can put them to a separate commit if it helps.

>>   {
>>   	ASSERT(tid_ == Thread::currentId());
>>   @@ -357,7 +363,7 @@ void eGL::updateTexture2D(eGLImage &eglImage, void *data)
>>    */
>>   void eGL::createOutputTexture2D(eGLImage &eglImage)
>>   {
>> -	createTexture2D(eglImage, NULL, GL_NEAREST);
>> +	createTexture2D(eglImage, eglImage.format_, GL_UNSIGNED_BYTE, NULL, GL_NEAREST);
>>   	attachTextureToFBO(eglImage);
>>   }
>>   [...]
>> diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
>> index a64dc925a..ac0de23a0 100644
>> --- a/src/libcamera/software_isp/debayer_egl.h
>> +++ b/src/libcamera/software_isp/debayer_egl.h
>> @@ -83,7 +83,12 @@ private:
>>   	unsigned int inputBufferCount_;
>>   	unsigned int outputBufferCount_;
>>   +	/* LSC lookup table */
>> +	std::unique_ptr<eGLImage> eglImageLscLookup_;
>>   	bool lscEnabled_;
>> +	bool lscTextureCreated_ = false;
>
> I believe the unique_ptr/optional could be used instead of an extra bool
> if you move the construction into `setShaderVariableValues()`?

OK.

>> +	uint64_t lscLutVersion_ = 0;
>
> Shouldn't this be reset in `start()` / `stop()`? And should it possibly be an `std::optional`?

Good catch.  It should be reset in stop() and using std::optional would
be cleaner and safer.

> Especially given that `DebayerParams::lscLutVersion` does not seem to be reset anywhere?

It should be reset together with the other parameters, I'll add that.

>> +
>>   	/* Shader parameters */
>>   	float firstRed_x_;
>>   	float firstRed_y_;
>> @@ -100,6 +105,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_;
>>

Patch
diff mbox series

diff --git a/include/libcamera/internal/egl.h b/include/libcamera/internal/egl.h
index 790ec4b22..b80d65877 100644
--- a/include/libcamera/internal/egl.h
+++ b/include/libcamera/internal/egl.h
@@ -107,8 +107,12 @@  public:
 
 	int createInputDMABufTexture2D(eGLImage &eglImage, int fd);
 	int createOutputDMABufTexture2D(eGLImage &eglImage, int fd);
-	void createTexture2D(eGLImage &eglImage, void *data, GLint filter);
-	void updateTexture2D(eGLImage &eglImage, void *data);
+	void createTexture2D(eGLImage &eglImage,
+			     GLint internalFormat,
+			     GLenum type,
+			     const void *data,
+			     GLint filter);
+	void updateTexture2D(eGLImage &eglImage, const void *data);
 	void createOutputTexture2D(eGLImage &eglImage);
 
 	int attachTextureToFBO(eGLImage &eglImage);
diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
index 93dcf42e6..ae51c3271 100644
--- a/include/libcamera/internal/software_isp/debayer_params.h
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -33,6 +33,8 @@  struct DebayerParams {
 	 **/
 	static constexpr unsigned int kLscGridSize = 17;
 	static constexpr unsigned int kLscValuesPerCell = 4;
+	static constexpr unsigned int kLscBytesPerCell =
+		kLscValuesPerCell * sizeof(uint8_t);
 	using LscLookupTable =
 		std::array<uint8_t, kLscGridSize * kLscGridSize * kLscValuesPerCell>;
 	LscLookupTable lscLut{};
diff --git a/src/libcamera/egl.cpp b/src/libcamera/egl.cpp
index 32e7e0e9c..cbae3a664 100644
--- a/src/libcamera/egl.cpp
+++ b/src/libcamera/egl.cpp
@@ -277,6 +277,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
  *
@@ -285,14 +287,18 @@  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());
 
 	activateBindTexture(eglImage);
 
 	// 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);
@@ -338,7 +344,7 @@  EGLDisplay eGL::probeDisplay()
  *
  * Updates a 2D texture in VRAM.
  */
-void eGL::updateTexture2D(eGLImage &eglImage, void *data)
+void eGL::updateTexture2D(eGLImage &eglImage, const void *data)
 {
 	ASSERT(tid_ == Thread::currentId());
 
@@ -357,7 +363,7 @@  void eGL::updateTexture2D(eGLImage &eglImage, void *data)
  */
 void eGL::createOutputTexture2D(eGLImage &eglImage)
 {
-	createTexture2D(eglImage, NULL, GL_NEAREST);
+	createTexture2D(eglImage, eglImage.format_, GL_UNSIGNED_BYTE, NULL, GL_NEAREST);
 	attachTextureToFBO(eglImage);
 }
 
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
index 801c28fc9..44df03990 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 e03fef684..5562a7b76 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -115,6 +115,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_
@@ -126,7 +128,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;
 }
 
@@ -143,6 +146,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.
@@ -338,6 +344,18 @@  int DebayerEGL::configure(const StreamConfiguration &inputCfg,
 	inputBufferCount_ = inputCfg.bufferCount;
 	outputBufferCount_ = outputCfg.bufferCount;
 
+	if (lscEnabled_) {
+		constexpr unsigned int gridSize = DebayerParams::kLscGridSize;
+		const unsigned int stride = gridSize * DebayerParams::kLscBytesPerCell;
+		eglImageLscLookup_ =
+			std::make_unique<eGLImage>(GL_RGBA,
+						   gridSize,
+						   gridSize,
+						   stride,
+						   GL_TEXTURE2,
+						   2);
+	}
+
 	return 0;
 }
 
@@ -467,6 +485,21 @@  void DebayerEGL::setShaderVariableValues(eGLImage &eglImageIn, const DebayerPara
 	glUniformMatrix3fv(ccmUniformDataIn_, 1, GL_TRUE, params.combinedMatrix.data().data());
 	LOG(Debayer, Debug) << " ccmUniformDataIn_ " << ccmUniformDataIn_ << " data " << params.combinedMatrix;
 
+	if (lscEnabled_) {
+		glUniform1i(textureUniformLsc_, eglImageLscLookup_->texture_unit_uniform_id_);
+		if (params.lscLutVersion != lscLutVersion_) {
+			if (lscTextureCreated_) {
+				egl_.updateTexture2D(*eglImageLscLookup_, params.lscLut.data());
+			} else {
+				egl_.createTexture2D(*eglImageLscLookup_, eglImageLscLookup_->format_, GL_UNSIGNED_BYTE,
+						     params.lscLut.data(), GL_LINEAR);
+				lscTextureCreated_ = true;
+			}
+
+			lscLutVersion_ = params.lscLutVersion;
+		}
+	}
+
 	/*
 	 * 0 = Red, 1 = Green, 2 = Blue
 	 */
@@ -536,7 +569,11 @@  eGLImage *DebayerEGL::getCachedInputFrameBuffer(FrameBuffer *input, std::optiona
 		return nullptr;
 	}
 	if (cache_miss)
-		egl_.createTexture2D(*eglImageIn, inMapped->value().planes()[0].data(), GL_NEAREST);
+		egl_.createTexture2D(*eglImageIn,
+				     eglImageIn->format_,
+				     GL_UNSIGNED_BYTE,
+				     inMapped->value().planes()[0].data(),
+				     GL_NEAREST);
 	else
 		egl_.updateTexture2D(*eglImageIn, inMapped->value().planes()[0].data());
 
@@ -682,6 +719,7 @@  void DebayerEGL::stop()
 {
 	eglImageOutCache_.clear();
 	eglImageInCache_.clear();
+	lscTextureCreated_ = false;
 
 	if (programId_)
 		glDeleteProgram(programId_);
diff --git a/src/libcamera/software_isp/debayer_egl.h b/src/libcamera/software_isp/debayer_egl.h
index a64dc925a..ac0de23a0 100644
--- a/src/libcamera/software_isp/debayer_egl.h
+++ b/src/libcamera/software_isp/debayer_egl.h
@@ -83,7 +83,12 @@  private:
 	unsigned int inputBufferCount_;
 	unsigned int outputBufferCount_;
 
+	/* LSC lookup table */
+	std::unique_ptr<eGLImage> eglImageLscLookup_;
 	bool lscEnabled_;
+	bool lscTextureCreated_ = false;
+	uint64_t lscLutVersion_ = 0;
+
 	/* Shader parameters */
 	float firstRed_x_;
 	float firstRed_y_;
@@ -100,6 +105,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_;