[RFC,v1,20/23] libcamera: pipeline: rpi: Queue metadata until completion
diff mbox series

Message ID 20250606164156.1442682-21-barnabas.pocze@ideasonboard.com
State New
Headers show
Series
  • libcamera: Add `MetadataList`
Related show

Commit Message

Barnabás Pőcze June 6, 2025, 4:41 p.m. UTC
The rpi pipeline drops a certain number of initial frames. It uses
`ControlList::clear()` to remove the metadata items of a request that
was populated while processing a frame that is ultimately dropped.
Since the decision to drop a frame only occurs at the very end of the
processing, this stop-gap measure is introduced to delay metadata
completion until the request is actually completed.

Obsoleted by https://patchwork.libcamera.org/cover/23474/.

Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
---
 .../pipeline/rpi/common/pipeline_base.cpp     | 29 +++++++++++--------
 .../pipeline/rpi/common/pipeline_base.h       |  4 +--
 src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  8 ++---
 src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  8 ++---
 4 files changed, 27 insertions(+), 22 deletions(-)

Comments

Jacopo Mondi June 19, 2025, 1:57 p.m. UTC | #1
Hi Barnabás

if I'm not mistaken this patch can now be dropped, right ?

On Fri, Jun 06, 2025 at 06:41:53PM +0200, Barnabás Pőcze wrote:
> The rpi pipeline drops a certain number of initial frames. It uses
> `ControlList::clear()` to remove the metadata items of a request that
> was populated while processing a frame that is ultimately dropped.
> Since the decision to drop a frame only occurs at the very end of the
> processing, this stop-gap measure is introduced to delay metadata
> completion until the request is actually completed.
>
> Obsoleted by https://patchwork.libcamera.org/cover/23474/.
>
> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
> ---
>  .../pipeline/rpi/common/pipeline_base.cpp     | 29 +++++++++++--------
>  .../pipeline/rpi/common/pipeline_base.h       |  4 +--
>  src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  8 ++---
>  src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  8 ++---
>  4 files changed, 27 insertions(+), 22 deletions(-)
>
> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
> index 98507a152..d432cfb51 100644
> --- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
> +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
> @@ -780,7 +780,7 @@ int PipelineHandlerBase::queueRequestDevice(Camera *camera, Request *request)
>  	}
>
>  	/* Push the request to the back of the queue. */
> -	data->requestQueue_.push(request);
> +	data->requestQueue_.push({ request, {} });
>  	data->handleState();
>
>  	return 0;
> @@ -1232,8 +1232,8 @@ void CameraData::metadataReady(const ControlList &metadata)
>
>  	/* Add to the Request metadata buffer what the IPA has provided. */
>  	/* Last thing to do is to fill up the request metadata. */
> -	Request *request = requestQueue_.front();
> -	request->metadata().merge(metadata);
> +	ControlList &requestMetadata = requestQueue_.front().second;
> +	requestMetadata.merge(metadata);
>
>  	/*
>  	 * Inform the sensor of the latest colour gains if it has the
> @@ -1392,7 +1392,7 @@ void CameraData::clearIncompleteRequests()
>  	 * back to the application.
>  	 */
>  	while (!requestQueue_.empty()) {
> -		Request *request = requestQueue_.front();
> +		auto &[request, metadata] = requestQueue_.front();
>
>  		for (auto &b : request->buffers()) {
>  			FrameBuffer *buffer = b.second;
> @@ -1406,6 +1406,9 @@ void CameraData::clearIncompleteRequests()
>  			}
>  		}
>
> +		// TODO: need this when cancelled?
> +		request->metadata().merge(metadata);
> +
>  		pipe()->completeRequest(request);
>  		requestQueue_.pop();
>  	}
> @@ -1418,7 +1421,7 @@ void CameraData::handleStreamBuffer(FrameBuffer *buffer, RPi::Stream *stream)
>  	 * that we actually have one to action, otherwise we just return
>  	 * buffer back to the stream.
>  	 */
> -	Request *request = requestQueue_.empty() ? nullptr : requestQueue_.front();
> +	Request *request = requestQueue_.empty() ? nullptr : requestQueue_.front().first;
>  	if (!dropFrameCount_ && request && request->findBuffer(stream) == buffer) {
>  		/*
>  		 * Tag the buffer as completed, returning it to the
> @@ -1471,7 +1474,7 @@ void CameraData::checkRequestCompleted()
>  	 * change the state to IDLE when ready.
>  	 */
>  	if (!dropFrameCount_) {
> -		Request *request = requestQueue_.front();
> +		auto &[request, metadata] = requestQueue_.front();
>  		if (request->hasPendingBuffers())
>  			return;
>
> @@ -1482,6 +1485,8 @@ void CameraData::checkRequestCompleted()
>  		LOG(RPI, Debug) << "Completing request sequence: "
>  				<< request->sequence();
>
> +		request->metadata().merge(metadata);
> +
>  		pipe()->completeRequest(request);
>  		requestQueue_.pop();
>  		requestCompleted = true;
> @@ -1504,10 +1509,10 @@ void CameraData::checkRequestCompleted()
>  	}
>  }
>
> -void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request *request)
> +void CameraData::fillRequestMetadata(const ControlList &bufferControls, ControlList &metadata)
>  {
> -	request->metadata().set(controls::SensorTimestamp,
> -				bufferControls.get(controls::SensorTimestamp).value_or(0));
> +	metadata.set(controls::SensorTimestamp,
> +		     bufferControls.get(controls::SensorTimestamp).value_or(0));
>
>  	if (cropParams_.size()) {
>  		std::vector<Rectangle> crops;
> @@ -1515,10 +1520,10 @@ void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request
>  		for (auto const &[k, v] : cropParams_)
>  			crops.push_back(scaleIspCrop(v.ispCrop));
>
> -		request->metadata().set(controls::ScalerCrop, crops[0]);
> +		metadata.set(controls::ScalerCrop, crops[0]);
>  		if (crops.size() > 1) {
> -			request->metadata().set(controls::rpi::ScalerCrops,
> -						Span<const Rectangle>(crops.data(), crops.size()));
> +			metadata.set(controls::rpi::ScalerCrops,
> +				     Span<const Rectangle>(crops.data(), crops.size()));
>  		}
>  	}
>  }
> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h
> index aae0c2f35..d3a1bd216 100644
> --- a/src/libcamera/pipeline/rpi/common/pipeline_base.h
> +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.h
> @@ -129,7 +129,7 @@ public:
>  		return state_ != State::Stopped && state_ != State::Error;
>  	}
>
> -	std::queue<Request *> requestQueue_;
> +	std::queue<std::pair<Request *, ControlList>> requestQueue_;
>
>  	/* For handling digital zoom. */
>  	IPACameraSensorInfo sensorInfo_;
> @@ -179,7 +179,7 @@ public:
>
>  protected:
>  	void fillRequestMetadata(const ControlList &bufferControls,
> -				 Request *request);
> +				 ControlList &metadata);
>
>  	virtual void tryRunPipeline() = 0;
>
> diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
> index 91e7f4c94..8436cb4fa 100644
> --- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp
> +++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
> @@ -2318,7 +2318,7 @@ void PiSPCameraData::tryRunPipeline()
>  	CfeJob &job = cfeJobQueue_.front();
>
>  	/* Take the first request from the queue and action the IPA. */
> -	Request *request = requestQueue_.front();
> +	auto &[request, metadata] = requestQueue_.front();
>
>  	/* See if a new ScalerCrop value needs to be applied. */
>  	applyScalerCrop(request->controls());
> @@ -2328,8 +2328,8 @@ void PiSPCameraData::tryRunPipeline()
>  	 * related controls. We clear it first because the request metadata
>  	 * may have been populated if we have dropped the previous frame.
>  	 */
> -	request->metadata().clear();
> -	fillRequestMetadata(job.sensorControls, request);
> +	metadata.clear();
> +	fillRequestMetadata(job.sensorControls, metadata);
>
>  	/* Set our state to say the pipeline is active. */
>  	state_ = State::Busy;
> @@ -2347,7 +2347,7 @@ void PiSPCameraData::tryRunPipeline()
>  	params.buffers.bayer = RPi::MaskBayerData | bayerId;
>  	params.buffers.stats = RPi::MaskStats | statsId;
>  	params.buffers.embedded = 0;
> -	params.ipaContext = requestQueue_.front()->sequence();
> +	params.ipaContext = requestQueue_.front().first->sequence();
>  	params.delayContext = job.delayContext;
>  	params.sensorControls = std::move(job.sensorControls);
>  	params.requestControls = request->controls();
> diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
> index fe910bdf2..a311f43a4 100644
> --- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp
> +++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
> @@ -836,7 +836,7 @@ void Vc4CameraData::ispOutputDequeue(FrameBuffer *buffer)
>  	if (stream == &isp_[Isp::Stats]) {
>  		ipa::RPi::ProcessParams params;
>  		params.buffers.stats = index | RPi::MaskStats;
> -		params.ipaContext = requestQueue_.front()->sequence();
> +		params.ipaContext = requestQueue_.front().first->sequence();
>  		ipa_->processStats(params);
>  	} else {
>  		/* Any other ISP output can be handed back to the application now. */
> @@ -935,7 +935,7 @@ void Vc4CameraData::tryRunPipeline()
>  		return;
>
>  	/* Take the first request from the queue and action the IPA. */
> -	Request *request = requestQueue_.front();
> +	auto &[request, metadata] = requestQueue_.front();
>
>  	/* See if a new ScalerCrop value needs to be applied. */
>  	applyScalerCrop(request->controls());
> @@ -945,8 +945,8 @@ void Vc4CameraData::tryRunPipeline()
>  	 * related controls. We clear it first because the request metadata
>  	 * may have been populated if we have dropped the previous frame.
>  	 */
> -	request->metadata().clear();
> -	fillRequestMetadata(bayerFrame.controls, request);
> +	metadata.clear();
> +	fillRequestMetadata(bayerFrame.controls, metadata);
>
>  	/* Set our state to say the pipeline is active. */
>  	state_ = State::Busy;
> --
> 2.49.0
>
Barnabás Pőcze June 19, 2025, 1:57 p.m. UTC | #2
2025. 06. 19. 15:57 keltezéssel, Jacopo Mondi írta:
> Hi Barnabás
> 
> if I'm not mistaken this patch can now be dropped, right ?

Yes.


> 
> On Fri, Jun 06, 2025 at 06:41:53PM +0200, Barnabás Pőcze wrote:
>> The rpi pipeline drops a certain number of initial frames. It uses
>> `ControlList::clear()` to remove the metadata items of a request that
>> was populated while processing a frame that is ultimately dropped.
>> Since the decision to drop a frame only occurs at the very end of the
>> processing, this stop-gap measure is introduced to delay metadata
>> completion until the request is actually completed.
>>
>> Obsoleted by https://patchwork.libcamera.org/cover/23474/.
>>
>> Signed-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>
>> ---
>>   .../pipeline/rpi/common/pipeline_base.cpp     | 29 +++++++++++--------
>>   .../pipeline/rpi/common/pipeline_base.h       |  4 +--
>>   src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  8 ++---
>>   src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  8 ++---
>>   4 files changed, 27 insertions(+), 22 deletions(-)
>>
>> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
>> index 98507a152..d432cfb51 100644
>> --- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
>> +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
>> @@ -780,7 +780,7 @@ int PipelineHandlerBase::queueRequestDevice(Camera *camera, Request *request)
>>   	}
>>
>>   	/* Push the request to the back of the queue. */
>> -	data->requestQueue_.push(request);
>> +	data->requestQueue_.push({ request, {} });
>>   	data->handleState();
>>
>>   	return 0;
>> @@ -1232,8 +1232,8 @@ void CameraData::metadataReady(const ControlList &metadata)
>>
>>   	/* Add to the Request metadata buffer what the IPA has provided. */
>>   	/* Last thing to do is to fill up the request metadata. */
>> -	Request *request = requestQueue_.front();
>> -	request->metadata().merge(metadata);
>> +	ControlList &requestMetadata = requestQueue_.front().second;
>> +	requestMetadata.merge(metadata);
>>
>>   	/*
>>   	 * Inform the sensor of the latest colour gains if it has the
>> @@ -1392,7 +1392,7 @@ void CameraData::clearIncompleteRequests()
>>   	 * back to the application.
>>   	 */
>>   	while (!requestQueue_.empty()) {
>> -		Request *request = requestQueue_.front();
>> +		auto &[request, metadata] = requestQueue_.front();
>>
>>   		for (auto &b : request->buffers()) {
>>   			FrameBuffer *buffer = b.second;
>> @@ -1406,6 +1406,9 @@ void CameraData::clearIncompleteRequests()
>>   			}
>>   		}
>>
>> +		// TODO: need this when cancelled?
>> +		request->metadata().merge(metadata);
>> +
>>   		pipe()->completeRequest(request);
>>   		requestQueue_.pop();
>>   	}
>> @@ -1418,7 +1421,7 @@ void CameraData::handleStreamBuffer(FrameBuffer *buffer, RPi::Stream *stream)
>>   	 * that we actually have one to action, otherwise we just return
>>   	 * buffer back to the stream.
>>   	 */
>> -	Request *request = requestQueue_.empty() ? nullptr : requestQueue_.front();
>> +	Request *request = requestQueue_.empty() ? nullptr : requestQueue_.front().first;
>>   	if (!dropFrameCount_ && request && request->findBuffer(stream) == buffer) {
>>   		/*
>>   		 * Tag the buffer as completed, returning it to the
>> @@ -1471,7 +1474,7 @@ void CameraData::checkRequestCompleted()
>>   	 * change the state to IDLE when ready.
>>   	 */
>>   	if (!dropFrameCount_) {
>> -		Request *request = requestQueue_.front();
>> +		auto &[request, metadata] = requestQueue_.front();
>>   		if (request->hasPendingBuffers())
>>   			return;
>>
>> @@ -1482,6 +1485,8 @@ void CameraData::checkRequestCompleted()
>>   		LOG(RPI, Debug) << "Completing request sequence: "
>>   				<< request->sequence();
>>
>> +		request->metadata().merge(metadata);
>> +
>>   		pipe()->completeRequest(request);
>>   		requestQueue_.pop();
>>   		requestCompleted = true;
>> @@ -1504,10 +1509,10 @@ void CameraData::checkRequestCompleted()
>>   	}
>>   }
>>
>> -void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request *request)
>> +void CameraData::fillRequestMetadata(const ControlList &bufferControls, ControlList &metadata)
>>   {
>> -	request->metadata().set(controls::SensorTimestamp,
>> -				bufferControls.get(controls::SensorTimestamp).value_or(0));
>> +	metadata.set(controls::SensorTimestamp,
>> +		     bufferControls.get(controls::SensorTimestamp).value_or(0));
>>
>>   	if (cropParams_.size()) {
>>   		std::vector<Rectangle> crops;
>> @@ -1515,10 +1520,10 @@ void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request
>>   		for (auto const &[k, v] : cropParams_)
>>   			crops.push_back(scaleIspCrop(v.ispCrop));
>>
>> -		request->metadata().set(controls::ScalerCrop, crops[0]);
>> +		metadata.set(controls::ScalerCrop, crops[0]);
>>   		if (crops.size() > 1) {
>> -			request->metadata().set(controls::rpi::ScalerCrops,
>> -						Span<const Rectangle>(crops.data(), crops.size()));
>> +			metadata.set(controls::rpi::ScalerCrops,
>> +				     Span<const Rectangle>(crops.data(), crops.size()));
>>   		}
>>   	}
>>   }
>> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h
>> index aae0c2f35..d3a1bd216 100644
>> --- a/src/libcamera/pipeline/rpi/common/pipeline_base.h
>> +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.h
>> @@ -129,7 +129,7 @@ public:
>>   		return state_ != State::Stopped && state_ != State::Error;
>>   	}
>>
>> -	std::queue<Request *> requestQueue_;
>> +	std::queue<std::pair<Request *, ControlList>> requestQueue_;
>>
>>   	/* For handling digital zoom. */
>>   	IPACameraSensorInfo sensorInfo_;
>> @@ -179,7 +179,7 @@ public:
>>
>>   protected:
>>   	void fillRequestMetadata(const ControlList &bufferControls,
>> -				 Request *request);
>> +				 ControlList &metadata);
>>
>>   	virtual void tryRunPipeline() = 0;
>>
>> diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
>> index 91e7f4c94..8436cb4fa 100644
>> --- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp
>> +++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
>> @@ -2318,7 +2318,7 @@ void PiSPCameraData::tryRunPipeline()
>>   	CfeJob &job = cfeJobQueue_.front();
>>
>>   	/* Take the first request from the queue and action the IPA. */
>> -	Request *request = requestQueue_.front();
>> +	auto &[request, metadata] = requestQueue_.front();
>>
>>   	/* See if a new ScalerCrop value needs to be applied. */
>>   	applyScalerCrop(request->controls());
>> @@ -2328,8 +2328,8 @@ void PiSPCameraData::tryRunPipeline()
>>   	 * related controls. We clear it first because the request metadata
>>   	 * may have been populated if we have dropped the previous frame.
>>   	 */
>> -	request->metadata().clear();
>> -	fillRequestMetadata(job.sensorControls, request);
>> +	metadata.clear();
>> +	fillRequestMetadata(job.sensorControls, metadata);
>>
>>   	/* Set our state to say the pipeline is active. */
>>   	state_ = State::Busy;
>> @@ -2347,7 +2347,7 @@ void PiSPCameraData::tryRunPipeline()
>>   	params.buffers.bayer = RPi::MaskBayerData | bayerId;
>>   	params.buffers.stats = RPi::MaskStats | statsId;
>>   	params.buffers.embedded = 0;
>> -	params.ipaContext = requestQueue_.front()->sequence();
>> +	params.ipaContext = requestQueue_.front().first->sequence();
>>   	params.delayContext = job.delayContext;
>>   	params.sensorControls = std::move(job.sensorControls);
>>   	params.requestControls = request->controls();
>> diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
>> index fe910bdf2..a311f43a4 100644
>> --- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp
>> +++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
>> @@ -836,7 +836,7 @@ void Vc4CameraData::ispOutputDequeue(FrameBuffer *buffer)
>>   	if (stream == &isp_[Isp::Stats]) {
>>   		ipa::RPi::ProcessParams params;
>>   		params.buffers.stats = index | RPi::MaskStats;
>> -		params.ipaContext = requestQueue_.front()->sequence();
>> +		params.ipaContext = requestQueue_.front().first->sequence();
>>   		ipa_->processStats(params);
>>   	} else {
>>   		/* Any other ISP output can be handed back to the application now. */
>> @@ -935,7 +935,7 @@ void Vc4CameraData::tryRunPipeline()
>>   		return;
>>
>>   	/* Take the first request from the queue and action the IPA. */
>> -	Request *request = requestQueue_.front();
>> +	auto &[request, metadata] = requestQueue_.front();
>>
>>   	/* See if a new ScalerCrop value needs to be applied. */
>>   	applyScalerCrop(request->controls());
>> @@ -945,8 +945,8 @@ void Vc4CameraData::tryRunPipeline()
>>   	 * related controls. We clear it first because the request metadata
>>   	 * may have been populated if we have dropped the previous frame.
>>   	 */
>> -	request->metadata().clear();
>> -	fillRequestMetadata(bayerFrame.controls, request);
>> +	metadata.clear();
>> +	fillRequestMetadata(bayerFrame.controls, metadata);
>>
>>   	/* Set our state to say the pipeline is active. */
>>   	state_ = State::Busy;
>> --
>> 2.49.0
>>

Patch
diff mbox series

diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
index 98507a152..d432cfb51 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
@@ -780,7 +780,7 @@  int PipelineHandlerBase::queueRequestDevice(Camera *camera, Request *request)
 	}
 
 	/* Push the request to the back of the queue. */
