diff --git a/src/ipa/ipu3/algorithms/agc.cpp b/src/ipa/ipu3/algorithms/agc.cpp
index ee9a42cf..a84534ea 100644
--- a/src/ipa/ipu3/algorithms/agc.cpp
+++ b/src/ipa/ipu3/algorithms/agc.cpp
@@ -71,11 +71,41 @@ static constexpr uint32_t kNumStartupFrames = 10;
 static constexpr double kRelativeLuminanceTarget = 0.16;
 
 Agc::Agc()
-	: frameCount_(0), minShutterSpeed_(0s),
-	  maxShutterSpeed_(0s), filteredExposure_(0s)
+	: minShutterSpeed_(0s), maxShutterSpeed_(0s), context_(nullptr)
 {
 }
 
+/**
+ * \brief Initialise the AGC algorith from tuning files
+ *
+ * \param[in] context The shared IPA context
+ * \param[in] tuningData The YamlObject containing Agc tuning data
+ *
+ * This function calls the base class' tuningData parsers to discover which
+ * control values are supported.
+ *
+ * \return 0 on success or errors from the base class
+ */
+int Agc::init(IPAContext &context, const YamlObject &tuningData)
+{
+	int ret;
+
+	parseRelativeLuminanceTarget(tuningData);
+
+	ret = parseConstraintModes(tuningData);
+	if (ret)
+		return ret;
+
+	ret = parseExposureModes(tuningData);
+	if (ret)
+		return ret;
+
+	context.ctrlMap.merge(controls());
+	context_ = &context;
+
+	return 0;
+}
+
 /**
  * \brief Configure the AGC given a configInfo
  * \param[in] context The shared IPA context
@@ -103,6 +133,20 @@ int Agc::configure(IPAContext &context,
 	activeState.agc.exposure = 10ms / configuration.sensor.lineDuration;
 
 	frameCount_ = 0;
+
+	/*
+	 * \todo We should use the first available mode rather than assume that
+	 * the "Normal" modes are present in tuning data.
+	 */
+	context.activeState.agc.constraintMode = controls::ConstraintNormal;
+	context.activeState.agc.exposureMode = controls::ExposureNormal;
+
+	for (auto &[id, helper] : exposureModeHelpers()) {
+		/* \todo Run this again when FrameDurationLimits is passed in */
+		helper->configure(minShutterSpeed_, maxShutterSpeed_,
+				  minAnalogueGain_, maxAnalogueGain_);
+	}
+
 	return 0;
 }
 
@@ -280,11 +324,6 @@ void Agc::computeExposure(IPAContext &context, IPAFrameContext &frameContext,
 	LOG(IPU3Agc, Debug) << "Divided up shutter and gain are "
 			    << shutterTime << " and "
 			    << stepGain;
-
-	IPAActiveState &activeState = context.activeState;
-	/* Update the estimated exposure and gain. */
-	activeState.agc.exposure = shutterTime / configuration.sensor.lineDuration;
-	activeState.agc.gain = stepGain;
 }
 
 /**
@@ -347,6 +386,26 @@ double Agc::estimateLuminance(IPAActiveState &activeState,
 	return ySum / (grid.height * grid.width) / 255;
 }
 
+double Agc::estimateLuminance(double gain)
+{
+	ASSERT(reds_.size() == greens_.size());
+	ASSERT(greens_.size() == blues_.size());
+	const ipu3_uapi_grid_config &grid = context_->configuration.grid.bdsGrid;
+	double redSum = 0, greenSum = 0, blueSum = 0;
+
+	for (unsigned int i = 0; i < reds_.size(); i++) {
+		redSum += std::min(reds_[i] * gain, 255.0);
+		greenSum += std::min(greens_[i] * gain, 255.0);
+		blueSum += std::min(blues_[i] * gain, 255.0);
+	}
+
+	double ySum = redSum * context_->activeState.awb.gains.red * 0.299
+		+ greenSum * context_->activeState.awb.gains.green * 0.587
+		+ blueSum * context_->activeState.awb.gains.blue * 0.114;
+
+	return ySum / (grid.height * grid.width) / 255;
+}
+
 /**
  * \brief Process IPU3 statistics, and run AGC operations
  * \param[in] context The shared IPA context
@@ -399,8 +458,33 @@ void Agc::process(IPAContext &context, [[maybe_unused]] const uint32_t frame,
 	computeExposure(context, frameContext, yGain, iqMeanGain);
 	frameCount_++;
 
+	parseStatistics(stats, context.configuration.grid.bdsGrid);
+
+	/*
+	 * The Agc algorithm needs to know the effective exposure value that was
+	 * applied to the sensor when the statistics were collected.
+	 */
 	utils::Duration exposureTime = context.configuration.sensor.lineDuration
 				     * frameContext.sensor.exposure;
