[v4] virtual: Implement framerate handling
diff mbox series

Message ID 20261013204921.115407-1-robert.mader@collabora.com
State Superseded
Headers show
Series
  • [v4] virtual: Implement framerate handling
Related show

Commit Message

Robert Mader Oct. 13, 2026, 8:49 p.m. UTC
Throttle requests to the minimal frame duration in order to honor the
configured framerates.

Signed-off-by: Robert Mader <robert.mader@collabora.com>

---

Changes in v4:
 - Calculate minFrameDuration_ once in start() using FDLs from camera
   and, if present, from client provided controls. The later is not yet
   tested.
 - Assert that the camera has FDLs set by the config parser.
 - Set FrameDuration in request metadata.

Changes in v3:
 - Removed a leftover debug change

Changes in v2:
 - Remove debug log in order to avoid CI issues.
 - Simplify code in queueRequestDevice(), hopefully making it easier to
   follow.

Changes in v1 (from RFC version):
 - Use std::this_thread::sleep_for() instead usleep() (Barnabás)
 - Sleep only once per request, after generating all frames
 - Remove control handling in start() (defer that for later)
 - Reset state in stop() instead of start()
---
 src/libcamera/pipeline/virtual/virtual.cpp | 53 ++++++++++++++++++----
 src/libcamera/pipeline/virtual/virtual.h   |  3 ++
 2 files changed, 48 insertions(+), 8 deletions(-)

Comments

Robert Mader Oct. 13, 2026, 11:26 p.m. UTC | #1
Hi,

On 13.10.26 22:49, Robert Mader wrote:
> Throttle requests to the minimal frame duration in order to honor the
> configured framerates.
>
> Signed-off-by: Robert Mader <robert.mader@collabora.com>
>
> ---
>
> Changes in v4:
>   - Calculate minFrameDuration_ once in start() using FDLs from camera
>     and, if present, from client provided controls. The later is not yet
>     tested.

I just successfully tested it with the following Pipewire 
implementation: 
https://gitlab.freedesktop.org/pipewire/pipewire/-/merge_requests/3059

Various clients like Firefox and Snapshot request 30 FPS by default if 
no negotiation is possible. This patch, together with the PW MR above, 
make the virtual pipeline honor that, even if the config allows e.g. 60 FPS.

What's missing though is checking against the maximum frame duration 
values - i.e. clients can now request values lower than what the config 
file allows. Will fix that in v5

