diff --git a/src/ipa/ipu3/algorithms/tone_mapping.cpp b/src/ipa/ipu3/algorithms/tone_mapping.cpp
index 160338c139448cc9a0bc1fe2400c335a96f68f73..2bc29bb9124dd8bd327ca3064b52c55637f56e7b 100644
--- a/src/ipa/ipu3/algorithms/tone_mapping.cpp
+++ b/src/ipa/ipu3/algorithms/tone_mapping.cpp
@@ -10,6 +10,8 @@
 #include <cmath>
 #include <string.h>
 
+#include <libcamera/base/span.h>
+
 /**
  * \file tone_mapping.h
  */
@@ -27,10 +29,17 @@ namespace ipa::ipu3::algorithms {
  */
 
 ToneMapping::ToneMapping()
-	: gamma_(1.0)
 {
 }
 
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int ToneMapping::init(IPAContext &context, const ValueNode &tuningData)
+{
+	return gammaAlgo_.init(context.ctrlMap, tuningData);
+}
+
 /**
  * \brief Configure the tone mapping given a configInfo
  * \param[in] context The shared IPA context
@@ -41,12 +50,21 @@ ToneMapping::ToneMapping()
 int ToneMapping::configure(IPAContext &context,
 			   [[maybe_unused]] const IPAConfigInfo &configInfo)
 {
-	/* Initialise tone mapping gamma value. */
-	context.activeState.toneMapping.gamma = 0.0;
-
+	gammaAlgo_.configure(context.activeState.gamma);
 	return 0;
 }
 
