diff --git a/src/ipa/ipu3/ipa_context.cpp b/src/ipa/ipu3/ipa_context.cpp
index 06fdf2a1efc7..f09d061e2d19 100644
--- a/src/ipa/ipu3/ipa_context.cpp
+++ b/src/ipa/ipu3/ipa_context.cpp
@@ -126,8 +126,18 @@ namespace libcamera::ipa::ipu3 {
  * \var IPASessionConfiguration::sensor.lineDuration
  * \brief Line duration in microseconds
  *
- * \var IPASessionConfiguration::sensor.vBlank
- * \brief The vertical blanking expressed in number of lines
+ * \var IPASessionConfiguration::sensor.info
+ * \brief The IPACameraSensorInfo valid for the session
+ *
+ * \var IPASessionConfiguration::sensor.controls
+ * \brief The ControlInfoMap of camera sensor control limits valid for the
+ * session
+ *
+ * \var IPASessionConfiguration::lens
+ * \brief Lens-specific configuration of the IPA
+ *
+ * \var IPASessionConfiguration::lens.controls
+ * \brief The ControlInfoMap of lens control limits valid for the session
  */
 
 /**
diff --git a/src/ipa/ipu3/ipa_context.h b/src/ipa/ipu3/ipa_context.h
index a5b878ab7792..35baa17e8708 100644
--- a/src/ipa/ipu3/ipa_context.h
+++ b/src/ipa/ipu3/ipa_context.h
@@ -17,6 +17,8 @@
 #include <libcamera/controls.h>
 #include <libcamera/geometry.h>
 
+#include <libcamera/ipa/ipu3_ipa_interface.h>
+
 namespace libcamera {
 
 namespace ipa::ipu3 {
@@ -43,9 +45,14 @@ struct IPASessionConfiguration {
 	} agc;
 
 	struct {
-		int32_t vBlank;
 		utils::Duration lineDuration;
+		ControlInfoMap controls;
+		IPACameraSensorInfo info;
 	} sensor;
+
+	struct {
+		ControlInfoMap controls;
+	} lens;
 };
 
 struct IPAActiveState {
diff --git a/src/ipa/ipu3/ipu3.cpp b/src/ipa/ipu3/ipu3.cpp
index eb97c8be5431..f2e28c06a2c2 100644
--- a/src/ipa/ipu3/ipu3.cpp
+++ b/src/ipa/ipu3/ipu3.cpp
@@ -152,16 +152,11 @@ private:
 
 	bool validateSensorControls(const ControlInfoMap &sensorControls);
 
-	void setControls(unsigned int frame);
+	void setControls(unsigned int frame, const IPAActiveState &state);
 	void calculateBdsGrid(const Size &bdsOutputSize);
 
 	std::map<unsigned int, MappedFrameBuffer> buffers_;
 
-	ControlInfoMap sensorCtrls_;
-	ControlInfoMap lensCtrls_;
-
-	IPACameraSensorInfo sensorInfo_;
-
 	/* Interface to the Camera Helper */
 	std::unique_ptr<CameraSensorHelper> camHelper_;
 
@@ -179,7 +174,6 @@ private:
 void IPAIPU3::updateSessionConfiguration(const IPAConfigInfo &info)
 {
 	const IPACameraSensorInfo &sensorInfo = info.sensorInfo;
-	context_.configuration.sensor.vBlank = sensorInfo.vblank;
 	context_.configuration.sensor.lineDuration = sensorInfo.lineLength * 1.0s
 						   / sensorInfo.pixelRate;
 
@@ -199,6 +193,11 @@ void IPAIPU3::updateSessionConfiguration(const IPAConfigInfo &info)
 	const ControlInfo &gain = sensorControls.at(&controls::internal::AnalogueGain);
 	context_.configuration.agc.minAnalogueGain = gain.min().get<float>();
 	context_.configuration.agc.maxAnalogueGain = gain.max().get<float>();
+
+	/* Store the sensor and lens configuration in the current session. */
+	context_.configuration.sensor.info = info.sensorInfo;
+	context_.configuration.sensor.controls = info.sensorControls;
+	context_.configuration.lens.controls = info.lensControls;
 }
 
 /**
@@ -262,7 +261,7 @@ int IPAIPU3::start()
 	 * Set the sensors V4L2 controls before the first frame to ensure that
 	 * we have an expected and known configuration from the start.
 	 */
-	setControls(0);
+	setControls(0, context_.activeState);
 
 	return 0;
 }
@@ -379,10 +378,6 @@ int IPAIPU3::configure(const IPAConfigInfo &configInfo)
 			return ret;
 	}
 
-	sensorInfo_ = configInfo.sensorInfo;
-	lensCtrls_ = configInfo.lensControls;
-	sensorCtrls_ = configInfo.sensorControls;
-
 	return 0;
 }
 