>   - Assert that the camera has FDLs set by the config parser.
>   - Set FrameDuration in request metadata.
>
> Changes in v3:
>   - Removed a leftover debug change
>
> Changes in v2:
>   - Remove debug log in order to avoid CI issues.
>   - Simplify code in queueRequestDevice(), hopefully making it easier to
>     follow.
>
> Changes in v1 (from RFC version):
>   - Use std::this_thread::sleep_for() instead usleep() (Barnabás)
>   - Sleep only once per request, after generating all frames
>   - Remove control handling in start() (defer that for later)
>   - Reset state in stop() instead of start()
> ---
>   src/libcamera/pipeline/virtual/virtual.cpp | 53 ++++++++++++++++++----
>   src/libcamera/pipeline/virtual/virtual.h   |  3 ++
>   2 files changed, 48 insertions(+), 8 deletions(-)
>
> diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
> index e25dd85a7391..442a793f82f0 100644
> --- a/src/libcamera/pipeline/virtual/virtual.cpp
> +++ b/src/libcamera/pipeline/virtual/virtual.cpp
> @@ -130,6 +130,9 @@ VirtualCameraData::VirtualCameraData(PipelineHandler *pipe,
>   
>   void VirtualCameraData::processRequest(Request *request)
>   {
> +	int64_t sensorTimestamp = *request->metadata().get(controls::SensorTimestamp);
> +	std::vector<FrameBuffer *> buffers;
> +
>   	for (const auto &[stream, buffer] : request->buffers()) {
>   		bool found = false;
>   		/* map buffer and fill test patterns */
> @@ -139,7 +142,7 @@ void VirtualCameraData::processRequest(Request *request)
>   
>   				fmd.status = FrameMetadata::Status::FrameSuccess;
>   				fmd.sequence = streamConfig.seq++;
> -				fmd.timestamp = currentTimestamp();
> +				fmd.timestamp = sensorTimestamp;
>   
>   				std::span<const FrameBuffer::Plane> planes = buffer->planes();
>   				for (const auto [i, p] : utils::enumerate(planes))
> @@ -151,12 +154,21 @@ void VirtualCameraData::processRequest(Request *request)
>   					    stream->configuration().size, buffer))
>   					fmd.status = FrameMetadata::Status::FrameError;
>   
> -				bufferCompleted.emit(buffer);
> +				buffers.push_back(buffer);
>   				break;
>   			}
>   		}
>   		ASSERT(found);
>   	}
> +
> +	const uint64_t now = currentTimestamp();
> +	if (now < static_cast<uint64_t>(sensorTimestamp)) {
> +		std::chrono::nanoseconds duration(sensorTimestamp - now);
> +		std::this_thread::sleep_for(duration);
> +	}
> +
> +	for (auto buffer : buffers)
> +		bufferCompleted.emit(buffer);
>   }
>   
>   VirtualCameraConfiguration::VirtualCameraConfiguration(VirtualCameraData *data)
> @@ -323,11 +335,24 @@ int PipelineHandlerVirtual::exportFrameBuffers([[maybe_unused]] Camera *camera,
>   	return dmaBufAllocator_.exportBuffers(config.bufferCount, planeSizes, buffers);
>   }
>   
> -int PipelineHandlerVirtual::start([[maybe_unused]] Camera *camera,
> -				  [[maybe_unused]] const ControlList *controls)
> +int PipelineHandlerVirtual::start(Camera *camera, const ControlList *controls)
>   {
>   	VirtualCameraData *data = cameraData(camera);
>   
> +	auto cameraFrameDurationLimits =
> +		camera->controls().find(&controls::FrameDurationLimits);
> +	ASSERT(cameraFrameDurationLimits != camera->controls().end());
> +	data->minFrameDuration_ =
> +		cameraFrameDurationLimits->second.min().get<int64_t>();
> +
> +	if (controls) {
> +		if (auto clientFrameDurationLimits =
> +			    controls->get(controls::FrameDurationLimits)) {
> +			data->minFrameDuration_ = std::max(data->minFrameDuration_,
> +							   clientFrameDurationLimits->front());
> +		}
> +	}
> +
>   	for (auto &s : data->streamConfigs_)
>   		s.seq = 0;
>   
> @@ -349,18 +374,30 @@ void PipelineHandlerVirtual::stopDevice(Camera *camera)
>   	/* Process pending `bufferCompleted` signals. */
>   	thread()->dispatchMessages(Message::Type::InvokeMessage, this);
>   	data->bufferCompleted.disconnect(this);
> +	data->minFrameDuration_ = 0;
> +	data->lastTimestamp_ = 0;
>   
>   	while (!data->queuedRequests_.empty())
>   		cancelRequest(data->queuedRequests_.front());
>   }
>   
> -int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera,
> -					       Request *request)
> +int PipelineHandlerVirtual::queueRequestDevice(Camera *camera, Request *request)
>   {
>   	VirtualCameraData *data = cameraData(camera);
> -	const auto timestamp = currentTimestamp();
>   
> -	request->_d()->metadata().set(controls::SensorTimestamp, timestamp);
> +	const uint64_t now = currentTimestamp();
> +	uint64_t targetTimestamp =
> +		data->lastTimestamp_ + data->minFrameDuration_ * 1000;
> +
> +	if (targetTimestamp < now)
> +		targetTimestamp = now;
> +	data->lastTimestamp_ = targetTimestamp;
> +
> +	request->_d()->metadata().set(controls::SensorTimestamp,
> +				      targetTimestamp);
> +	request->_d()->metadata().set(controls::FrameDuration,
> +				      data->minFrameDuration_);
> +
>   	data->invokeMethod(&VirtualCameraData::processRequest,
>   			   ConnectionTypeQueued, request);
>   
> diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h
> index 215e56fa3b9d..477f5b15a88f 100644
> --- a/src/libcamera/pipeline/virtual/virtual.h
> +++ b/src/libcamera/pipeline/virtual/virtual.h
> @@ -66,6 +66,9 @@ public:
>   
>   	std::vector<StreamConfig> streamConfigs_;
>   	Signal<FrameBuffer *> bufferCompleted;
> +
> +	int64_t minFrameDuration_ = 0;
> +	uint64_t lastTimestamp_ = 0;
>   };
>   
>   } /* namespace libcamera */
Kieran Bingham Oct. 14, 2026, 8:25 a.m. UTC | #2
Quoting Robert Mader (2026-10-13 21:49:21)
> Throttle requests to the minimal frame duration in order to honor the
> configured framerates.
> 
> Signed-off-by: Robert Mader <robert.mader@collabora.com>
> 
> ---
> 
> Changes in v4:
>  - Calculate minFrameDuration_ once in start() using FDLs from camera
>    and, if present, from client provided controls. The later is not yet
>    tested.
>  - Assert that the camera has FDLs set by the config parser.
>  - Set FrameDuration in request metadata.
> 
> Changes in v3:
>  - Removed a leftover debug change
> 
> Changes in v2:
>  - Remove debug log in order to avoid CI issues.
>  - Simplify code in queueRequestDevice(), hopefully making it easier to
>    follow.
> 
> Changes in v1 (from RFC version):
>  - Use std::this_thread::sleep_for() instead usleep() (Barnabás)
>  - Sleep only once per request, after generating all frames
>  - Remove control handling in start() (defer that for later)
>  - Reset state in stop() instead of start()
> ---
>  src/libcamera/pipeline/virtual/virtual.cpp | 53 ++++++++++++++++++----
>  src/libcamera/pipeline/virtual/virtual.h   |  3 ++
>  2 files changed, 48 insertions(+), 8 deletions(-)
> 
> diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
> index e25dd85a7391..442a793f82f0 100644
> --- a/src/libcamera/pipeline/virtual/virtual.cpp
> +++ b/src/libcamera/pipeline/virtual/virtual.cpp
> @@ -130,6 +130,9 @@ VirtualCameraData::VirtualCameraData(PipelineHandler *pipe,
>  
>  void VirtualCameraData::processRequest(Request *request)
>  {
> +       int64_t sensorTimestamp = *request->metadata().get(controls::SensorTimestamp);
> +       std::vector<FrameBuffer *> buffers;
> +
>         for (const auto &[stream, buffer] : request->buffers()) {
>                 bool found = false;
>                 /* map buffer and fill test patterns */
> @@ -139,7 +142,7 @@ void VirtualCameraData::processRequest(Request *request)
>  
>                                 fmd.status = FrameMetadata::Status::FrameSuccess;
>                                 fmd.sequence = streamConfig.seq++;
> -                               fmd.timestamp = currentTimestamp();
> +                               fmd.timestamp = sensorTimestamp;
>  
>                                 std::span<const FrameBuffer::Plane> planes = buffer->planes();
>                                 for (const auto [i, p] : utils::enumerate(planes))
> @@ -151,12 +154,21 @@ void VirtualCameraData::processRequest(Request *request)
>                                             stream->configuration().size, buffer))
>                                         fmd.status = FrameMetadata::Status::FrameError;
>  
> -                               bufferCompleted.emit(buffer);
> +                               buffers.push_back(buffer);
>                                 break;
>                         }
>                 }
>                 ASSERT(found);
>         }
> +
> +       const uint64_t now = currentTimestamp();
> +       if (now < static_cast<uint64_t>(sensorTimestamp)) {
> +               std::chrono::nanoseconds duration(sensorTimestamp - now);
> +               std::this_thread::sleep_for(duration);
> +       }
> +
> +       for (auto buffer : buffers)
> +               bufferCompleted.emit(buffer);
>  }
>  
>  VirtualCameraConfiguration::VirtualCameraConfiguration(VirtualCameraData *data)
> @@ -323,11 +335,24 @@ int PipelineHandlerVirtual::exportFrameBuffers([[maybe_unused]] Camera *camera,
>         return dmaBufAllocator_.exportBuffers(config.bufferCount, planeSizes, buffers);
>  }
>  
> -int PipelineHandlerVirtual::start([[maybe_unused]] Camera *camera,
> -                                 [[maybe_unused]] const ControlList *controls)
> +int PipelineHandlerVirtual::start(Camera *camera, const ControlList *controls)
>  {
>         VirtualCameraData *data = cameraData(camera);
>  
> +       auto cameraFrameDurationLimits =
> +               camera->controls().find(&controls::FrameDurationLimits);
> +       ASSERT(cameraFrameDurationLimits != camera->controls().end());

As far as I can tell this is somewhat guaranteed by the time we get
here, but no objection to it to make sure it stays that way.


> +       data->minFrameDuration_ =
> +               cameraFrameDurationLimits->second.min().get<int64_t>();
> +
> +       if (controls) {
> +               if (auto clientFrameDurationLimits =
> +                           controls->get(controls::FrameDurationLimits)) {
> +                       data->minFrameDuration_ = std::max(data->minFrameDuration_,
> +                                                          clientFrameDurationLimits->front());

This means data->minFrameDuration_ can only grow which might not be 100%
correct. We're into corner cases here but imagine this psuedo code:

	v = cm.Camera("Virtual");
	v.start({FrameDurationLimits(20000, 30000)});
	# data->minFrameDuration_ = 20ms
	v.run();
	v.stop();

	...

	v.start({FrameDurationLimits(10000, 15000)});

	# FrameDuration was clamped to 20ms as data->minFrameDuration_
	# is still 20ms,, but the user asked to test 10ms.

	v.run();
	v.stop();

So I think the clamp/std::max here should be against the values
registered in the ControlInfoMap.

kbingham@Monstersaurus:~/iob/libcamera/libcamera$ ./build/gcc/src/apps/cam/cam -c"Virtual1" --list-controls
Using camera Virtual1 as cam0
Control: [inout] draft::FaceDetectMode:
  - FaceDetectModeOff (0) [default]
Control: [inout] libcamera::FrameDurationLimits: [16666..16666]
   Size: 2


edit: I think I've just seen you've already noticed and that's for the
next v :-)

Otherwise this is looking good to me.





> +               }
> +       }
> +
>         for (auto &s : data->streamConfigs_)
>                 s.seq = 0;
>  
> @@ -349,18 +374,30 @@ void PipelineHandlerVirtual::stopDevice(Camera *camera)
>         /* Process pending `bufferCompleted` signals. */
>         thread()->dispatchMessages(Message::Type::InvokeMessage, this);
>         data->bufferCompleted.disconnect(this);
> +       data->minFrameDuration_ = 0;
> +       data->lastTimestamp_ = 0;
>  
>         while (!data->queuedRequests_.empty())
>                 cancelRequest(data->queuedRequests_.front());
>  }
>  
> -int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera,
> -                                              Request *request)
> +int PipelineHandlerVirtual::queueRequestDevice(Camera *camera, Request *request)
>  {
>         VirtualCameraData *data = cameraData(camera);
> -       const auto timestamp = currentTimestamp();
>  
> -       request->_d()->metadata().set(controls::SensorTimestamp, timestamp);
> +       const uint64_t now = currentTimestamp();
> +       uint64_t targetTimestamp =
> +               data->lastTimestamp_ + data->minFrameDuration_ * 1000;
> +
> +       if (targetTimestamp < now)
> +               targetTimestamp = now;
> +       data->lastTimestamp_ = targetTimestamp;
> +
> +       request->_d()->metadata().set(controls::SensorTimestamp,
> +                                     targetTimestamp);
> +       request->_d()->metadata().set(controls::FrameDuration,
> +                                     data->minFrameDuration_);
> +
>         data->invokeMethod(&VirtualCameraData::processRequest,
>                            ConnectionTypeQueued, request);
>  
> diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h
> index 215e56fa3b9d..477f5b15a88f 100644
> --- a/src/libcamera/pipeline/virtual/virtual.h
> +++ b/src/libcamera/pipeline/virtual/virtual.h
> @@ -66,6 +66,9 @@ public:
>  
>         std::vector<StreamConfig> streamConfigs_;
>         Signal<FrameBuffer *> bufferCompleted;
> +
> +       int64_t minFrameDuration_ = 0;
> +       uint64_t lastTimestamp_ = 0;
>  };
>  
>  } /* namespace libcamera */
> -- 
> 2.56.0
>
Robert Mader Oct. 15, 2026, 8:46 a.m. UTC | #3
Hi,