-	data->requestQueue_.push(request);
+	data->requestQueue_.push({ request, {} });
 	data->handleState();
 
 	return 0;
@@ -1232,8 +1232,8 @@  void CameraData::metadataReady(const ControlList &metadata)
 
 	/* Add to the Request metadata buffer what the IPA has provided. */
 	/* Last thing to do is to fill up the request metadata. */
-	Request *request = requestQueue_.front();
-	request->metadata().merge(metadata);
+	ControlList &requestMetadata = requestQueue_.front().second;
+	requestMetadata.merge(metadata);
 
 	/*
 	 * Inform the sensor of the latest colour gains if it has the
@@ -1392,7 +1392,7 @@  void CameraData::clearIncompleteRequests()
 	 * back to the application.
 	 */
 	while (!requestQueue_.empty()) {
-		Request *request = requestQueue_.front();
+		auto &[request, metadata] = requestQueue_.front();
 
 		for (auto &b : request->buffers()) {
 			FrameBuffer *buffer = b.second;
@@ -1406,6 +1406,9 @@  void CameraData::clearIncompleteRequests()
 			}
 		}
 
+		// TODO: need this when cancelled?
+		request->metadata().merge(metadata);
+
 		pipe()->completeRequest(request);
 		requestQueue_.pop();
 	}
