diff --git a/src/ipa/simple/algorithms/lut.cpp b/src/ipa/simple/algorithms/lut.cpp
index 9744e773..dd76e117 100644
--- a/src/ipa/simple/algorithms/lut.cpp
+++ b/src/ipa/simple/algorithms/lut.cpp
@@ -9,14 +9,19 @@
 
 #include <algorithm>
 #include <cmath>
+#include <optional>
 #include <stdint.h>
 
 #include <libcamera/base/log.h>
 
 #include "simple/ipa_context.h"
 
+#include "control_ids.h"
+
 namespace libcamera {
 
+LOG_DEFINE_CATEGORY(IPASoftLut)
+
 namespace ipa::soft::algorithms {
 
 int Lut::configure(IPAContext &context,
@@ -24,24 +29,48 @@ int Lut::configure(IPAContext &context,
 {
 	/* Gamma value is fixed */
 	context.configuration.gamma = 0.5;
+	context.activeState.knobs.contrast = std::optional<double>();
 	updateGammaTable(context);
 
 	return 0;
 }
 
+void Lut::queueRequest(typename Module::Context &context,
+		       [[maybe_unused]] const uint32_t frame,
+		       [[maybe_unused]] typename Module::FrameContext &frameContext,
+		       const ControlList &controls)
+{
+	const auto &contrast = controls.get(controls::Contrast);
+	if (contrast.has_value()) {
+		context.activeState.knobs.contrast = contrast;
+		LOG(IPASoftLut, Debug) << "Setting contrast to " << contrast.value();
+	}
+}
+
 void Lut::updateGammaTable(IPAContext &context)
 {
 	auto &gammaTable = context.activeState.gamma.gammaTable;
-	auto blackLevel = context.activeState.blc.level;
+	const auto blackLevel = context.activeState.blc.level;
 	const unsigned int blackIndex = blackLevel * gammaTable.size() / 256;
+	const auto contrast = context.activeState.knobs.contrast.value_or(1.0);
 
 	std::fill(gammaTable.begin(), gammaTable.begin() + blackIndex, 0);
 	const float divisor = gammaTable.size() - blackIndex - 1.0;
-	for (unsigned int i = blackIndex; i < gammaTable.size(); i++)
-		gammaTable[i] = UINT8_MAX * std::pow((i - blackIndex) / divisor,
-						     context.configuration.gamma);
+	for (unsigned int i = blackIndex; i < gammaTable.size(); i++) {
+		double normalized = (i - blackIndex) / divisor;
+		/* Convert 0..2 to 0..infinity; avoid actual inifinity at tan(pi/2) */
+		double contrastExp = tan(std::clamp(contrast * M_PI_4, 0.0, M_PI_2 - 0.00001));
+		/* Apply simple S-curve */
+		if (normalized < 0.5)
+			normalized = 0.5 * std::pow(normalized / 0.5, contrastExp);
+		else
+			normalized = 1.0 - 0.5 * std::pow((1.0 - normalized) / 0.5, contrastExp);
+		gammaTable[i] = UINT8_MAX *
+				std::pow(normalized, context.configuration.gamma);
+	}
 
 	context.activeState.gamma.blackLevel = blackLevel;
+	context.activeState.gamma.contrast = contrast;
 }
 
 void Lut::prepare(IPAContext &context,
@@ -55,7 +84,8 @@ void Lut::prepare(IPAContext &context,
 	 * observed, it's not permanently prone to minor fluctuations or
 	 * rounding errors.
 	 */
-	if (context.activeState.gamma.blackLevel != context.activeState.blc.level)
+	if (context.activeState.gamma.blackLevel != context.activeState.blc.level ||
+	    context.activeState.gamma.contrast != context.activeState.knobs.contrast)
 		updateGammaTable(context);
 
 	auto &gains = context.activeState.gains;
diff --git a/src/ipa/simple/algorithms/lut.h b/src/ipa/simple/algorithms/lut.h
index b635987d..ef2df147 100644
--- a/src/ipa/simple/algorithms/lut.h
+++ b/src/ipa/simple/algorithms/lut.h
@@ -20,6 +20,11 @@ public:
 	~Lut() = default;
 
 	int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
+	void queueRequest(typename Module::Context &context,
+			  const uint32_t frame,
+			  typename Module::FrameContext &frameContext,
+			  const ControlList &controls)
+		override;
 	void prepare(IPAContext &context,
 		     const uint32_t frame,
 		     IPAFrameContext &frameContext,
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index fd7343e9..0c2f7021 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -11,6 +11,8 @@
 #include <optional>
 #include <stdint.h>
 
+#include <libcamera/controls.h>
+
 #include <libipa/fc_queue.h>
 
 namespace libcamera {
@@ -48,7 +50,12 @@ struct IPAActiveState {
 	struct {
 		std::array<double, kGammaLookupSize> gammaTable;
 		uint8_t blackLevel;
+		double contrast;
 	} gamma;
+	struct {
+		/* 0..2 range, 1.0 = normal */
+		std::optional<double> contrast;
+	} knobs;
 };
 
 struct IPAFrameContext : public FrameContext {