+	double analogueGain = frameContext.sensor.gain;
+	utils::Duration effectiveExposureValue = exposureTime * analogueGain;
+
+	utils::Duration shutterTime;
+	double aGain, dGain;
+	std::tie(shutterTime, aGain, dGain) =
+		calculateNewEv(context.activeState.agc.constraintMode,
+			       context.activeState.agc.exposureMode, hist_,
+			       effectiveExposureValue);
+
+	LOG(IPU3Agc, Debug)
+		<< "Divided up shutter, analogue gain and digital gain are "
+		<< shutterTime << ", " << aGain << " and " << dGain;
+
+	IPAActiveState &activeState = context.activeState;
+	/* Update the estimated exposure and gain. */
+	activeState.agc.exposure = shutterTime / context.configuration.sensor.lineDuration;
+	activeState.agc.gain = aGain;
+
 	metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
 	metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
 
diff --git a/src/ipa/ipu3/algorithms/agc.h b/src/ipa/ipu3/algorithms/agc.h
index 7ed0ef7a..8405da9d 100644
--- a/src/ipa/ipu3/algorithms/agc.h
+++ b/src/ipa/ipu3/algorithms/agc.h
@@ -13,6 +13,7 @@
 
 #include <libcamera/geometry.h>
 
+#include "libipa/agc.h"
 #include "libipa/histogram.h"
 
 #include "algorithm.h"
@@ -23,12 +24,13 @@ struct IPACameraSensorInfo;
 
 namespace ipa::ipu3::algorithms {
 
-class Agc : public Algorithm
+class Agc : public Algorithm, public MeanLuminanceAgc
 {
 public:
 	Agc();
 	~Agc() = default;
 
+	int init(IPAContext &context, const YamlObject &tuningData) override;
 	int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
 	void process(IPAContext &context, const uint32_t frame,
 		     IPAFrameContext &frameContext,
@@ -45,6 +47,7 @@ private:
 				 const ipu3_uapi_grid_config &grid,
 				 const ipu3_uapi_stats_3a *stats,
 				 double gain);
+	double estimateLuminance(double gain) override;
 	void parseStatistics(const ipu3_uapi_stats_3a *stats,
 			     const ipu3_uapi_grid_config &grid);
 
@@ -59,6 +62,7 @@ private:
 	utils::Duration filteredExposure_;
 
 	uint32_t stride_;
+	IPAContext *context_;
 	std::vector<uint8_t> reds_;
 	std::vector<uint8_t> blues_;
 	std::vector<uint8_t> greens_;
diff --git a/src/ipa/ipu3/ipa_context.cpp b/src/ipa/ipu3/ipa_context.cpp
index 959f314f..c4fb5642 100644
--- a/src/ipa/ipu3/ipa_context.cpp
+++ b/src/ipa/ipu3/ipa_context.cpp
@@ -47,6 +47,9 @@ namespace libcamera::ipa::ipu3 {
  *
  * \var IPAContext::activeState
  * \brief The current state of IPA algorithms
+ *
+ * \var IPAContext::ctrlMap
+ * \brief A ControlInfoMap::Map of controls populated by the algorithms
  */
 
 /**
diff --git a/src/ipa/ipu3/ipa_context.h b/src/ipa/ipu3/ipa_context.h
index e9a3863b..a92cb6ce 100644
--- a/src/ipa/ipu3/ipa_context.h
+++ b/src/ipa/ipu3/ipa_context.h
@@ -12,6 +12,7 @@
 
 #include <libcamera/base/utils.h>
 
+#include <libcamera/controls.h>
 #include <libcamera/geometry.h>
 
 #include <libipa/fc_queue.h>
@@ -55,6 +56,8 @@ struct IPAActiveState {
 	struct {
 		uint32_t exposure;
 		double gain;
+		uint32_t constraintMode;
+		uint32_t exposureMode;
 	} agc;
 
 	struct {
@@ -85,6 +88,8 @@ struct IPAContext {
 	IPAActiveState activeState;
 
 	FCQueue<IPAFrameContext> frameContexts;
+
+	ControlInfoMap::Map ctrlMap;
 };
 
 } /* namespace ipa::ipu3 */
diff --git a/src/ipa/ipu3/ipu3.cpp b/src/ipa/ipu3/ipu3.cpp
index 08ee6eb3..2fcc0c91 100644
--- a/src/ipa/ipu3/ipu3.cpp
+++ b/src/ipa/ipu3/ipu3.cpp
@@ -189,7 +189,7 @@ private:
 };
 
 IPAIPU3::IPAIPU3()
-	: context_({ {}, {}, { kMaxFrameContexts } })
+	: context_({ {}, {}, { kMaxFrameContexts }, {} })
 {
 }
 