On 14.10.26 10:25, Kieran Bingham wrote:
> Quoting Robert Mader (2026-10-13 21:49:21)
>> Throttle requests to the minimal frame duration in order to honor the
>> configured framerates.
>>
>> Signed-off-by: Robert Mader <robert.mader@collabora.com>
>>
>> ---
>>
>> Changes in v4:
>>   - Calculate minFrameDuration_ once in start() using FDLs from camera
>>     and, if present, from client provided controls. The later is not yet
>>     tested.
>>   - Assert that the camera has FDLs set by the config parser.
>>   - Set FrameDuration in request metadata.
>>
>> Changes in v3:
>>   - Removed a leftover debug change
>>
>> Changes in v2:
>>   - Remove debug log in order to avoid CI issues.
>>   - Simplify code in queueRequestDevice(), hopefully making it easier to
>>     follow.
>>
>> Changes in v1 (from RFC version):
>>   - Use std::this_thread::sleep_for() instead usleep() (Barnabás)
>>   - Sleep only once per request, after generating all frames
>>   - Remove control handling in start() (defer that for later)
>>   - Reset state in stop() instead of start()
>> ---
>>   src/libcamera/pipeline/virtual/virtual.cpp | 53 ++++++++++++++++++----
>>   src/libcamera/pipeline/virtual/virtual.h   |  3 ++
>>   2 files changed, 48 insertions(+), 8 deletions(-)
>>
>> diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
>> index e25dd85a7391..442a793f82f0 100644
>> --- a/src/libcamera/pipeline/virtual/virtual.cpp
>> +++ b/src/libcamera/pipeline/virtual/virtual.cpp
>> @@ -130,6 +130,9 @@ VirtualCameraData::VirtualCameraData(PipelineHandler *pipe,
>>   
>>   void VirtualCameraData::processRequest(Request *request)
>>   {
>> +       int64_t sensorTimestamp = *request->metadata().get(controls::SensorTimestamp);
>> +       std::vector<FrameBuffer *> buffers;
>> +
>>          for (const auto &[stream, buffer] : request->buffers()) {
>>                  bool found = false;
>>                  /* map buffer and fill test patterns */
>> @@ -139,7 +142,7 @@ void VirtualCameraData::processRequest(Request *request)
>>   
>>                                  fmd.status = FrameMetadata::Status::FrameSuccess;
>>                                  fmd.sequence = streamConfig.seq++;
>> -                               fmd.timestamp = currentTimestamp();
>> +                               fmd.timestamp = sensorTimestamp;
>>   
>>                                  std::span<const FrameBuffer::Plane> planes = buffer->planes();
>>                                  for (const auto [i, p] : utils::enumerate(planes))
>> @@ -151,12 +154,21 @@ void VirtualCameraData::processRequest(Request *request)
>>                                              stream->configuration().size, buffer))
>>                                          fmd.status = FrameMetadata::Status::FrameError;
>>   
>> -                               bufferCompleted.emit(buffer);
>> +                               buffers.push_back(buffer);
>>                                  break;
>>                          }
>>                  }
>>                  ASSERT(found);
>>          }
>> +
>> +       const uint64_t now = currentTimestamp();
>> +       if (now < static_cast<uint64_t>(sensorTimestamp)) {
>> +               std::chrono::nanoseconds duration(sensorTimestamp - now);
>> +               std::this_thread::sleep_for(duration);
>> +       }
>> +
>> +       for (auto buffer : buffers)
>> +               bufferCompleted.emit(buffer);
>>   }
>>   
>>   VirtualCameraConfiguration::VirtualCameraConfiguration(VirtualCameraData *data)
>> @@ -323,11 +335,24 @@ int PipelineHandlerVirtual::exportFrameBuffers([[maybe_unused]] Camera *camera,
>>          return dmaBufAllocator_.exportBuffers(config.bufferCount, planeSizes, buffers);
>>   }
>>   
>> -int PipelineHandlerVirtual::start([[maybe_unused]] Camera *camera,
>> -                                 [[maybe_unused]] const ControlList *controls)
>> +int PipelineHandlerVirtual::start(Camera *camera, const ControlList *controls)
>>   {
>>          VirtualCameraData *data = cameraData(camera);
>>   
>> +       auto cameraFrameDurationLimits =
>> +               camera->controls().find(&controls::FrameDurationLimits);
>> +       ASSERT(cameraFrameDurationLimits != camera->controls().end());
> As far as I can tell this is somewhat guaranteed by the time we get
> here, but no objection to it to make sure it stays that way.
>
>
>> +       data->minFrameDuration_ =
>> +               cameraFrameDurationLimits->second.min().get<int64_t>();
>> +
>> +       if (controls) {
>> +               if (auto clientFrameDurationLimits =
>> +                           controls->get(controls::FrameDurationLimits)) {
>> +                       data->minFrameDuration_ = std::max(data->minFrameDuration_,
>> +                                                          clientFrameDurationLimits->front());
> This means data->minFrameDuration_ can only grow which might not be 100%
> correct. We're into corner cases here but imagine this psuedo code:
>
> 	v = cm.Camera("Virtual");
> 	v.start({FrameDurationLimits(20000, 30000)});
> 	# data->minFrameDuration_ = 20ms
> 	v.run();
> 	v.stop();
>
> 	...
>
> 	v.start({FrameDurationLimits(10000, 15000)});
>
> 	# FrameDuration was clamped to 20ms as data->minFrameDuration_
> 	# is still 20ms,, but the user asked to test 10ms.
>
> 	v.run();
> 	v.stop();
>
> So I think the clamp/std::max here should be against the values
> registered in the ControlInfoMap.
>
> kbingham@Monstersaurus:~/iob/libcamera/libcamera$ ./build/gcc/src/apps/cam/cam -c"Virtual1" --list-controls
> Using camera Virtual1 as cam0
> Control: [inout] draft::FaceDetectMode:
>    - FaceDetectModeOff (0) [default]
> Control: [inout] libcamera::FrameDurationLimits: [16666..16666]
>     Size: 2
>
>
> edit: I think I've just seen you've already noticed and that's for the
> next v :-)
Yep, just send out v5 with that change.
>
> Otherwise this is looking good to me.
🤞
>
>
>
>
>
>> +               }
>> +       }
>> +
>>          for (auto &s : data->streamConfigs_)
>>                  s.seq = 0;
>>   
>> @@ -349,18 +374,30 @@ void PipelineHandlerVirtual::stopDevice(Camera *camera)
>>          /* Process pending `bufferCompleted` signals. */
>>          thread()->dispatchMessages(Message::Type::InvokeMessage, this);
>>          data->bufferCompleted.disconnect(this);
>> +       data->minFrameDuration_ = 0;
>> +       data->lastTimestamp_ = 0;
>>   
>>          while (!data->queuedRequests_.empty())
>>                  cancelRequest(data->queuedRequests_.front());
>>   }
>>   
>> -int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera,
>> -                                              Request *request)
>> +int PipelineHandlerVirtual::queueRequestDevice(Camera *camera, Request *request)
>>   {
>>          VirtualCameraData *data = cameraData(camera);
>> -       const auto timestamp = currentTimestamp();
>>   
>> -       request->_d()->metadata().set(controls::SensorTimestamp, timestamp);
>> +       const uint64_t now = currentTimestamp();
>> +       uint64_t targetTimestamp =
>> +               data->lastTimestamp_ + data->minFrameDuration_ * 1000;
>> +
>> +       if (targetTimestamp < now)
>> +               targetTimestamp = now;
>> +       data->lastTimestamp_ = targetTimestamp;
>> +
>> +       request->_d()->metadata().set(controls::SensorTimestamp,
>> +                                     targetTimestamp);
>> +       request->_d()->metadata().set(controls::FrameDuration,
>> +                                     data->minFrameDuration_);
>> +
>>          data->invokeMethod(&VirtualCameraData::processRequest,
>>                             ConnectionTypeQueued, request);
>>   
>> diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h
>> index 215e56fa3b9d..477f5b15a88f 100644
>> --- a/src/libcamera/pipeline/virtual/virtual.h
>> +++ b/src/libcamera/pipeline/virtual/virtual.h
>> @@ -66,6 +66,9 @@ public:
>>   
>>          std::vector<StreamConfig> streamConfigs_;
>>          Signal<FrameBuffer *> bufferCompleted;
>> +
>> +       int64_t minFrameDuration_ = 0;
>> +       uint64_t lastTimestamp_ = 0;
>>   };
>>   
>>   } /* namespace libcamera */
>> -- 
>> 2.56.0
>>