@@ -1418,7 +1421,7 @@  void CameraData::handleStreamBuffer(FrameBuffer *buffer, RPi::Stream *stream)
 	 * that we actually have one to action, otherwise we just return
 	 * buffer back to the stream.
 	 */
-	Request *request = requestQueue_.empty() ? nullptr : requestQueue_.front();
+	Request *request = requestQueue_.empty() ? nullptr : requestQueue_.front().first;
 	if (!dropFrameCount_ && request && request->findBuffer(stream) == buffer) {
 		/*
 		 * Tag the buffer as completed, returning it to the
@@ -1471,7 +1474,7 @@  void CameraData::checkRequestCompleted()
 	 * change the state to IDLE when ready.
 	 */
 	if (!dropFrameCount_) {
-		Request *request = requestQueue_.front();
+		auto &[request, metadata] = requestQueue_.front();
 		if (request->hasPendingBuffers())
 			return;
 
@@ -1482,6 +1485,8 @@  void CameraData::checkRequestCompleted()
 		LOG(RPI, Debug) << "Completing request sequence: "
 				<< request->sequence();
 
+		request->metadata().merge(metadata);
+
 		pipe()->completeRequest(request);
 		requestQueue_.pop();
 		requestCompleted = true;
@@ -1504,10 +1509,10 @@  void CameraData::checkRequestCompleted()
 	}
 }
 