@@ -472,10 +467,10 @@ void IPAIPU3::processStatsBuffer(const uint32_t frame,
 		return;
 	}
 
-	Span<uint8_t> mem = it->second.planes()[0];
-	const ipu3_uapi_stats_3a *stats =
-		reinterpret_cast<ipu3_uapi_stats_3a *>(mem.data());
-
+	/*
+	 * Update the per-frame context storing the sensor exposure and
+	 * gain for later use by algorithms.
+	 */
 	IPAFrameContext &frameContext = context_.frameContexts[frame % kMaxFrameContexts];
 
 	if (frameContext.frame != frame)
@@ -484,23 +479,28 @@ void IPAIPU3::processStatsBuffer(const uint32_t frame,
 	frameContext.sensor.exposure = sensorControls.get(V4L2_CID_EXPOSURE).get<int32_t>();
 	frameContext.sensor.gain = camHelper_->gain(sensorControls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>());
 
-	double lineDuration = context_.configuration.sensor.lineDuration.get<std::micro>();
-	int32_t vBlank = context_.configuration.sensor.vBlank;
-	ControlList ctrls(controls::controls);
+	/* Run algorithms on the statistics and per-frame context. */
+	Span<uint8_t> mem = it->second.planes()[0];
+	const ipu3_uapi_stats_3a *stats = reinterpret_cast<ipu3_uapi_stats_3a *>(mem.data());
 
 	for (auto const &algo : algorithms_)
 		algo->process(context_, &frameContext, stats);
 
-	setControls(frame);
-
-	int64_t frameDuration = (vBlank + sensorInfo_.outputSize.height) * lineDuration;
-	ctrls.set(controls::FrameDuration, frameDuration);
+	/* Set sensor controls using the newly computed values. */
+	setControls(frame, context_.activeState);
 
-	ctrls.set(controls::AnalogueGain, frameContext.sensor.gain);
+	/* Prepare metadata for the frame. */
+	const IPASessionConfiguration &ipaConfig = context_.configuration;
+	const IPACameraSensorInfo &sensorInfo = ipaConfig.sensor.info;
+	double lineDuration = ipaConfig.sensor.lineDuration.get<std::micro>();
+	int64_t frameDuration = (sensorInfo.vblank + sensorInfo.outputSize.height) * lineDuration;
 
-	ctrls.set(controls::ColourTemperature, context_.activeState.awb.temperatureK);
+	ControlList metadata(controls::controls);
 
-	ctrls.set(controls::ExposureTime, frameContext.sensor.exposure * lineDuration);
+	metadata.set(controls::FrameDuration, frameDuration);
+	metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
+	metadata.set(controls::ColourTemperature, context_.activeState.awb.temperatureK);
+	metadata.set(controls::ExposureTime, frameContext.sensor.exposure * lineDuration);
 
 	/*
 	 * \todo The Metadata provides a path to getting extended data
@@ -510,7 +510,7 @@ void IPAIPU3::processStatsBuffer(const uint32_t frame,
 	 * likely want to avoid putting platform specific metadata in.
 	 */
 
-	metadataReady.emit(frame, ctrls);
+	metadataReady.emit(frame, metadata);
 }
 
 /**
@@ -530,22 +530,24 @@ void IPAIPU3::queueRequest(const uint32_t frame, const ControlList &controls)
 /**
  * \brief Handle sensor controls for a given \a frame number
  * \param[in] frame The frame on which the sensor controls should be set
+ * \param[in] state The IPA active state which contains the control values as
+ * computed by the algorithms
  *
  * Send the desired sensor control values to the pipeline handler to request
  * that they are applied on the camera sensor.
  */
-void IPAIPU3::setControls(unsigned int frame)
+void IPAIPU3::setControls(unsigned int frame, const IPAActiveState &state)
 {
-	int32_t exposure = context_.activeState.agc.exposure;
-	int32_t gain = camHelper_->gainCode(context_.activeState.agc.gain);
+	int32_t exposure = state.agc.exposure;
+	int32_t gain = camHelper_->gainCode(state.agc.gain);
 
-	ControlList ctrls(sensorCtrls_);
+	ControlList ctrls(controls::controls);
 	ctrls.set(V4L2_CID_EXPOSURE, exposure);
 	ctrls.set(V4L2_CID_ANALOGUE_GAIN, gain);
 
-	ControlList lensCtrls(lensCtrls_);
+	ControlList lensCtrls(controls::controls);
 	lensCtrls.set(V4L2_CID_FOCUS_ABSOLUTE,
-		      static_cast<int32_t>(context_.activeState.af.focus));
+		      static_cast<int32_t>(state.af.focus));
 
 	setSensorControls.emit(frame, ctrls, lensCtrls);
 }