Patch
diff mbox series

diff --git a/src/libcamera/pipeline/virtual/virtual.cpp b/src/libcamera/pipeline/virtual/virtual.cpp
index e25dd85a7391..442a793f82f0 100644
--- a/src/libcamera/pipeline/virtual/virtual.cpp
+++ b/src/libcamera/pipeline/virtual/virtual.cpp
@@ -130,6 +130,9 @@  VirtualCameraData::VirtualCameraData(PipelineHandler *pipe,
 
 void VirtualCameraData::processRequest(Request *request)
 {
+	int64_t sensorTimestamp = *request->metadata().get(controls::SensorTimestamp);
+	std::vector<FrameBuffer *> buffers;
+
 	for (const auto &[stream, buffer] : request->buffers()) {
 		bool found = false;
 		/* map buffer and fill test patterns */
@@ -139,7 +142,7 @@  void VirtualCameraData::processRequest(Request *request)
 
 				fmd.status = FrameMetadata::Status::FrameSuccess;
 				fmd.sequence = streamConfig.seq++;
-				fmd.timestamp = currentTimestamp();
+				fmd.timestamp = sensorTimestamp;
 
 				std::span<const FrameBuffer::Plane> planes = buffer->planes();
 				for (const auto [i, p] : utils::enumerate(planes))
@@ -151,12 +154,21 @@  void VirtualCameraData::processRequest(Request *request)
 					    stream->configuration().size, buffer))
 					fmd.status = FrameMetadata::Status::FrameError;
 
-				bufferCompleted.emit(buffer);
+				buffers.push_back(buffer);
 				break;
 			}
 		}
 		ASSERT(found);
 	}
