diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
index 93dcf42e6..2d6fde780 100644
--- a/include/libcamera/internal/software_isp/debayer_params.h
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -35,7 +35,12 @@ struct DebayerParams {
 	static constexpr unsigned int kLscValuesPerCell = 4;
 	using LscLookupTable =
 		std::array<uint8_t, kLscGridSize * kLscGridSize * kLscValuesPerCell>;
-	LscLookupTable lscLut{};
+	static constexpr auto identityLscLut = [] {
+		LscLookupTable lut = {};
+		lut.fill(64); /* == UQ<2, 6>(1.0f).quantized() */
+		return lut;
+	}();
+	LscLookupTable lscLut = identityLscLut;
 	uint64_t lscLutVersion = 0;
 };
 
diff --git a/src/ipa/simple/algorithms/lsc.cpp b/src/ipa/simple/algorithms/lsc.cpp
new file mode 100644
index 000000000..d515be3c2
--- /dev/null
+++ b/src/ipa/simple/algorithms/lsc.cpp
@@ -0,0 +1,96 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Lens shading correction
+ */
+
+#include "lsc.h"
+
+#include <libcamera/base/log.h>
+
+namespace libcamera {
+
+namespace ipa::soft::algorithms {
+
+LOG_DEFINE_CATEGORY(IPASoftLsc)
+
+int Lsc::init(IPAContext &context, const ValueNode &tuningData)
+{
+	static constexpr unsigned int kGridSize = DebayerParams::kLscGridSize;
+
+	for (unsigned int i = 0; i < kGridSize; i++)
+		gridPos_.push_back(static_cast<double>(i) / (kGridSize - 1));
+
+	return lscAlgo_.init(tuningData, context.ctrlMap,
+			     { .keys = { "r", "g", "b" },
+			       .numHSamples = kGridSize,
+			       .numVSamples = kGridSize,
+			       .sensorSize = context.sensorInfo.activeAreaSize });
+}
+
+int Lsc::configure(IPAContext &context,
+		   [[maybe_unused]] const IPAConfigInfo &configInfo)
+{
+	return lscAlgo_.configure(context.activeState.lsc,
+				  context.sensorInfo.analogCrop,
+				  gridPos_, gridPos_);
+}
+
+void Lsc::prepare([[maybe_unused]] IPAContext &context,
+		  [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext,
+		  DebayerParams *params)
+{
+	unsigned int ct = frameContext.awb.colourTemperature;
+	constexpr unsigned int minTemperatureChange = 100;
+
+	if (!frameContext.lsc.enabled) {
+		if (lastAppliedCt_ != 0) {
+			params->lscLut = DebayerParams::identityLscLut;
+			lastAppliedCt_ = 0;
+		}
+		return;
+	}
+
+	if (utils::abs_diff(ct, lastAppliedCt_) < minTemperatureChange)
+		return;
+
+	const auto &set = lscAlgo_.interpolateComponents(ct);
+
+	const auto &red = set.at("r");
+	const auto &green = set.at("g");
+	const auto &blue = set.at("b");
+
+	DebayerParams::LscLookupTable &lut = params->lscLut;
+	constexpr unsigned int gridSize = DebayerParams::kLscGridSize;
+	for (unsigned int i = 0, j = 0; i < gridSize * gridSize; i++) {
+		lut[j++] = red[i];
+		lut[j++] = green[i];
+		lut[j++] = blue[i];
+		lut[j++] = 0; /* padding */
+	}
+	params->lscLutVersion++;
+
+	lastAppliedCt_ = ct;
+}
+
+void Lsc::queueRequest(IPAContext &context, [[maybe_unused]] const uint32_t frame,
+		       IPAFrameContext &frameContext, const ControlList &controls)
+{
+	lscAlgo_.queueRequest(context.activeState.lsc, frameContext.lsc,
+			      controls);
+}
+
+void Lsc::process([[maybe_unused]] IPAContext &context,
+		  [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext,
+		  [[maybe_unused]] const SwIspStats *stats,
+		  ControlList &metadata)
+{
+	lscAlgo_.process(frameContext.lsc, metadata);
+}
+
+REGISTER_IPA_ALGORITHM(Lsc, "Lsc")
+
+} /* namespace ipa::soft::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/lsc.h b/src/ipa/simple/algorithms/lsc.h
new file mode 100644
index 000000000..13d9f2bca
--- /dev/null
+++ b/src/ipa/simple/algorithms/lsc.h
@@ -0,0 +1,55 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Lens shading correction
+ */
+
+#pragma once
+
+#include <libipa/interpolator.h>
+
+#include "libipa/fixedpoint.h"
+#include "libipa/lsc.h"
+
+#include "algorithm.h"
+#include "ipa_context.h"
+
+namespace libcamera {
+
+namespace ipa {
+
+namespace soft::algorithms {
+
+class Lsc : public Algorithm
+{
+public:
+	Lsc() = default;
+	~Lsc() = default;
+
+	int init(IPAContext &context, const ValueNode &tuningData) override;
+	int configure(IPAContext &context,
+		      const IPAConfigInfo &configInfo) override;
+	void queueRequest(IPAContext &context, [[maybe_unused]] const uint32_t frame,
+			  IPAFrameContext &frameContext, const ControlList &controls) override;
+	void prepare(IPAContext &context,
+		     const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     DebayerParams *params) override;
+	void process([[maybe_unused]] IPAContext &context,
+		     [[maybe_unused]] const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     [[maybe_unused]] const SwIspStats *stats,
+		     ControlList &metadata) override;
+
+private:
+	LscAlgorithm<UQ<2, 6>> lscAlgo_;
+
+	std::vector<double> gridPos_;
+
+	unsigned int lastAppliedCt_ = 0;
+};
+
+} /* namespace soft::algorithms */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/algorithms/meson.build b/src/ipa/simple/algorithms/meson.build
index 73c637220..c9f6e5590 100644
--- a/src/ipa/simple/algorithms/meson.build
+++ b/src/ipa/simple/algorithms/meson.build
@@ -6,4 +6,5 @@ soft_simple_ipa_algorithms = files([
     'agc.cpp',
     'blc.cpp',
     'ccm.cpp',
+    'lsc.cpp',
 ])
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index ff312ae8f..23c2cfd0a 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -19,6 +19,7 @@
 #include <libipa/awb.h>
 #include <libipa/ccm.h>
 #include <libipa/fc_queue.h>
+#include "libipa/lsc.h"
 
 #include "core_ipa_interface.h"
 
@@ -61,6 +62,8 @@ struct IPAActiveState {
 		std::optional<float> contrast;
 		std::optional<float> saturation;
 	} knobs;
+
+	ipa::lsc::ActiveState lsc;
 };
 
 struct IPAFrameContext : public FrameContext {
@@ -75,6 +78,7 @@ struct IPAFrameContext : public FrameContext {
 	float gamma;
 	std::optional<float> contrast;
 	std::optional<float> saturation;
+	ipa::lsc::FrameContext lsc;
 };
 
 struct IPAContext {
@@ -89,6 +93,7 @@ struct IPAContext {
 	FCQueue<IPAFrameContext> frameContexts;
 	ControlInfoMap::Map ctrlMap;
 	bool ccmEnabled = false;
+	ipa::lsc::ActiveState lsc;
 };
 
 } /* namespace ipa::soft */
diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
index 629e1a32d..f48ef76a5 100644
--- a/src/ipa/simple/soft_simple.cpp
+++ b/src/ipa/simple/soft_simple.cpp
@@ -163,6 +163,8 @@ int IPASoftSimple::init(const IPASettings &settings,
 		params_->gamma = 1.0 / algorithms::kDefaultGamma;
 		params_->contrastExp = 1.0;
 		params_->gains = { { 1.0, 1.0, 1.0 } };
+		params_->lscLutVersion = 0;
+		params_->lscLut = DebayerParams::identityLscLut;
 		/* combinedMatrix is reset for each frame. */
 	}
 