+/**
+ * \copydoc libcamera::ipa::Algorithm::queueRequest
+ */
+void ToneMapping::queueRequest(IPAContext &context, const uint32_t frame,
+			       IPAFrameContext &frameContext,
+			       const ControlList &controls)
+{
+	gammaAlgo_.queueRequest(context.activeState.gamma, frame,
+				frameContext.gamma, controls);
+}
+
 /**
  * \brief Fill in the parameter structure, and enable gamma control
  * \param[in] context The shared IPA context
@@ -59,14 +77,21 @@ int ToneMapping::configure(IPAContext &context,
  */
 void ToneMapping::prepare([[maybe_unused]] IPAContext &context,
 			  [[maybe_unused]] const uint32_t frame,
-			  [[maybe_unused]] IPAFrameContext &frameContext,
+			  IPAFrameContext &frameContext,
 			  ipu3_uapi_params *params)
 {
-	/* Copy the calculated LUT into the parameters buffer. */
-	memcpy(params->acc_param.gamma.gc_lut.lut,
-	       context.activeState.toneMapping.gammaCorrection.lut,
-	       IPU3_UAPI_GAMMA_CORR_LUT_ENTRIES *
-	       sizeof(params->acc_param.gamma.gc_lut.lut[0]));
+	if (!frameContext.gamma.update)
+		return;
+
+	/*
+	 * Unfortunately necessary given the IPU3's gamma uAPI struct has the
+	 * __packed attribute.
+	 */
+	uint16_t *lutData = reinterpret_cast<uint16_t *>(
+		__builtin_assume_aligned(params->acc_param.gamma.gc_lut.lut, 16));
+	Span<uint16_t> lut {lutData, kNumLutNodes};
+
+	gammaAlgo_.prepare(frameContext.gamma, lut);
 
 	/* Enable the custom gamma table. */
 	params->use.acc_gamma = 1;
@@ -84,33 +109,13 @@ void ToneMapping::prepare([[maybe_unused]] IPAContext &context,
  * The tone mapping look up table is generated as an inverse power curve from
  * our gamma setting.
  */
-void ToneMapping::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
-			  [[maybe_unused]] IPAFrameContext &frameContext,
+void ToneMapping::process([[maybe_unused]] IPAContext &context,
+			  [[maybe_unused]] const uint32_t frame,
+			  IPAFrameContext &frameContext,
 			  [[maybe_unused]] const ipu3_uapi_stats_3a *stats,
 			  [[maybe_unused]] ControlList &metadata)
 {
-	/*
-	 * Hardcode gamma to 1.1 as a default for now.
-	 *
-	 * \todo Expose gamma control setting through the libcamera control API
-	 */
-	gamma_ = 1.1;
-
-	if (context.activeState.toneMapping.gamma == gamma_)
-		return;
-
-	struct ipu3_uapi_gamma_corr_lut &lut =
-		context.activeState.toneMapping.gammaCorrection;
-
-	for (uint32_t i = 0; i < std::size(lut.lut); i++) {
-		double j = static_cast<double>(i) / (std::size(lut.lut) - 1);
-		double gamma = std::pow(j, 1.0 / gamma_);
-
-		/* The output value is expressed on 13 bits. */
-		lut.lut[i] = gamma * 8191;
-	}
-
-	context.activeState.toneMapping.gamma = gamma_;
+	gammaAlgo_.process(frameContext.gamma, metadata);
 }
 
 REGISTER_IPA_ALGORITHM(ToneMapping, "ToneMapping")
diff --git a/src/ipa/ipu3/algorithms/tone_mapping.h b/src/ipa/ipu3/algorithms/tone_mapping.h
index b2b380108e014b3d5ee7b93bcdd948dea4d2302d..db351a32b4383c4d607a2d6ee6a8fa3994a1d436 100644
--- a/src/ipa/ipu3/algorithms/tone_mapping.h
+++ b/src/ipa/ipu3/algorithms/tone_mapping.h
@@ -7,6 +7,8 @@
 
 #pragma once
 
+#include <libipa/gamma.h>
+
 #include "algorithm.h"
 
 namespace libcamera {
@@ -18,7 +20,11 @@ class ToneMapping : public Algorithm
 public:
 	ToneMapping();
 
+	int init(IPAContext &context, const ValueNode &tuningData) override;
 	int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
+	void queueRequest(IPAContext &context, const uint32_t frame,
+			  IPAFrameContext &frameContext,
+			  const ControlList &controls) override;
 	void prepare(IPAContext &context, const uint32_t frame,
 		     IPAFrameContext &frameContext, ipu3_uapi_params *params) override;
 	void process(IPAContext &context, const uint32_t frame,
@@ -27,7 +33,8 @@ public:
 		     ControlList &metadata) override;
 
 private:
-	double gamma_;
+	static constexpr unsigned int kNumLutNodes = IPU3_UAPI_GAMMA_CORR_LUT_ENTRIES;
+	GammaAlgorithm<kNumLutNodes, UQ<0, 13>> gammaAlgo_;
 };
 
 } /* namespace ipa::ipu3::algorithms */
diff --git a/src/ipa/ipu3/ipa_context.cpp b/src/ipa/ipu3/ipa_context.cpp
index dc43bc0877ed5c2e7287414e12667374f5ee1c80..7152d070d3ab1bc463fdaad437d5e1c1b87ce25c 100644
--- a/src/ipa/ipu3/ipa_context.cpp
+++ b/src/ipa/ipu3/ipa_context.cpp
@@ -119,6 +119,11 @@ namespace libcamera::ipa::ipu3 {
  * \brief Active colour Correction Matrix parameters for the IPA
  */
 
+/**
+ * \var IPAActiveState::gamma
+ * \brief Active gamma correction parameters for the IPA
+ */
+
 /**
  * \var IPASessionConfiguration::sensor
  * \brief Sensor-specific configuration of the IPA
@@ -154,20 +159,6 @@ namespace libcamera::ipa::ipu3 {
  * The gain should be adapted to the sensor specific gain code before applying.
  */
 
-/**
- * \var IPAActiveState::toneMapping
- * \brief Context for ToneMapping and Gamma control
- *
- * \var IPAActiveState::toneMapping.gamma
- * \brief Gamma value for the LUT
- *
- * \var IPAActiveState::toneMapping.gammaCorrection
- * \brief Per-pixel tone mapping implemented as a LUT
- *
- * The LUT structure is defined by the IPU3 kernel interface. See
- * <linux/intel-ipu3.h> struct ipu3_uapi_gamma_corr_lut for further details.
- */
-
 /**
  * \struct IPAFrameContext
  * \brief IPU3-specific FrameContext
@@ -192,4 +183,9 @@ namespace libcamera::ipa::ipu3 {
  * \brief Per-frame colour Correction Matrix parameters for the IPA
  */
 
+/**
+ * \var IPAFrameContext::gamma
+ * \brief Per-frame gamma correction parameters for the IPA
+ */
+
 } /* namespace libcamera::ipa::ipu3 */
diff --git a/src/ipa/ipu3/ipa_context.h b/src/ipa/ipu3/ipa_context.h
index be626d30d966b1bdaa322e5154f95f745f799976..1eaaac82da0e3ad5bed0749c39d9dad8c585cab0 100644
--- a/src/ipa/ipu3/ipa_context.h
+++ b/src/ipa/ipu3/ipa_context.h
@@ -18,6 +18,7 @@
 #include <libipa/awb.h>
 #include <libipa/ccm.h>
 #include <libipa/fc_queue.h>
+#include <libipa/gamma.h>
 
 namespace libcamera {
 
@@ -66,11 +67,7 @@ struct IPAActiveState {
 
 	ipa::awb::ActiveState awb;
 	ipa::ccm::ActiveState ccm;
-
-	struct {
-		double gamma;
-		struct ipu3_uapi_gamma_corr_lut gammaCorrection;
-	} toneMapping;
+	ipa::gamma::ActiveState gamma;
 };
 
 struct IPAFrameContext : public FrameContext {
@@ -81,6 +78,7 @@ struct IPAFrameContext : public FrameContext {
 
 	ipa::awb::FrameContext awb;
 	ipa::ccm::FrameContext ccm;
+	ipa::gamma::FrameContext gamma;
 };
 
 struct IPAContext {