+
+	const uint64_t now = currentTimestamp();
+	if (now < static_cast<uint64_t>(sensorTimestamp)) {
+		std::chrono::nanoseconds duration(sensorTimestamp - now);
+		std::this_thread::sleep_for(duration);
+	}
+
+	for (auto buffer : buffers)
+		bufferCompleted.emit(buffer);
 }
 
 VirtualCameraConfiguration::VirtualCameraConfiguration(VirtualCameraData *data)
@@ -323,11 +335,24 @@  int PipelineHandlerVirtual::exportFrameBuffers([[maybe_unused]] Camera *camera,
 	return dmaBufAllocator_.exportBuffers(config.bufferCount, planeSizes, buffers);
 }
 
-int PipelineHandlerVirtual::start([[maybe_unused]] Camera *camera,
-				  [[maybe_unused]] const ControlList *controls)
+int PipelineHandlerVirtual::start(Camera *camera, const ControlList *controls)
 {
 	VirtualCameraData *data = cameraData(camera);
 
+	auto cameraFrameDurationLimits =
+		camera->controls().find(&controls::FrameDurationLimits);
+	ASSERT(cameraFrameDurationLimits != camera->controls().end());
+	data->minFrameDuration_ =
+		cameraFrameDurationLimits->second.min().get<int64_t>();
+
+	if (controls) {
+		if (auto clientFrameDurationLimits =
+			    controls->get(controls::FrameDurationLimits)) {
+			data->minFrameDuration_ = std::max(data->minFrameDuration_,
+							   clientFrameDurationLimits->front());
+		}
+	}
+
 	for (auto &s : data->streamConfigs_)
 		s.seq = 0;
 
@@ -349,18 +374,30 @@  void PipelineHandlerVirtual::stopDevice(Camera *camera)
 	/* Process pending `bufferCompleted` signals. */
 	thread()->dispatchMessages(Message::Type::InvokeMessage, this);
 	data->bufferCompleted.disconnect(this);
+	data->minFrameDuration_ = 0;
+	data->lastTimestamp_ = 0;
 
 	while (!data->queuedRequests_.empty())
 		cancelRequest(data->queuedRequests_.front());
 }
 
