From patchwork Wed Dec 10 13:37:03 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 25455 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 7CBB4C326B for ; Wed, 10 Dec 2025 13:37:13 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id 7876A61463; Wed, 10 Dec 2025 14:37:12 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="YL7R62NM"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 7FA7E6143E for ; Wed, 10 Dec 2025 14:37:10 +0100 (CET) Received: from pb-laptop.local (185.221.143.114.nat.pool.zt.hu [185.221.143.114]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 3AF7210C4 for ; Wed, 10 Dec 2025 14:37:07 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1765373827; bh=cubgMvdtQ+KzRQ8LxYpxreucREysBuJ9Xb5azFQkSMI=; h=From:To:Subject:Date:In-Reply-To:References:From; b=YL7R62NMTtwW7lNcPU5dfuqh0QBJdPEfzRGXvkG2qsYaMkWxrYgA48fP+5yIluzcX v6uzWaMMWRka1zV7Spi/2iJC0pdkti9F06/8VB5kDqxw2c4XIrBZWJ+l1dueQ8Qado ePlBelMQ39gsRoOL3mjoPtJXPKKz5gGPvc8qFBww= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Subject: [RFC PATCH v1 1/2] libcamera: pipeline: uvcvideo: Report `FrameDuration` Date: Wed, 10 Dec 2025 14:37:03 +0100 Message-ID: <20251210133704.2711629-2-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.52.0 In-Reply-To: <20251210133704.2711629-1-barnabas.pocze@ideasonboard.com> References: <20251210133704.2711629-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" If available, query the time-per-frame parameter from the device after starting, and report it in the metadata. The reported frame duration remains constant during a particular streaming session, and will most likely not accurately reflect the actual frame duration for any given frame, depending on the manual exposure time, or if `V4L2_CID_EXPOSURE_AUTO_PRIORITY` or similar is in effect. But at least it shows the "intended" frame duration. Signed-off-by: Barnabás Pőcze --- include/libcamera/internal/v4l2_videodevice.h | 2 + src/libcamera/pipeline/uvcvideo/uvcvideo.cpp | 13 +++++ src/libcamera/v4l2_videodevice.cpp | 55 +++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/include/libcamera/internal/v4l2_videodevice.h b/include/libcamera/internal/v4l2_videodevice.h index 57db0036db..82d98184ed 100644 --- a/include/libcamera/internal/v4l2_videodevice.h +++ b/include/libcamera/internal/v4l2_videodevice.h @@ -208,6 +208,8 @@ public: int setFormat(V4L2DeviceFormat *format); Formats formats(uint32_t code = 0); + int getFrameInterval(std::chrono::microseconds *interval); + int getSelection(unsigned int target, Rectangle *rect); int setSelection(unsigned int target, Rectangle *rect); diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp index cb8cc82dff..3f98e8ece0 100644 --- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp +++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp @@ -60,6 +60,7 @@ public: std::optional autoExposureMode_; std::optional manualExposureMode_; + std::optional timePerFrame_; private: bool generateId(); @@ -295,6 +296,8 @@ int PipelineHandlerUVC::start(Camera *camera, const ControlList *controls) UVCCameraData *data = cameraData(camera); unsigned int count = data->stream_.configuration().bufferCount; + data->timePerFrame_.reset(); + int ret = data->video_->importBuffers(count); if (ret < 0) return ret; @@ -309,6 +312,13 @@ int PipelineHandlerUVC::start(Camera *camera, const ControlList *controls) if (ret < 0) goto err_release_buffers; + if (!data->timePerFrame_) { + std::chrono::microseconds interval; + ret = data->video_->getFrameInterval(&interval); + if (ret == 0) + data->timePerFrame_ = interval; + } + return 0; err_release_buffers: @@ -898,6 +908,9 @@ void UVCCameraData::imageBufferReady(FrameBuffer *buffer) request->metadata().set(controls::SensorTimestamp, buffer->metadata().timestamp); + if (timePerFrame_) + request->metadata().set(controls::FrameDuration, timePerFrame_->count()); + pipe()->completeBuffer(request, buffer); pipe()->completeRequest(request); } diff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp index 25b61d049a..3836dabef3 100644 --- a/src/libcamera/v4l2_videodevice.cpp +++ b/src/libcamera/v4l2_videodevice.cpp @@ -1147,6 +1147,61 @@ V4L2VideoDevice::Formats V4L2VideoDevice::formats(uint32_t code) return formats; } +namespace { + +std::chrono::microseconds +v4l2FractionToMs(const v4l2_fract &f) +{ + auto seconds = std::chrono::duration(f.numerator) / f.denominator; + return std::chrono::duration_cast(seconds); +} + +} + +/** + * \brief Retrieve the frame interval set on the V4L2 video device + * \param[out] interval The frame interval applied on the device + * + * Retrieve the current time-per-frame parameter from the device. + * + * \return 0 on success or a negative error code otherwise + */ +int V4L2VideoDevice::getFrameInterval(std::chrono::microseconds *interval) +{ + const v4l2_fract *frameInterval = nullptr; + v4l2_streamparm sparm = {}; + uint32_t caps = 0; + + sparm.type = bufferType_; + + int ret = ioctl(VIDIOC_G_PARM, &sparm); + if (ret) + return ret; + + switch (sparm.type) { + case V4L2_BUF_TYPE_VIDEO_CAPTURE: + case V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE: + frameInterval = &sparm.parm.capture.timeperframe; + caps = sparm.parm.capture.capability; + break; + case V4L2_BUF_TYPE_VIDEO_OUTPUT: + case V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE: + frameInterval = &sparm.parm.output.timeperframe; + caps = sparm.parm.output.capability; + break; + } + + if (!frameInterval) + return -EINVAL; + + if (!(caps & V4L2_CAP_TIMEPERFRAME)) + return -ENOTSUP; + + *interval = v4l2FractionToMs(*frameInterval); + + return 0; +} + std::vector V4L2VideoDevice::enumPixelformats(uint32_t code) { std::vector formats; From patchwork Wed Dec 10 13:37:04 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 25456 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id 54AE4C3257 for ; Wed, 10 Dec 2025 13:37:15 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id F3BE161471; Wed, 10 Dec 2025 14:37:14 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="BiNKgxAL"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [213.167.242.64]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id C353F6146D for ; Wed, 10 Dec 2025 14:37:10 +0100 (CET) Received: from pb-laptop.local (185.221.143.114.nat.pool.zt.hu [185.221.143.114]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 6FDCD10D4 for ; Wed, 10 Dec 2025 14:37:07 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1765373827; bh=u+RoPo6LEwoM+Wn5y5XnPVK3FTHPbpudV4186SjnjVc=; h=From:To:Subject:Date:In-Reply-To:References:From; b=BiNKgxALLfvg0rTWn3HNm78Jj4i/mM3yNqGEpcM5LXYkiQ0eMl7KOctTjN6ngLz68 tTOlaXHmwvtHgyox35Wmn2khiEZaEJRiWNWqJ171Ou85FybzheLqT0Ztv8wltU+M4P JFbfICQaRJivpgzXgV7d9qJjPqz8YvvsX+c8MlbQ= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Subject: [RFC PATCH v1 2/2] libcamera: pipeline: uvcvideo: Handle `FrameDurationLimits` Date: Wed, 10 Dec 2025 14:37:04 +0100 Message-ID: <20251210133704.2711629-3-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.52.0 In-Reply-To: <20251210133704.2711629-1-barnabas.pocze@ideasonboard.com> References: <20251210133704.2711629-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" Setting a frame rate is possible with UVC devices using `VIDIOC_S_PARM`. However, compared to other platforms supported by libcamera, limitations apply. Probably most crucially, the desired frame rate cannot be set while the camera is streaming. Furthermore, it is only a single value and not an allowed range. So this change only adds support for `FrameDurationLimits` in the control list passed to `Camera::start()`, and the control is otherwise ignored in requests. The kernel interface also only allows a single number and not a range, so the midpoint of the desired range is used. Checking the supplied values is not necessary since the kernel will adjust the value if it is not supported by the device. Initially the global min/max values are advertised in the `ControlInfo` of the `FrameDurationLimits` control, which are then updated after the camera is configured. Updating the control limits after configuration matches the behaviour of other platforms. While the kernel interface differentiates three types of frame intervals (discrete, continuous, stepwise), when querying the available frame intervals for a given (pixel format, size) combination, all options are evaluated and only the "local" minimum and maximum is used, as that is the only way the limits can reasonably be advertised on the libcamera side. Closes: https://gitlab.freedesktop.org/camera/libcamera/-/issues/296 Signed-off-by: Barnabás Pőcze --- include/libcamera/internal/v4l2_videodevice.h | 3 + src/libcamera/pipeline/uvcvideo/uvcvideo.cpp | 59 +++++++++ src/libcamera/v4l2_videodevice.cpp | 116 ++++++++++++++++++ 3 files changed, 178 insertions(+) diff --git a/include/libcamera/internal/v4l2_videodevice.h b/include/libcamera/internal/v4l2_videodevice.h index 82d98184ed..e97c0f9bf8 100644 --- a/include/libcamera/internal/v4l2_videodevice.h +++ b/include/libcamera/internal/v4l2_videodevice.h @@ -209,6 +209,9 @@ public: Formats formats(uint32_t code = 0); int getFrameInterval(std::chrono::microseconds *interval); + int setFrameInterval(std::chrono::microseconds *interval); + std::optional> + getFrameIntervalLimits(V4L2PixelFormat pixelFormat, Size size); int getSelection(unsigned int target, Rectangle *rect); int setSelection(unsigned int target, Rectangle *rect); diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp index 3f98e8ece0..e4e9b8ab9b 100644 --- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp +++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp @@ -57,6 +57,7 @@ public: std::unique_ptr video_; Stream stream_; std::map> formats_; + std::map, std::array> frameIntervals_; std::optional autoExposureMode_; std::optional manualExposureMode_; @@ -277,6 +278,22 @@ int PipelineHandlerUVC::configure(Camera *camera, CameraConfiguration *config) format.fourcc != data->video_->toV4L2PixelFormat(cfg.pixelFormat)) return -EINVAL; + auto it = data->controlInfo_.find(&controls::FrameDurationLimits); + if (it != data->controlInfo_.end()) { + auto it2 = data->frameIntervals_.find({ cfg.pixelFormat, cfg.size }); + if (it2 != data->frameIntervals_.end()) { + std::chrono::microseconds current; + + ret = data->video_->getFrameInterval(¤t); + + it->second = ControlInfo{ + int64_t(it2->second[0].count()), + int64_t(it2->second[1].count()), + ret == 0 ? ControlValue(int64_t(current.count())) : ControlValue() + }; + } + } + cfg.setStream(&data->stream_); return 0; @@ -306,6 +323,19 @@ int PipelineHandlerUVC::start(Camera *camera, const ControlList *controls) ret = processControls(data, *controls); if (ret < 0) goto err_release_buffers; + + /* Can only be set before starting. */ + auto fdl = controls->get(controls::FrameDurationLimits); + if (fdl) { + const auto wantMin = std::chrono::microseconds((*fdl)[0]); + const auto wantMax = std::chrono::microseconds((*fdl)[1]); + auto want = (wantMin + wantMax) / 2; + + /* Let the kernel choose something close to the middle. */ + ret = data->video_->setFrameInterval(&want); + if (ret == 0) + data->timePerFrame_ = want; + } } ret = data->video_->streamOn(); @@ -355,6 +385,8 @@ int PipelineHandlerUVC::processControl(const UVCCameraData *data, ControlList *c cid = V4L2_CID_GAMMA; else if (id == controls::AeEnable) return 0; /* Handled in `Camera::queueRequest()`. */ + else if (id == controls::FrameDurationLimits) + return 0; /* Handled in `start()` */ else return -EINVAL; @@ -555,6 +587,23 @@ int UVCCameraData::init(std::shared_ptr media) * resolution from the largest size it advertises. */ Size resolution; + auto minFrameInterval = std::chrono::microseconds::max(); + auto maxFrameInterval = std::chrono::microseconds::min(); + + const auto processFrameIntervals = [&](PixelFormat pf, V4L2PixelFormat v4l2pf, SizeRange size) { + if (size.min != size.max) + return; + + auto frameIntervals = video_->getFrameIntervalLimits(v4l2pf, size.min); + if (!frameIntervals) + return; + + minFrameInterval = std::min(minFrameInterval, (*frameIntervals)[0]); + maxFrameInterval = std::max(maxFrameInterval, (*frameIntervals)[1]); + + frameIntervals_.try_emplace({ pf, size.min }, *frameIntervals); + }; + for (const auto &format : video_->formats()) { PixelFormat pixelFormat = format.first.toPixelFormat(); if (!pixelFormat.isValid()) @@ -566,6 +615,8 @@ int UVCCameraData::init(std::shared_ptr media) for (const SizeRange &sizeRange : sizeRanges) { if (sizeRange.max > resolution) resolution = sizeRange.max; + + processFrameIntervals(pixelFormat, format.first, sizeRange); } } @@ -625,6 +676,14 @@ int UVCCameraData::init(std::shared_ptr media) ctrls[&controls::AeEnable] = ControlInfo(false, true, true); } + /* Use the global min/max here, limits will be updated in `configure()`. */ + if (!frameIntervals_.empty()) { + ctrls[&controls::FrameDurationLimits] = ControlInfo{ + int64_t(minFrameInterval.count()), + int64_t(maxFrameInterval.count()), + }; + } + controlInfo_ = ControlInfoMap(std::move(ctrls), controls::controls); /* diff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp index 3836dabef3..3db6e80aed 100644 --- a/src/libcamera/v4l2_videodevice.cpp +++ b/src/libcamera/v4l2_videodevice.cpp @@ -1202,6 +1202,122 @@ int V4L2VideoDevice::getFrameInterval(std::chrono::microseconds *interval) return 0; } +/** + * \brief Configure the frame interval + * \param[inout] interval The frame interval + * + * Apply the supplied \a interval as the time-per-frame stream parameter + * on the device, and return the actually applied value. + * + * \return 0 on success or a negative error code otherwise + */ +int V4L2VideoDevice::setFrameInterval(std::chrono::microseconds *interval) +{ + v4l2_fract *frameInterval = nullptr; + uint32_t *caps = nullptr; + v4l2_streamparm sparm = {}; + + sparm.type = bufferType_; + + switch (sparm.type) { + case V4L2_BUF_TYPE_VIDEO_CAPTURE: + case V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE: + frameInterval = &sparm.parm.capture.timeperframe; + caps = &sparm.parm.capture.capability; + break; + case V4L2_BUF_TYPE_VIDEO_OUTPUT: + case V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE: + frameInterval = &sparm.parm.output.timeperframe; + caps = &sparm.parm.output.capability; + break; + } + + if (!frameInterval) + return -EINVAL; + + constexpr auto max = std::numeric_limitsnumerator)>::max(); + if (interval->count() <= 0 || interval->count() > max) + return -EINVAL; + + frameInterval->numerator = interval->count(); + frameInterval->denominator = std::chrono::microseconds(std::chrono::seconds(1)).count(); + + int ret = ioctl(VIDIOC_S_PARM, &sparm); + if (ret) + return ret; + + if (!(*caps & V4L2_CAP_TIMEPERFRAME)) + return -ENOTSUP; + + *interval = v4l2FractionToMs(*frameInterval); + + return 0; +} + +/** + * \brief Retrieve the frame interval limits + * \param[in] pixelFormat The pixel format + * \param[in] size The size + * + * Retrieve the minimum and maximum available frame interval for + * the given \a pixelFormat and \a size. + * + * \return The min and max frame interval or std::nullopt otherwise + */ +std::optional> +V4L2VideoDevice::getFrameIntervalLimits(V4L2PixelFormat pixelFormat, Size size) +{ + auto min = std::chrono::microseconds::max(); + auto max = std::chrono::microseconds::min(); + unsigned int index = 0; + int ret; + + for (;; index++) { + struct v4l2_frmivalenum frameInterval = {}; + frameInterval.index = index; + frameInterval.pixel_format = pixelFormat; + frameInterval.width = size.width; + frameInterval.height = size.height; + + ret = ioctl(VIDIOC_ENUM_FRAMEINTERVALS, &frameInterval); + if (ret) + break; + + switch (frameInterval.type) { + case V4L2_FRMIVAL_TYPE_DISCRETE: { + auto ms = v4l2FractionToMs(frameInterval.discrete); + + min = std::min(min, ms); + max = std::max(max, ms); + break; + } + case V4L2_FRMIVAL_TYPE_CONTINUOUS: + case V4L2_FRMIVAL_TYPE_STEPWISE: { + min = std::min(min, v4l2FractionToMs(frameInterval.stepwise.min)); + max = std::max(max, v4l2FractionToMs(frameInterval.stepwise.max)); + break; + } + default: + LOG(V4L2, Error) + << "Unknown v4l2_frmsizetypes value " + << frameInterval.type; + return {}; + } + } + + if (ret && ret != -EINVAL) { + LOG(V4L2, Error) + << "Unable to enumerate pixel formats: " + << strerror(-ret); + return {}; + } + + if (index <= 0) + return {}; + + return {{ min, max }}; +} + std::vector V4L2VideoDevice::enumPixelformats(uint32_t code) { std::vector formats;