-void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request *request)
+void CameraData::fillRequestMetadata(const ControlList &bufferControls, ControlList &metadata)
 {
-	request->metadata().set(controls::SensorTimestamp,
-				bufferControls.get(controls::SensorTimestamp).value_or(0));
+	metadata.set(controls::SensorTimestamp,
+		     bufferControls.get(controls::SensorTimestamp).value_or(0));
 
 	if (cropParams_.size()) {
 		std::vector<Rectangle> crops;
@@ -1515,10 +1520,10 @@  void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request
 		for (auto const &[k, v] : cropParams_)
 			crops.push_back(scaleIspCrop(v.ispCrop));
 
-		request->metadata().set(controls::ScalerCrop, crops[0]);
+		metadata.set(controls::ScalerCrop, crops[0]);
 		if (crops.size() > 1) {
-			request->metadata().set(controls::rpi::ScalerCrops,
-						Span<const Rectangle>(crops.data(), crops.size()));
+			metadata.set(controls::rpi::ScalerCrops,
+				     Span<const Rectangle>(crops.data(), crops.size()));
 		}
 	}
 }
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h
index aae0c2f35..d3a1bd216 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.h
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.h
@@ -129,7 +129,7 @@  public:
 		return state_ != State::Stopped && state_ != State::Error;
 	}
 
