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 4eb6c7e8d..0b96d4ffe 100644
--- a/include/libcamera/internal/software_isp/debayer_params.h
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -30,6 +30,8 @@ struct DebayerParams {
 	static constexpr unsigned int kLscGridSize = 16;
 	static constexpr unsigned int kLscValuesPerCell = 3;
 	using LscValueType = float;
+	static constexpr unsigned int kLscBytesPerCell =
+		kLscValuesPerCell * sizeof(LscValueType);
 	using LscLookupTable =
 		std::array<LscValueType, kLscGridSize * kLscGridSize * kLscValuesPerCell>;
 	LscLookupTable lscLut{};
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 5c4bd914b..2bf64e570 100644
--- a/src/libcamera/software_isp/debayer.cpp
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -58,6 +58,11 @@ namespace libcamera {
  * \brief Type of LSC grid values
  */
 
+/**
+ * \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..a73a00759 100644
--- a/src/libcamera/software_isp/debayer_egl.cpp
+++ b/src/libcamera/software_isp/debayer_egl.cpp
@@ -128,6 +128,9 @@ int DebayerEGL::getShaderVariableLocations(void)
 	textureUniformBayerFirstRed_ = glGetUniformLocation(programId_, "tex_bayer_first_red");
 	textureUniformProjMatrix_ = glGetUniformLocation(programId_, "proj_matrix");
 
+	textureUniformLsc_ = glGetUniformLocation(programId_, "lsc_tex");
+	lscEnabledUniform_ = glGetUniformLocation(programId_, "lsc_enabled");
+
 	LOG(Debayer, Debug) << "vertexIn " << attributeVertex_ << " textureIn " << attributeTexture_
 			    << " tex_y " << textureUniformBayerDataIn_
 			    << " awb " << awbUniformDataIn_
@@ -139,7 +142,9 @@ int DebayerEGL::getShaderVariableLocations(void)
 			    << " tex_size " << textureUniformSize_
 			    << " stride_factor " << textureUniformStrideFactor_
 			    << " tex_bayer_first_red " << textureUniformBayerFirstRed_
-			    << " proj_matrix " << textureUniformProjMatrix_;
+			    << " proj_matrix " << textureUniformProjMatrix_
+			    << " tex_lsc " << textureUniformLsc_
+			    << " lsc_enabled" << lscEnabledUniform_;
 	return 0;
 }
 
@@ -156,6 +161,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 +356,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 +503,13 @@ 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_);
+		glUniform1i(lscEnabledUniform_, params.lscEnabled);
+	}
+
 	/*
 	 * 0 = Red, 1 = Green, 2 = Blue
 	 */
@@ -530,7 +557,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..2af26fb74 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,10 @@ private:
 	/* Per-frame AWB gains */
 	GLint awbUniformDataIn_;
 
+	/* Lens shading correction */
+	GLint textureUniformLsc_;
+	GLboolean lscEnabledUniform_;
+
 	/* Represent per-frame CCM as a uniform vector of floats 3 x 3 */
 	GLint ccmUniformDataIn_;
 
