{"id":26326,"url":"https://patchwork.libcamera.org/api/1.1/patches/26326/?format=json","web_url":"https://patchwork.libcamera.org/patch/26326/","project":{"id":1,"url":"https://patchwork.libcamera.org/api/1.1/projects/1/?format=json","name":"libcamera","link_name":"libcamera","list_id":"libcamera_core","list_email":"libcamera-devel@lists.libcamera.org","web_url":"","scm_url":"","webscm_url":""},"msgid":"<20260324151714.3345-4-david.plowman@raspberrypi.com>","date":"2026-03-24T14:32:09","name":"[v1,3/3] pipeline: rpi: Make control lists in requests properly atomic","commit_ref":null,"pull_url":null,"state":"superseded","archived":false,"hash":"39cba8b0f81ca7102178a100587a9cde793f9f61","submitter":{"id":42,"url":"https://patchwork.libcamera.org/api/1.1/people/42/?format=json","name":"David Plowman","email":"david.plowman@raspberrypi.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/26326/mbox/","series":[{"id":5842,"url":"https://patchwork.libcamera.org/api/1.1/series/5842/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5842","date":"2026-03-24T14:32:06","name":"Atomic control lists on Raspberry Pi","version":1,"mbox":"https://patchwork.libcamera.org/series/5842/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/26326/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/26326/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 E83C7C32F6\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 24 Mar 2026 15:17:24 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 795466278D;\n\tTue, 24 Mar 2026 16:17:24 +0100 (CET)","from mail-wm1-x32b.google.com (mail-wm1-x32b.google.com\n\t[IPv6:2a00:1450:4864:20::32b])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 8141762784\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 24 Mar 2026 16:17:20 +0100 (CET)","by mail-wm1-x32b.google.com with SMTP id\n\t5b1f17b1804b1-48374014a77so54137695e9.3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 24 Mar 2026 08:17:20 -0700 (PDT)","from davidp-pi5.pitowers.org\n\t([2a00:1098:3142:1f:88ea:c658:5b20:5e46])\n\tby smtp.gmail.com with ESMTPSA id\n\t5b1f17b1804b1-487116c44bfsm63826065e9.9.2026.03.24.08.17.18\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tTue, 24 Mar 2026 08:17:18 -0700 (PDT)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=raspberrypi.com header.i=@raspberrypi.com\n\theader.b=\"WJ3j3b12\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1774365439; x=1774970239;\n\tdarn=lists.libcamera.org; \n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=Felfuv/vx6wuAkWZ2K+rXbJMlM17aSgPLpgoMJipYjY=;\n\tb=WJ3j3b12kYbxenCgaf4BcYpuE2CKJTeGf7DyOD7C8lo32g0ULX2ZEepdlbPD6ruLRs\n\tYM8owPNlVAEmR9nAoZCKdYMoO+PWZMSWcDlPUqwvmoHd1+rRcAKS8TYRNBTcFw36uu3I\n\tPn1ZHRybDKMvLg7c2e0PCww8Y8krSjnSFxnkQ+lUbWxF7sGV1nos0uUkvIVdv9ejNaV4\n\tk1NTMi3i8AhBVOOnSaSaZvwLrBngqPzwIyOawEOgQp+9ecCuKAfunRwmjuOSjflQ7iWz\n\ts+DKVGwzzhBL6/sCJp8RoeVmyIbyQi4FlkbK39q3OD5/U85HBkb0MKfOSJWVJPxxufAT\n\tPceg==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20251104; t=1774365439; x=1774970239;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from\n\t:to:cc:subject:date:message-id:reply-to;\n\tbh=Felfuv/vx6wuAkWZ2K+rXbJMlM17aSgPLpgoMJipYjY=;\n\tb=rhHVLtngdF9x0isGrtd0K8FOiqWwgHtjYneVOjRahymFQpND+UMBPCv8PkP0ncy8K9\n\t1gjrvuC3sfUTNMKUkSfs/Wf6PNggT4I3pJNWtYhP/lbOBsTq8XCnHGwlT9oli3fThZzg\n\tteyhEdj1G0VwWB6h3bc13YyQZaHDdME147+DIq7aD9pboZJWAh40S48EWNWnYP3XFPlG\n\t/F5GcU+QEAXoM8QbxGPAH9IkPgElhcfv62O1VzMOPOzWmF+kwkukFhheEdD/PRUrS514\n\t7Ab5zpcypK8lacjsrGV8tRnEapageVCvb3Tl5xSPHqfDIIDBtHqye6S0MEc9Xy5/yBXC\n\te4qA==","X-Gm-Message-State":"AOJu0Yxk7hpAok1yplAUU0TuhNOB3VwIsFk1k1IbRvKa2rlHcRiRUylw\n\tubPW5O+HErvq9zJnxzjyjFmyPwpTl9WkVc6nNiUNF8xuBssW5L2WqTN9n++Usetgf0kHf44Mgsy\n\tuj1aD","X-Gm-Gg":"ATEYQzzyuybYypMrFBRamHF/2/ZlYfqrGiKM2cL72G/6JIMeZh4955JLdOF9D3eTTd6\n\tM4JiOW0AAw96EX+6s1zEzyQwy7seA2P44FQ6QVgymhnVpY1CB9+HgoHuYGBIZDQye9MwIEUPLpD\n\t5yLWhpUfB5G/tu8YGlBAz4L35l4rhRYzr/SdWG6wVMrynaxmjR+1bJHqPJfb56NTtYbC6Snud+d\n\tfplVkwzaJB/ZtB62tTV5pGl2ppRU0O+k/RY2552vRi6HTDKjk0osVhOj6REZbba52UD0K5Pvji+\n\tQLngkhyV/HqwMW7n7/VIrp0ECP5Ve6OPpFtwc7UrCbozpktTHge15yEh2iQF1etM2drlOHYgx/5\n\t035JKBXz5Q2R5wuwilfqiqbGOYHUztWok8VRP3UouUvzpqbqTpkkX1cJ25uhFjXIs8qzCwF3TK8\n\t/LDxpBi5P9BhqpECMgVqh5CS886l8TCm6+3EUlipWICWuYFSKI10sVRKZ1rMPyf+XSYlIiw3KFj\n\tNO2basp6JntY1sJAu/cSzGvBd4GBJgRNI9tPtXWaA==","X-Received":"by 2002:a05:600c:3ba1:b0:485:3ff1:d5c3 with SMTP id\n\t5b1f17b1804b1-48715fc322bmr2570915e9.5.1774365439283; \n\tTue, 24 Mar 2026 08:17:19 -0700 (PDT)","From":"David Plowman <david.plowman@raspberrypi.com>","To":"libcamera-devel@lists.libcamera.org","Cc":"David Plowman <david.plowman@raspberrypi.com>","Subject":"[PATCH v1 3/3] pipeline: rpi: Make control lists in requests\n\tproperly atomic","Date":"Tue, 24 Mar 2026 14:32:09 +0000","Message-ID":"<20260324151714.3345-4-david.plowman@raspberrypi.com>","X-Mailer":"git-send-email 2.47.3","In-Reply-To":"<20260324151714.3345-1-david.plowman@raspberrypi.com>","References":"<20260324151714.3345-1-david.plowman@raspberrypi.com>","MIME-Version":"1.0","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":"When a request is about to be processed, this commit separates\n\"delayed controls\" (camera-related ones that take \"a few frames\" to\napply) from \"immediate controls\" (ISP-related controls) that will\nhappen instantly.\n\nThe immediate controls are held back until the delayed controls that\nthey were submitted with, have happened. This means all the controls\nsubmitted in a request happen atomically.\n\nWe therefore already have the sequence number of the request whose\ncontrols have just been applied, so we additionally attach this to the\ncurrent request as \"ControlId\" metadata.\n\nSigned-off-by: David Plowman <david.plowman@raspberrypi.com>\n---\n src/ipa/rpi/common/ipa_base.cpp               | 24 ++++++----\n .../pipeline/rpi/common/pipeline_base.cpp     | 46 +++++++++++++++++++\n .../pipeline/rpi/common/pipeline_base.h       |  8 ++++\n src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  3 ++\n src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  3 ++\n 5 files changed, 76 insertions(+), 8 deletions(-)","diff":"diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp\nindex faa77719..dacafa57 100644\n--- a/src/ipa/rpi/common/ipa_base.cpp\n+++ b/src/ipa/rpi/common/ipa_base.cpp\n@@ -431,6 +431,15 @@ void IpaBase::prepareIsp(const PrepareParams &params)\n \trpiMetadata.clear();\n \tfillDeviceStatus(params.sensorControls, ipaContext);\n \n+\t/*\n+\t * When there are controls, it's important that we don't skip running the\n+\t * IPAs, as that can mess with synchronisation. Crucially though, we need\n+\t * to know whether there were controls when this comes back as the\n+\t * _delayed_ metadata, hence why we flag this in the metadata itself.\n+\t */\n+\tif (!params.requestControls.empty())\n+\t\trpiMetadata.set(\"ipa.request_controls\", true);\n+\n \tif (params.buffers.embedded) {\n \t\t/*\n \t\t * Pipeline handler has supplied us with an embedded data buffer,\n@@ -451,7 +460,7 @@ void IpaBase::prepareIsp(const PrepareParams &params)\n \t */\n \tAgcStatus agcStatus;\n \tbool hdrChange = false;\n-\tRPiController::Metadata &delayedMetadata = rpiMetadata_[params.delayContext];\n+\tRPiController::Metadata &delayedMetadata = rpiMetadata_[params.delayContext % rpiMetadata_.size()];\n \tif (!delayedMetadata.get<AgcStatus>(\"agc.status\", agcStatus)) {\n \t\trpiMetadata.set(\"agc.delayed_status\", agcStatus);\n \t\thdrChange = agcStatus.hdr.mode != hdrStatus_.mode;\n@@ -464,9 +473,13 @@ void IpaBase::prepareIsp(const PrepareParams &params)\n \t */\n \thelper_->prepare(embeddedBuffer, rpiMetadata);\n \n+\tbool delayedRequestControls = false;\n+\tdelayedMetadata.get<bool>(\"ipa.request_controls\", delayedRequestControls);\n+\n \t/* Allow a 10% margin on the comparison below. */\n \tDuration delta = (frameTimestamp - lastRunTimestamp_) * 1.0ns;\n-\tif (lastRunTimestamp_ && frameCount_ > invalidCount_ &&\n+\tif (!delayedRequestControls && params.requestControls.empty() &&\n+\t    lastRunTimestamp_ && frameCount_ > invalidCount_ &&\n \t    delta < controllerMinFrameDuration_ * 0.9 && !hdrChange) {\n \t\t/*\n \t\t * Ensure we merge the previous frame's metadata with the current\n@@ -535,7 +548,7 @@ void IpaBase::processStats(const ProcessParams &params)\n \t\tControlList ctrls(sensorCtrls_);\n \t\tapplyAGC(&agcStatus, ctrls);\n \t\trpiMetadata.set(\"agc.status\", agcStatus);\n-\t\tsetDelayedControls.emit(ctrls, ipaContext);\n+\t\tsetDelayedControls.emit(ctrls, params.ipaContext);\n \t\tsetCameraTimeoutValue();\n \t}\n \n@@ -951,8 +964,6 @@ void IpaBase::applyControls(const ControlList &controls)\n \n \t\t\t/* The control provides units of microseconds. */\n \t\t\tagc->setFixedExposureTime(0, ctrl.second.get<int32_t>() * 1.0us);\n-\n-\t\t\tlibcameraMetadata_.set(controls::ExposureTime, ctrl.second.get<int32_t>());\n \t\t\tbreak;\n \t\t}\n \n@@ -976,9 +987,6 @@ void IpaBase::applyControls(const ControlList &controls)\n \t\t\t\tbreak;\n \n \t\t\tagc->setFixedGain(0, ctrl.second.get<float>());\n-\n-\t\t\tlibcameraMetadata_.set(controls::AnalogueGain,\n-\t\t\t\t\t       ctrl.second.get<float>());\n \t\t\tbreak;\n \t\t}\n \ndiff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\nindex 867ecf1b..9e162870 100644\n--- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\n+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\n@@ -1528,4 +1528,50 @@ void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request\n \t}\n }\n \n+static bool isControlDelayed(unsigned int id)\n+{\n+\treturn id == controls::ExposureTime ||\n+\t       id == controls::AnalogueGain ||\n+\t       id == controls::FrameDurationLimits ||\n+\t       id == controls::AeEnable ||\n+\t       id == controls::ExposureTimeMode ||\n+\t       id == controls::AnalogueGainMode;\n+}\n+\n+void CameraData::handleControlLists(uint32_t delayContext)\n+{\n+\t/*\n+\t * THe delayContext is the sequence number after it's gone through the various\n+\t * pipeline delays, so that's what gets reported as the \"ControlId\" in the\n+\t * metadata, being the sequence number of the request whose ControlList has\n+\t * just been applied.\n+\t */\n+\tRequest *request = requestQueue_.front();\n+\trequest->_d()->metadata().set(controls::rpi::ControlId, delayContext);\n+\n+\t/*\n+\t * Controls that take effect immediately (typically ISP controls) have to be\n+\t * delayed so as to synchronise with those controls that do get delayed. So we\n+\t * must remove them from the current request, and push them onto a queue so\n+\t * that they can be used later.\n+\t */\n+\tControlList controls = std::move(request->controls());\n+\trequest->controls().clear();\n+\timmediateControls_.push({ request->sequence(), {} });\n+\tfor (const auto &ctrl : controls) {\n+\t\tif (isControlDelayed(ctrl.first))\n+\t\t\trequest->controls().set(ctrl.first, ctrl.second);\n+\t\telse\n+\t\t\timmediateControls_.back().controls.set(ctrl.first, ctrl.second);\n+\t}\n+\n+\t/* \"Immediate\" controls that have become due are now merged back into this request. */\n+\twhile (!immediateControls_.empty() &&\n+\t       immediateControls_.front().controlListId <= delayContext) {\n+\t\trequest->controls().merge(immediateControls_.front().controls,\n+\t\t\t\t\t  ControlList::MergePolicy::OverwriteExisting);\n+\t\timmediateControls_.pop();\n+\t}\n+}\n+\n } /* namespace libcamera */\ndiff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h\nindex 597eb587..65d8efdc 100644\n--- a/src/libcamera/pipeline/rpi/common/pipeline_base.h\n+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.h\n@@ -180,10 +180,18 @@ public:\n \n \tClockRecovery wallClockRecovery_;\n \n+\tstruct ImmediateControlsEntry {\n+\t\tuint64_t controlListId;\n+\t\tControlList controls;\n+\t};\n+\tstd::queue<ImmediateControlsEntry> immediateControls_;\n+\n protected:\n \tvoid fillRequestMetadata(const ControlList &bufferControls,\n \t\t\t\t Request *request);\n \n+\tvoid handleControlLists(uint32_t delayContext);\n+\n \tvirtual void tryRunPipeline() = 0;\n \n private:\ndiff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\nindex dff73a79..cc8aa4d4 100644\n--- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n+++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n@@ -2322,6 +2322,9 @@ void PiSPCameraData::tryRunPipeline()\n \n \tfillRequestMetadata(job.sensorControls, request);\n \n+\t/* This sorts out synchronisation with ControlLists in earlier requests. */\n+\thandleControlLists(job.delayContext);\n+\n \t/* Set our state to say the pipeline is active. */\n \tstate_ = State::Busy;\n \ndiff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\nindex b734889d..f743c8a7 100644\n--- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n+++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n@@ -939,6 +939,9 @@ void Vc4CameraData::tryRunPipeline()\n \n \tfillRequestMetadata(bayerFrame.controls, request);\n \n+\t/* This sorts out synchronisation with ControlLists in earlier requests. */\n+\thandleControlLists(bayerFrame.delayContext);\n+\n \t/* Set our state to say the pipeline is active. */\n \tstate_ = State::Busy;\n \n","prefixes":["v1","3/3"]}