diff --git a/src/ipa/simple/algorithms/agc.cpp b/src/ipa/simple/algorithms/agc.cpp
index 7b980c7382..927bc6ab22 100644
--- a/src/ipa/simple/algorithms/agc.cpp
+++ b/src/ipa/simple/algorithms/agc.cpp
@@ -13,30 +13,76 @@
 
 #include <libipa/histogram.h>
 
-#include "control_ids.h"
-
 namespace libcamera {
 
 LOG_DEFINE_CATEGORY(IPASoftExposure)
 
 namespace ipa::soft::algorithms {
 
+namespace {
+
+class AgcTraits : public AgcMeanLuminance::Traits
+{
+public:
+        AgcTraits(const Histogram &yHist)
+                : yHist_(yHist)
+        {
+        }
+
+        double estimateLuminance(double gain) const override
+        {
+		/*
+		 * TODO: Improve by asking the weight of saturating and non-saturating
+		 * bins directly from the histogram
+		 */
+		double sum = 0;
+
+		for (size_t i = 0; i < yHist_.bins(); i++)
+			sum += std::min<double>(yHist_.bins(), i * gain) * yHist_[i];
+
+		return sum / yHist_.total() / yHist_.bins();
+        }
+
+private:
+        const Histogram &yHist_;
+};
+
+} /* namespace */
+
+int Agc::init(IPAContext &context, const ValueNode &tuningData)
+{
+	int ret = agc_.init(tuningData, context.camHelper.get());
+	if (ret)
+		return ret;
+
+	return agc_.configure(context.configuration.agc, context.activeState.agc, {
+		.sensorInfo = context.sensorInfo,
+		.sensorControls = context.sensorControls,
+		.ctrlMap = context.ctrlMap,
+		.autoAllowed = true,
+	});
+}
+
 int Agc::configure(IPAContext &context, [[maybe_unused]] const IPAConfigInfo &configInfo)
 {
-	agc_.setLimits({
-		.exposure = {
-			context.configuration.agc.exposureMin,
-			context.configuration.agc.exposureMax,
-		},
-		.gain = {
-			context.configuration.agc.againMin,
-			context.configuration.agc.againMax,
-		},
-		.gainMinStep = context.configuration.agc.againMinStep,
-		.gain1 = context.configuration.agc.again10,
+	return agc_.configure(context.configuration.agc, context.activeState.agc, {
+		.sensorInfo = context.sensorInfo,
+		.sensorControls = context.sensorControls,
+		.ctrlMap = context.ctrlMap,
+		.autoAllowed = true,
 	});
+}
+
+void Agc::queueRequest(IPAContext &context, [[maybe_unused]] const uint32_t frame,
+		       IPAFrameContext &frameContext, const ControlList &controls)
+{
+	agc_.queueRequest(context.configuration.agc, context.activeState.agc, frameContext.agc, controls);
+}
 
-	return 0;
+void Agc::prepare(IPAContext &context, [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext, [[maybe_unused]] DebayerParams *params)
+{
+	agc_.prepare(context.activeState.agc, frameContext.agc);
 }
 
 void Agc::process(IPAContext &context,
@@ -45,49 +91,25 @@ void Agc::process(IPAContext &context,
 		  const SwIspStats *stats,
 		  ControlList &metadata)
 {
-	utils::Duration exposureTime =
-		context.configuration.agc.lineDuration * frameContext.sensor.exposure;
-	metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
-	metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
-
-	if (!context.activeState.agc.valid) {
-		/*
-		 * Init active-state from sensor values in case updateExposure()
-		 * does not run for the first frame.
-		 */
-		context.activeState.agc.exposure = frameContext.sensor.exposure;
-		context.activeState.agc.again = frameContext.sensor.gain;
-		context.activeState.agc.valid = true;
-	}
-
-	if (!stats->valid) {
-		/*
-		 * Use the new exposure and gain values calculated the last time
-		 * there were valid stats.
-		 */
-		frameContext.agc.exposure = context.activeState.agc.exposure;
-		frameContext.agc.gain = context.activeState.agc.again;
-		return;
+	if (stats->valid) {
+		auto histogram = stats->yHistogram;
+
+		const unsigned int blackLevelHistIdx =
+			context.activeState.blc.level * std::size(histogram) / 256;
+		for (size_t i = 1; i < blackLevelHistIdx; i++)
+			histogram[0] += std::exchange(histogram[i], 0);
+
+		Histogram yHist(histogram);
+
+		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {{
+			.traits = AgcTraits(yHist),
+			.yHist = yHist,
+			.exposure = frameContext.sensor.exposure,
+			.gain = frameContext.sensor.gain,
+		}}, metadata);
+	} else {
+		agc_.process(context.configuration.agc, context.activeState.agc, frameContext.agc, {}, metadata);
 	}
-
-	auto histogram = stats->yHistogram;
-	const unsigned int blackLevelHistIdx =
-		context.activeState.blc.level * std::size(histogram) / 256;
-
-	for (unsigned int i = 1; i < blackLevelHistIdx; i++)
-		histogram[0] += std::exchange(histogram[i], 0);
-
-	const auto &newEv = agc_.calculateNewEv({
-		.yHist = { histogram },
-		.exposure = frameContext.sensor.exposure,
-		.gain = frameContext.sensor.gain,
-	});
-
-	frameContext.agc.exposure = newEv.exposure;
-	frameContext.agc.gain = newEv.analogueGain;
-
-	context.activeState.agc.exposure = frameContext.agc.exposure;
-	context.activeState.agc.again = frameContext.agc.gain;
 }
 
 REGISTER_IPA_ALGORITHM(Agc, "Agc")
diff --git a/src/ipa/simple/algorithms/agc.h b/src/ipa/simple/algorithms/agc.h
index 2e156e135c..6e37ce6eff 100644
--- a/src/ipa/simple/algorithms/agc.h
+++ b/src/ipa/simple/algorithms/agc.h
@@ -9,7 +9,7 @@
 
 #include "algorithm.h"
 
-#include <libipa/agc_msv.h>
+#include <libipa/agc.h>
 
 namespace libcamera {
 
@@ -18,15 +18,23 @@ namespace ipa::soft::algorithms {
 class Agc : public Algorithm
 {
 public:
+	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, DebayerParams *params) override;
+
 	void process(IPAContext &context, const uint32_t frame,
 		     IPAFrameContext &frameContext,
 		     const SwIspStats *stats,
 		     ControlList &metadata) override;
 
 private:
-	AgcMSV agc_;
+	AgcAlgorithm agc_;
 };
 
 } /* namespace ipa::soft::algorithms */
diff --git a/src/ipa/simple/ipa_context.h b/src/ipa/simple/ipa_context.h
index 96b3c5b21d..63889cd8f6 100644
--- a/src/ipa/simple/ipa_context.h
+++ b/src/ipa/simple/ipa_context.h
@@ -17,6 +17,7 @@
 #include "libcamera/internal/matrix.h"
 #include "libcamera/internal/vector.h"
 
+#include <libipa/agc.h>
 #include <libipa/camera_sensor_helper.h>
 #include <libipa/fc_queue.h>
 
@@ -27,10 +28,7 @@ namespace libcamera {
 namespace ipa::soft {
 
 struct IPASessionConfiguration {
-	struct {
-		uint32_t exposureMin, exposureMax;
-		double againMin, againMax, again10, againMinStep;
-		utils::Duration lineDuration;
+	struct Agc : agc::Session {
 	} agc;
 	struct {
 		std::optional<uint8_t> level;
@@ -38,10 +36,7 @@ struct IPASessionConfiguration {
 };
 
 struct IPAActiveState {
-	struct {
-		uint32_t exposure;
-		double again;
-		bool valid;
+	struct Agc : agc::ActiveState {
 	} agc;
 
 	struct {
@@ -68,9 +63,7 @@ struct IPAActiveState {
 struct IPAFrameContext : public FrameContext {
 	Matrix<float, 3, 3> ccm;
 
-	struct {
-		uint32_t exposure;
-		double gain;
+	struct Agc : agc::FrameContext {
 	} agc;
 
 	struct {
diff --git a/src/ipa/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
index 8b5df884d4..f35e269deb 100644
--- a/src/ipa/simple/soft_simple.cpp
+++ b/src/ipa/simple/soft_simple.cpp
@@ -107,6 +107,7 @@ int IPASoftSimple::init(const IPASettings &settings,
 	}
 
 	context_.sensorInfo = sensorInfo;
+	context_.sensorControls = sensorControls;
 
 	/* Load the tuning data file */
 	File file(settings.configurationFile);
@@ -180,22 +181,6 @@ int IPASoftSimple::init(const IPASettings &settings,
 	ControlInfoMap::Map ctrlMap = context_.ctrlMap;
 	*ipaControls = ControlInfoMap(std::move(ctrlMap), controls::controls);
 
-	/*
-	 * Check if the sensor driver supports the controls required by the
-	 * Soft IPA.
-	 * Don't save the min and max control values yet, as e.g. the limits
-	 * for V4L2_CID_EXPOSURE depend on the configured sensor resolution.
-	 */
-	if (sensorControls.find(V4L2_CID_EXPOSURE) == sensorControls.end()) {
-		LOG(IPASoft, Error) << "Don't have exposure control";
-		return -EINVAL;
-	}
-
-	if (sensorControls.find(V4L2_CID_ANALOGUE_GAIN) == sensorControls.end()) {
-		LOG(IPASoft, Error) << "Don't have gain control";
-		return -EINVAL;
-	}
-
 	return 0;
 }
 
@@ -203,55 +188,17 @@ int IPASoftSimple::configure(const IPAConfigInfo &configInfo, ControlInfoMap *ip
 {
 	context_.sensorControls = configInfo.sensorControls;
 
-	const ControlInfo &exposureInfo = context_.sensorControls.find(V4L2_CID_EXPOSURE)->second;
-	const ControlInfo &gainInfo = context_.sensorControls.find(V4L2_CID_ANALOGUE_GAIN)->second;
-
 	/* Clear the IPA context before the streaming session. */
 	context_.configuration = {};
 	context_.activeState = {};
 	context_.frameContexts.clear();
 
-	context_.configuration.agc.lineDuration =
-		context_.sensorInfo.minLineLength * 1.0s / context_.sensorInfo.pixelRate;
-	context_.configuration.agc.exposureMin = exposureInfo.min().get<int32_t>();
-	context_.configuration.agc.exposureMax = exposureInfo.max().get<int32_t>();
-	if (!context_.configuration.agc.exposureMin) {
-		LOG(IPASoft, Warning) << "Minimum exposure is zero, that can't be linear";
-		context_.configuration.agc.exposureMin = 1;
-	}
-
-	int32_t againMin = gainInfo.min().get<int32_t>();
-	int32_t againMax = gainInfo.max().get<int32_t>();
-	int32_t againDef = gainInfo.def().get<int32_t>();
-
-	if (context_.camHelper) {
-		context_.configuration.agc.againMin = context_.camHelper->gain(againMin);
-		context_.configuration.agc.againMax = context_.camHelper->gain(againMax);
-		context_.configuration.agc.again10 = std::max(context_.configuration.agc.againMin, 1.0);
-		context_.configuration.agc.againMinStep =
-			(context_.configuration.agc.againMax -
-			 context_.configuration.agc.againMin) /
-			100.0;
-	} else {
-		context_.configuration.agc.againMax = againMax;
-		context_.configuration.agc.again10 = againDef;
-		context_.configuration.agc.againMin = againMin;
-		context_.configuration.agc.againMinStep = 1.0;
-	}
-
 	for (const auto &algo : algorithms()) {
 		int ret = algo->configure(context_, configInfo);
 		if (ret)
 			return ret;
 	}
 
-	LOG(IPASoft, Info)
-		<< "Exposure " << context_.configuration.agc.exposureMin << "-"
-		<< context_.configuration.agc.exposureMax
-		<< ", gain " << context_.configuration.agc.againMin << "-"
-		<< context_.configuration.agc.againMax
-		<< " (" << context_.configuration.agc.againMinStep << ")";
-
 	*ipaControls = { ControlInfoMap::Map(context_.ctrlMap), controls::controls };
 
 	return 0;
