Show a patch.

GET /api/patches/25456/?format=api
HTTP 200 OK
Allow: GET, PUT, PATCH, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{
    "id": 25456,
    "url": "https://patchwork.libcamera.org/api/patches/25456/?format=api",
    "web_url": "https://patchwork.libcamera.org/patch/25456/",
    "project": {
        "id": 1,
        "url": "https://patchwork.libcamera.org/api/projects/1/?format=api",
        "name": "libcamera",
        "link_name": "libcamera",
        "list_id": "libcamera_core",
        "list_email": "libcamera-devel@lists.libcamera.org",
        "web_url": "",
        "scm_url": "",
        "webscm_url": ""
    },
    "msgid": "<20251210133704.2711629-3-barnabas.pocze@ideasonboard.com>",
    "date": "2025-12-10T13:37:04",
    "name": "[RFC,v1,2/2] libcamera: pipeline: uvcvideo: Handle `FrameDurationLimits`",
    "commit_ref": null,
    "pull_url": null,
    "state": "new",
    "archived": false,
    "hash": "0d029cf300f07fb602d25185261065b45cea7fab",
    "submitter": {
        "id": 216,
        "url": "https://patchwork.libcamera.org/api/people/216/?format=api",
        "name": "Barnabás Pőcze",
        "email": "barnabas.pocze@ideasonboard.com"
    },
    "delegate": null,
    "mbox": "https://patchwork.libcamera.org/patch/25456/mbox/",
    "series": [
        {
            "id": 5648,
            "url": "https://patchwork.libcamera.org/api/series/5648/?format=api",
            "web_url": "https://patchwork.libcamera.org/project/libcamera/list/?series=5648",
            "date": "2025-12-10T13:37:02",
            "name": "libcamera: pipeline: uvcvideo: FrameDurationLimits",
            "version": 1,
            "mbox": "https://patchwork.libcamera.org/series/5648/mbox/"
        }
    ],
    "comments": "https://patchwork.libcamera.org/api/patches/25456/comments/",
    "check": "pending",
    "checks": "https://patchwork.libcamera.org/api/patches/25456/checks/",
    "tags": {},
    "headers": {
        "Return-Path": "<libcamera-devel-bounces@lists.libcamera.org>",
        "X-Original-To": "parsemail@patchwork.libcamera.org",
        "Delivered-To": "parsemail@patchwork.libcamera.org",
        "Received": [
            "from lancelot.ideasonboard.com (lancelot.ideasonboard.com\n\t[92.243.16.209])\n\tby patchwork.libcamera.org (Postfix) with ESMTPS id 54AE4C3257\n\tfor <parsemail@patchwork.libcamera.org>;\n\tWed, 10 Dec 2025 13:37:15 +0000 (UTC)",
            "from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id F3BE161471;\n\tWed, 10 Dec 2025 14:37:14 +0100 (CET)",
            "from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[213.167.242.64])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id C353F6146D\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 10 Dec 2025 14:37:10 +0100 (CET)",
            "from pb-laptop.local (185.221.143.114.nat.pool.zt.hu\n\t[185.221.143.114])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 6FDCD10D4\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tWed, 10 Dec 2025 14:37:07 +0100 (CET)"
        ],
        "Authentication-Results": "lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"BiNKgxAL\"; dkim-atps=neutral",
        "DKIM-Signature": "v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1765373827;\n\tbh=u+RoPo6LEwoM+Wn5y5XnPVK3FTHPbpudV4186SjnjVc=;\n\th=From:To:Subject:Date:In-Reply-To:References:From;\n\tb=BiNKgxALLfvg0rTWn3HNm78Jj4i/mM3yNqGEpcM5LXYkiQ0eMl7KOctTjN6ngLz68\n\ttTOlaXHmwvtHgyox35Wmn2khiEZaEJRiWNWqJ171Ou85FybzheLqT0Ztv8wltU+M4P\n\tJFbfICQaRJivpgzXgV7d9qJjPqz8YvvsX+c8MlbQ=",
        "From": "=?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= <barnabas.pocze@ideasonboard.com>",
        "To": "libcamera-devel@lists.libcamera.org",
        "Subject": "[RFC PATCH v1 2/2] libcamera: pipeline: uvcvideo: Handle\n\t`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",
        "Content-Type": "text/plain; charset=UTF-8",
        "Content-Transfer-Encoding": "8bit",
        "X-BeenThere": "libcamera-devel@lists.libcamera.org",
        "X-Mailman-Version": "2.1.29",
        "Precedence": "list",
        "List-Id": "<libcamera-devel.lists.libcamera.org>",
        "List-Unsubscribe": "<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>",
        "List-Archive": "<https://lists.libcamera.org/pipermail/libcamera-devel/>",
        "List-Post": "<mailto:libcamera-devel@lists.libcamera.org>",
        "List-Help": "<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>",
        "List-Subscribe": "<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>",
        "Errors-To": "libcamera-devel-bounces@lists.libcamera.org",
        "Sender": "\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"
    },
    "content": "Setting a frame rate is possible with UVC devices using `VIDIOC_S_PARM`.\nHowever, compared to other platforms supported by libcamera, limitations\napply. Probably most crucially, the desired frame rate cannot be set\nwhile the camera is streaming. Furthermore, it is only a single value\nand not an allowed range.\n\nSo this change only adds support for `FrameDurationLimits` in the control\nlist passed to `Camera::start()`, and the control is otherwise ignored in\nrequests.\n\nThe kernel interface also only allows a single number and not a range,\nso the midpoint of the desired range is used. Checking the supplied\nvalues is not necessary since the kernel will adjust the value if\nit is not supported by the device.\n\nInitially the global min/max values are advertised in the `ControlInfo`\nof the `FrameDurationLimits` control, which are then updated after\nthe camera is configured. Updating the control limits after configuration\nmatches the behaviour of other platforms.\n\nWhile the kernel interface differentiates three types of frame intervals\n(discrete, continuous, stepwise), when querying the available frame intervals\nfor a given (pixel format, size) combination, all options are evaluated\nand only the \"local\" minimum and maximum is used, as that is the only way\nthe limits can reasonably be advertised on the libcamera side.\n\nCloses: https://gitlab.freedesktop.org/camera/libcamera/-/issues/296\nSigned-off-by: Barnabás Pőcze <barnabas.pocze@ideasonboard.com>\n---\n include/libcamera/internal/v4l2_videodevice.h |   3 +\n src/libcamera/pipeline/uvcvideo/uvcvideo.cpp  |  59 +++++++++\n src/libcamera/v4l2_videodevice.cpp            | 116 ++++++++++++++++++\n 3 files changed, 178 insertions(+)",
    "diff": "diff --git a/include/libcamera/internal/v4l2_videodevice.h b/include/libcamera/internal/v4l2_videodevice.h\nindex 82d98184ed..e97c0f9bf8 100644\n--- a/include/libcamera/internal/v4l2_videodevice.h\n+++ b/include/libcamera/internal/v4l2_videodevice.h\n@@ -209,6 +209,9 @@ public:\n \tFormats formats(uint32_t code = 0);\n \n \tint getFrameInterval(std::chrono::microseconds *interval);\n+\tint setFrameInterval(std::chrono::microseconds *interval);\n+\tstd::optional<std::array<std::chrono::microseconds, 2>>\n+\tgetFrameIntervalLimits(V4L2PixelFormat pixelFormat, Size size);\n \n \tint getSelection(unsigned int target, Rectangle *rect);\n \tint setSelection(unsigned int target, Rectangle *rect);\ndiff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp\nindex 3f98e8ece0..e4e9b8ab9b 100644\n--- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp\n+++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp\n@@ -57,6 +57,7 @@ public:\n \tstd::unique_ptr<V4L2VideoDevice> video_;\n \tStream stream_;\n \tstd::map<PixelFormat, std::vector<SizeRange>> formats_;\n+\tstd::map<std::pair<PixelFormat, Size>, std::array<std::chrono::microseconds, 2>> frameIntervals_;\n \n \tstd::optional<v4l2_exposure_auto_type> autoExposureMode_;\n \tstd::optional<v4l2_exposure_auto_type> manualExposureMode_;\n@@ -277,6 +278,22 @@ int PipelineHandlerUVC::configure(Camera *camera, CameraConfiguration *config)\n \t    format.fourcc != data->video_->toV4L2PixelFormat(cfg.pixelFormat))\n \t\treturn -EINVAL;\n \n+\tauto it = data->controlInfo_.find(&controls::FrameDurationLimits);\n+\tif (it != data->controlInfo_.end()) {\n+\t\tauto it2 = data->frameIntervals_.find({ cfg.pixelFormat, cfg.size });\n+\t\tif (it2 != data->frameIntervals_.end()) {\n+\t\t\tstd::chrono::microseconds current;\n+\n+\t\t\tret = data->video_->getFrameInterval(&current);\n+\n+\t\t\tit->second = ControlInfo{\n+\t\t\t\tint64_t(it2->second[0].count()),\n+\t\t\t\tint64_t(it2->second[1].count()),\n+\t\t\t\tret == 0 ? ControlValue(int64_t(current.count())) : ControlValue()\n+\t\t\t};\n+\t\t}\n+\t}\n+\n \tcfg.setStream(&data->stream_);\n \n \treturn 0;\n@@ -306,6 +323,19 @@ int PipelineHandlerUVC::start(Camera *camera, const ControlList *controls)\n \t\tret = processControls(data, *controls);\n \t\tif (ret < 0)\n \t\t\tgoto err_release_buffers;\n+\n+\t\t/* Can only be set before starting. */\n+\t\tauto fdl = controls->get(controls::FrameDurationLimits);\n+\t\tif (fdl) {\n+\t\t\tconst auto wantMin = std::chrono::microseconds((*fdl)[0]);\n+\t\t\tconst auto wantMax = std::chrono::microseconds((*fdl)[1]);\n+\t\t\tauto want = (wantMin + wantMax) / 2;\n+\n+\t\t\t/* Let the kernel choose something close to the middle. */\n+\t\t\tret = data->video_->setFrameInterval(&want);\n+\t\t\tif (ret == 0)\n+\t\t\t\tdata->timePerFrame_ = want;\n+\t\t}\n \t}\n \n \tret = data->video_->streamOn();\n@@ -355,6 +385,8 @@ int PipelineHandlerUVC::processControl(const UVCCameraData *data, ControlList *c\n \t\tcid = V4L2_CID_GAMMA;\n \telse if (id == controls::AeEnable)\n \t\treturn 0; /* Handled in `Camera::queueRequest()`. */\n+\telse if (id == controls::FrameDurationLimits)\n+\t\treturn 0; /* Handled in `start()` */\n \telse\n \t\treturn -EINVAL;\n \n@@ -555,6 +587,23 @@ int UVCCameraData::init(std::shared_ptr<MediaDevice> media)\n \t * resolution from the largest size it advertises.\n \t */\n \tSize resolution;\n+\tauto minFrameInterval = std::chrono::microseconds::max();\n+\tauto maxFrameInterval = std::chrono::microseconds::min();\n+\n+\tconst auto processFrameIntervals = [&](PixelFormat pf, V4L2PixelFormat v4l2pf, SizeRange size) {\n+\t\tif (size.min != size.max)\n+\t\t\treturn;\n+\n+\t\tauto frameIntervals = video_->getFrameIntervalLimits(v4l2pf, size.min);\n+\t\tif (!frameIntervals)\n+\t\t\treturn;\n+\n+\t\tminFrameInterval = std::min(minFrameInterval, (*frameIntervals)[0]);\n+\t\tmaxFrameInterval = std::max(maxFrameInterval, (*frameIntervals)[1]);\n+\n+\t\tframeIntervals_.try_emplace({ pf, size.min }, *frameIntervals);\n+\t};\n+\n \tfor (const auto &format : video_->formats()) {\n \t\tPixelFormat pixelFormat = format.first.toPixelFormat();\n \t\tif (!pixelFormat.isValid())\n@@ -566,6 +615,8 @@ int UVCCameraData::init(std::shared_ptr<MediaDevice> media)\n \t\tfor (const SizeRange &sizeRange : sizeRanges) {\n \t\t\tif (sizeRange.max > resolution)\n \t\t\t\tresolution = sizeRange.max;\n+\n+\t\t\tprocessFrameIntervals(pixelFormat, format.first, sizeRange);\n \t\t}\n \t}\n \n@@ -625,6 +676,14 @@ int UVCCameraData::init(std::shared_ptr<MediaDevice> media)\n \t\tctrls[&controls::AeEnable] = ControlInfo(false, true, true);\n \t}\n \n+\t/* Use the global min/max here, limits will be updated in `configure()`. */\n+\tif (!frameIntervals_.empty()) {\n+\t\tctrls[&controls::FrameDurationLimits] = ControlInfo{\n+\t\t\tint64_t(minFrameInterval.count()),\n+\t\t\tint64_t(maxFrameInterval.count()),\n+\t\t};\n+\t}\n+\n \tcontrolInfo_ = ControlInfoMap(std::move(ctrls), controls::controls);\n \n \t/*\ndiff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp\nindex 3836dabef3..3db6e80aed 100644\n--- a/src/libcamera/v4l2_videodevice.cpp\n+++ b/src/libcamera/v4l2_videodevice.cpp\n@@ -1202,6 +1202,122 @@ int V4L2VideoDevice::getFrameInterval(std::chrono::microseconds *interval)\n \treturn 0;\n }\n \n+/**\n+ * \\brief Configure the frame interval\n+ * \\param[inout] interval The frame interval\n+ *\n+ * Apply the supplied \\a interval as the time-per-frame stream parameter\n+ * on the device, and return the actually applied value.\n+ *\n+ * \\return 0 on success or a negative error code otherwise\n+ */\n+int V4L2VideoDevice::setFrameInterval(std::chrono::microseconds *interval)\n+{\n+\tv4l2_fract *frameInterval = nullptr;\n+\tuint32_t *caps = nullptr;\n+\tv4l2_streamparm sparm = {};\n+\n+\tsparm.type = bufferType_;\n+\n+\tswitch (sparm.type) {\n+\tcase V4L2_BUF_TYPE_VIDEO_CAPTURE:\n+\tcase V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE:\n+\t\tframeInterval = &sparm.parm.capture.timeperframe;\n+\t\tcaps = &sparm.parm.capture.capability;\n+\t\tbreak;\n+\tcase V4L2_BUF_TYPE_VIDEO_OUTPUT:\n+\tcase V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE:\n+\t\tframeInterval = &sparm.parm.output.timeperframe;\n+\t\tcaps = &sparm.parm.output.capability;\n+\t\tbreak;\n+\t}\n+\n+\tif (!frameInterval)\n+\t\treturn -EINVAL;\n+\n+\tconstexpr auto max = std::numeric_limits<decltype(frameInterval->numerator)>::max();\n+\tif (interval->count() <= 0 || interval->count() > max)\n+\t\treturn -EINVAL;\n+\n+\tframeInterval->numerator = interval->count();\n+\tframeInterval->denominator = std::chrono::microseconds(std::chrono::seconds(1)).count();\n+\n+\tint ret = ioctl(VIDIOC_S_PARM, &sparm);\n+\tif (ret)\n+\t\treturn ret;\n+\n+\tif (!(*caps & V4L2_CAP_TIMEPERFRAME))\n+\t\treturn -ENOTSUP;\n+\n+\t*interval = v4l2FractionToMs(*frameInterval);\n+\n+\treturn 0;\n+}\n+\n+/**\n+ * \\brief Retrieve the frame interval limits\n+ * \\param[in] pixelFormat The pixel format\n+ * \\param[in] size The size\n+ *\n+ * Retrieve the minimum and maximum available frame interval for\n+ * the given \\a pixelFormat and \\a size.\n+ *\n+ * \\return The min and max frame interval or std::nullopt otherwise\n+ */\n+std::optional<std::array<std::chrono::microseconds, 2>>\n+V4L2VideoDevice::getFrameIntervalLimits(V4L2PixelFormat pixelFormat, Size size)\n+{\n+\tauto min = std::chrono::microseconds::max();\n+\tauto max = std::chrono::microseconds::min();\n+\tunsigned int index = 0;\n+\tint ret;\n+\n+\tfor (;; index++) {\n+\t\tstruct v4l2_frmivalenum frameInterval = {};\n+\t\tframeInterval.index = index;\n+\t\tframeInterval.pixel_format = pixelFormat;\n+\t\tframeInterval.width = size.width;\n+\t\tframeInterval.height = size.height;\n+\n+\t\tret = ioctl(VIDIOC_ENUM_FRAMEINTERVALS, &frameInterval);\n+\t\tif (ret)\n+\t\t\tbreak;\n+\n+\t\tswitch (frameInterval.type) {\n+\t\tcase V4L2_FRMIVAL_TYPE_DISCRETE: {\n+\t\t\tauto ms = v4l2FractionToMs(frameInterval.discrete);\n+\n+\t\t\tmin = std::min(min, ms);\n+\t\t\tmax = std::max(max, ms);\n+\t\t\tbreak;\n+\t\t}\n+\t\tcase V4L2_FRMIVAL_TYPE_CONTINUOUS:\n+\t\tcase V4L2_FRMIVAL_TYPE_STEPWISE: {\n+\t\t\tmin = std::min(min, v4l2FractionToMs(frameInterval.stepwise.min));\n+\t\t\tmax = std::max(max, v4l2FractionToMs(frameInterval.stepwise.max));\n+\t\t\tbreak;\n+\t\t}\n+\t\tdefault:\n+\t\t\tLOG(V4L2, Error)\n+\t\t\t\t<< \"Unknown v4l2_frmsizetypes value \"\n+\t\t\t\t<< frameInterval.type;\n+\t\t\treturn {};\n+\t\t}\n+\t}\n+\n+\tif (ret && ret != -EINVAL) {\n+\t\tLOG(V4L2, Error)\n+\t\t\t<< \"Unable to enumerate pixel formats: \"\n+\t\t\t<< strerror(-ret);\n+\t\treturn {};\n+\t}\n+\n+\tif (index <= 0)\n+\t\treturn {};\n+\n+\treturn {{ min, max }};\n+}\n+\n std::vector<V4L2PixelFormat> V4L2VideoDevice::enumPixelformats(uint32_t code)\n {\n \tstd::vector<V4L2PixelFormat> formats;\n",
    "prefixes": [
        "RFC",
        "v1",
        "2/2"
    ]
}