-int PipelineHandlerVirtual::queueRequestDevice([[maybe_unused]] Camera *camera,
-					       Request *request)
+int PipelineHandlerVirtual::queueRequestDevice(Camera *camera, Request *request)
 {
 	VirtualCameraData *data = cameraData(camera);
-	const auto timestamp = currentTimestamp();
 
-	request->_d()->metadata().set(controls::SensorTimestamp, timestamp);
+	const uint64_t now = currentTimestamp();
+	uint64_t targetTimestamp =
+		data->lastTimestamp_ + data->minFrameDuration_ * 1000;
+
+	if (targetTimestamp < now)
+		targetTimestamp = now;
+	data->lastTimestamp_ = targetTimestamp;
+
+	request->_d()->metadata().set(controls::SensorTimestamp,
+				      targetTimestamp);
+	request->_d()->metadata().set(controls::FrameDuration,
+				      data->minFrameDuration_);
+
 	data->invokeMethod(&VirtualCameraData::processRequest,
 			   ConnectionTypeQueued, request);
 
diff --git a/src/libcamera/pipeline/virtual/virtual.h b/src/libcamera/pipeline/virtual/virtual.h
index 215e56fa3b9d..477f5b15a88f 100644
--- a/src/libcamera/pipeline/virtual/virtual.h
+++ b/src/libcamera/pipeline/virtual/virtual.h
@@ -66,6 +66,9 @@  public:
 
 	std::vector<StreamConfig> streamConfigs_;
 	Signal<FrameBuffer *> bufferCompleted;
+
+	int64_t minFrameDuration_ = 0;
+	uint64_t lastTimestamp_ = 0;
 };
 
 } /* namespace libcamera */