-	std::queue<Request *> requestQueue_;
+	std::queue<std::pair<Request *, ControlList>> requestQueue_;
 
 	/* For handling digital zoom. */
 	IPACameraSensorInfo sensorInfo_;
@@ -179,7 +179,7 @@  public:
 
 protected:
 	void fillRequestMetadata(const ControlList &bufferControls,
-				 Request *request);
+				 ControlList &metadata);
 
 	virtual void tryRunPipeline() = 0;
 
diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
index 91e7f4c94..8436cb4fa 100644
--- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp
+++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp
@@ -2318,7 +2318,7 @@  void PiSPCameraData::tryRunPipeline()
 	CfeJob &job = cfeJobQueue_.front();
 
 	/* Take the first request from the queue and action the IPA. */
-	Request *request = requestQueue_.front();
+	auto &[request, metadata] = requestQueue_.front();
 
 	/* See if a new ScalerCrop value needs to be applied. */
 	applyScalerCrop(request->controls());
@@ -2328,8 +2328,8 @@  void PiSPCameraData::tryRunPipeline()
 	 * related controls. We clear it first because the request metadata
 	 * may have been populated if we have dropped the previous frame.
 	 */
-	request->metadata().clear();
-	fillRequestMetadata(job.sensorControls, request);
+	metadata.clear();
+	fillRequestMetadata(job.sensorControls, metadata);
 
 	/* Set our state to say the pipeline is active. */
 	state_ = State::Busy;
@@ -2347,7 +2347,7 @@  void PiSPCameraData::tryRunPipeline()
 	params.buffers.bayer = RPi::MaskBayerData | bayerId;
 	params.buffers.stats = RPi::MaskStats | statsId;
 	params.buffers.embedded = 0;
-	params.ipaContext = requestQueue_.front()->sequence();
+	params.ipaContext = requestQueue_.front().first->sequence();
 	params.delayContext = job.delayContext;
 	params.sensorControls = std::move(job.sensorControls);
 	params.requestControls = request->controls();
diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
index fe910bdf2..a311f43a4 100644
--- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp
+++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
@@ -836,7 +836,7 @@  void Vc4CameraData::ispOutputDequeue(FrameBuffer *buffer)
 	if (stream == &isp_[Isp::Stats]) {
 		ipa::RPi::ProcessParams params;
 		params.buffers.stats = index | RPi::MaskStats;
-		params.ipaContext = requestQueue_.front()->sequence();
+		params.ipaContext = requestQueue_.front().first->sequence();
 		ipa_->processStats(params);
 	} else {
 		/* Any other ISP output can be handed back to the application now. */
@@ -935,7 +935,7 @@  void Vc4CameraData::tryRunPipeline()
 		return;
 
 	/* Take the first request from the queue and action the IPA. */
-	Request *request = requestQueue_.front();
+	auto &[request, metadata] = requestQueue_.front();
 
 	/* See if a new ScalerCrop value needs to be applied. */
 	applyScalerCrop(request->controls());
@@ -945,8 +945,8 @@  void Vc4CameraData::tryRunPipeline()
 	 * related controls. We clear it first because the request metadata
 	 * may have been populated if we have dropped the previous frame.
 	 */
-	request->metadata().clear();
-	fillRequestMetadata(bayerFrame.controls, request);
+	metadata.clear();
+	fillRequestMetadata(bayerFrame.controls, metadata);
 
 	/* Set our state to say the pipeline is active. */
 	state_ = State::Busy;