[{"id":38798,"web_url":"https://patchwork.libcamera.org/comment/38798/","msgid":"<af2YA7vMA_wlfOKb@zed>","date":"2026-05-08T08:15:54","subject":"Re: [PATCH v4 3/3] pipeline: rpi: Make control lists in requests\n\tproperly atomic","submitter":{"id":143,"url":"https://patchwork.libcamera.org/api/people/143/","name":"Jacopo Mondi","email":"jacopo.mondi@ideasonboard.com"},"content":"Hi David\n\nOn Thu, May 07, 2026 at 01:20:36PM +0100, David Plowman wrote:\n> When a request is about to be processed, this commit separates\n> \"delayed controls\" (camera-related ones that take \"a few frames\" to\n> apply) from \"immediate controls\" (ISP-related controls) that will\n> happen instantly. It does so in a copy of the request's control list,\n> so as to avoid disturbing the request's original version.\n>\n> The immediate controls are held back until the delayed controls that\n> they were submitted with, have happened. This means all the controls\n> submitted in a request happen atomically.\n>\n> We therefore already have the sequence number of the request whose\n> controls have just been applied, so we additionally attach this to the\n> current request as \"ControlId\" metadata.\n>\n> Signed-off-by: David Plowman <david.plowman@raspberrypi.com>\n> Reviewed-by: Naushir Patuck <naush@raspberrypi.com>\n> ---\n>  src/ipa/rpi/common/ipa_base.cpp               | 24 ++++++---\n>  .../pipeline/rpi/common/pipeline_base.cpp     | 49 +++++++++++++++++++\n>  .../pipeline/rpi/common/pipeline_base.h       |  9 ++++\n>  src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  9 ++--\n>  src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  9 ++--\n>  5 files changed, 86 insertions(+), 14 deletions(-)\n>\n> diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp\n> index 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>\n> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\n> index f6ef8a3b..0c82f549 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,53 @@ 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, ControlList &requestControls)\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 \"ControlListSequence\"\n> +\t * in the metadata, being the sequence number of the request whose ControlList\n> +\t * has just been applied.\n> +\t */\n> +\tRequest *request = requestQueue_.front();\n> +\trequest->_d()->metadata().set(controls::rpi::ControlListSequence, 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> +\t * Note that we are passed a copy of the request's controls, so that we can\n> +\t * avoid disturbing what was in the original request.\n> +\t */\n> +\tControlList controls = std::move(requestControls);\n\nDoes ControlList have a move() assignment operator ?\nIs this going through a plain copy ?\n\n> +\trequestControls.clear();\n\nProbably so, otherwise a proper move() would have emptied\nrequestControls.\n\nSo this makes two copies overall\n\n        params.requestControls = request->controls();\n        in the caller\n\n        ControlList controls = std::move(requestControls);\n        here\n\nYou could probably pass to this function\n\n        request->controls() as a const by reference\n        params.requestControls as mutable by pointer\n\nand have here\n\n        params.requestControls->clear();\n\n> +\timmediateControls_.push({ request->sequence(), {} });\n> +\tfor (const auto &ctrl : controls) {\n\n\tfor (const auto &ctrl : request->controls()) {\n\n> +\t\tif (isControlDelayed(ctrl.first))\n> +\t\t\trequestControls.set(ctrl.first, ctrl.second);\n                        params.requestControls->set()\n\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\trequestControls.merge(immediateControls_.front().controls,\n\n                params.requestControls->merge();\n\n> +\t\t\t\t      ControlList::MergePolicy::OverwriteExisting);\n> +\t\timmediateControls_.pop();\n> +\t}\n> +}\n> +\n\nThis would be the resulting patch\n\ndiff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\nindex 0c82f549675b..0318cf55a7b2 100644\n--- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\n+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\n@@ -1538,7 +1538,9 @@ static bool isControlDelayed(unsigned int id)\n               id == controls::AnalogueGainMode;\n }\n\n-void CameraData::handleControlLists(uint32_t delayContext, ControlList &requestControls)\n+void CameraData::handleControlLists(uint32_t delayContext,\n+                                   const ControlList &requestControls,\n+                                   ControlList *paramsControls)\n {\n        /*\n         * The delayContext is the sequence number after it's gone through the various\n@@ -1558,12 +1560,11 @@ void CameraData::handleControlLists(uint32_t delayContext, ControlList &requestC\n         * Note that we are passed a copy of the request's controls, so that we can\n         * avoid disturbing what was in the original request.\n         */\n-       ControlList controls = std::move(requestControls);\n-       requestControls.clear();\n        immediateControls_.push({ request->sequence(), {} });\n-       for (const auto &ctrl : controls) {\n+       paramsControls->clear();\n+       for (const auto &ctrl : requestControls) {\n                if (isControlDelayed(ctrl.first))\n-                       requestControls.set(ctrl.first, ctrl.second);\n+                       paramsControls->set(ctrl.first, ctrl.second);\n                else\n                        immediateControls_.back().controls.set(ctrl.first, ctrl.second);\n        }\n@@ -1571,7 +1572,7 @@ void CameraData::handleControlLists(uint32_t delayContext, ControlList &requestC\n        /* \"Immediate\" controls that have become due are now merged back into this request. */\n        while (!immediateControls_.empty() &&\n               immediateControls_.front().controlListId <= delayContext) {\n-               requestControls.merge(immediateControls_.front().controls,\n+               paramsControls->merge(immediateControls_.front().controls,\n                                      ControlList::MergePolicy::OverwriteExisting);\n                immediateControls_.pop();\n        }\ndiff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h\nindex 134262b66a04..856aa4f632b4 100644\n--- a/src/libcamera/pipeline/rpi/common/pipeline_base.h\n+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.h\n@@ -191,7 +191,8 @@ protected:\n                                 Request *request);\n\n        void handleControlLists(uint32_t delayContext,\n-                               ControlList &requestControls);\n+                               const ControlList &requestControls,\n+                               ControlList *paramsControls);\n\n        virtual void tryRunPipeline() = 0;\n\ndiff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\nindex 690d25118aba..4d2db33cd144 100644\n--- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n+++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n@@ -2338,10 +2338,9 @@ void PiSPCameraData::tryRunPipeline()\n        params.ipaContext = requestQueue_.front()->sequence();\n        params.delayContext = job.delayContext;\n        params.sensorControls = std::move(job.sensorControls);\n-       params.requestControls = request->controls();\n\n        /* This sorts out synchronisation with ControlLists in earlier requests. */\n-       handleControlLists(job.delayContext, params.requestControls);\n+       handleControlLists(job.delayContext, request->controls(), &params.requestControls);\n\n        /* Set our state to say the pipeline is active. */\n        state_ = State::Busy;\ndiff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\nindex 28e7ad4698bf..f010c877a927 100644\n--- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n+++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n@@ -947,13 +947,12 @@ void Vc4CameraData::tryRunPipeline()\n        ipa::RPi::PrepareParams params;\n        params.buffers.bayer = RPi::MaskBayerData | bayer;\n        params.sensorControls = std::move(bayerFrame.controls);\n-       params.requestControls = request->controls();\n        params.ipaContext = request->sequence();\n        params.delayContext = bayerFrame.delayContext;\n        params.buffers.embedded = 0;\n\n        /* This sorts out synchronisation with ControlLists in earlier requests. */\n-       handleControlLists(bayerFrame.delayContext, params.requestControls);\n+       handleControlLists(bayerFrame.delayContext, request->controls(), &params.requestControls);\n\n        /* Set our state to say the pipeline is active. */\n        state_ = State::Busy;\n\nI admit I have onlt compiled it though but this should avoid the two\ncopies ?\n\n\n>  } /* namespace libcamera */\n> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h\n> index 7bfac33e..134262b6 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,19 @@ 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> +\t\t\t\tControlList &requestControls);\n> +\n>  \tvirtual void tryRunPipeline() = 0;\n>\n>  private:\n> diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n> index cc7e32c3..690d2511 100644\n> --- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n> +++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n> @@ -2322,9 +2322,6 @@ void PiSPCameraData::tryRunPipeline()\n>\n>  \tfillRequestMetadata(job.sensorControls, request);\n>\n> -\t/* Set our state to say the pipeline is active. */\n> -\tstate_ = State::Busy;\n> -\n>  \tunsigned int bayerId = cfe_[Cfe::Output0].getBufferId(job.buffers[&cfe_[Cfe::Output0]]);\n>  \tunsigned int statsId = cfe_[Cfe::Stats].getBufferId(job.buffers[&cfe_[Cfe::Stats]]);\n>  \tASSERT(bayerId && statsId);\n> @@ -2343,6 +2340,12 @@ void PiSPCameraData::tryRunPipeline()\n>  \tparams.sensorControls = std::move(job.sensorControls);\n>  \tparams.requestControls = request->controls();\n>\n> +\t/* This sorts out synchronisation with ControlLists in earlier requests. */\n> +\thandleControlLists(job.delayContext, params.requestControls);\n> +\n> +\t/* Set our state to say the pipeline is active. */\n> +\tstate_ = State::Busy;\n> +\n>  \tif (sensorMetadata_) {\n>  \t\tunsigned int embeddedId =\n>  \t\t\tcfe_[Cfe::Embedded].getBufferId(job.buffers[&cfe_[Cfe::Embedded]]);\n> diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n> index f99cfdbc..28e7ad46 100644\n> --- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n> +++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n> @@ -939,9 +939,6 @@ void Vc4CameraData::tryRunPipeline()\n>\n>  \tfillRequestMetadata(bayerFrame.controls, request);\n>\n> -\t/* Set our state to say the pipeline is active. */\n> -\tstate_ = State::Busy;\n> -\n>  \tunsigned int bayer = unicam_[Unicam::Image].getBufferId(bayerFrame.buffer);\n>\n>  \tLOG(RPI, Debug) << \"Signalling prepareIsp:\"\n> @@ -955,6 +952,12 @@ void Vc4CameraData::tryRunPipeline()\n>  \tparams.delayContext = bayerFrame.delayContext;\n>  \tparams.buffers.embedded = 0;\n>\n> +\t/* This sorts out synchronisation with ControlLists in earlier requests. */\n> +\thandleControlLists(bayerFrame.delayContext, params.requestControls);\n> +\n> +\t/* Set our state to say the pipeline is active. */\n> +\tstate_ = State::Busy;\n> +\n\nWith or without the proposed patch, as this is ultimately your\npipeline code:\nReviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>\n\n>  \tif (embeddedBuffer) {\n>  \t\tunsigned int embeddedId = unicam_[Unicam::Embedded].getBufferId(embeddedBuffer);\n>\n> --\n> 2.47.3\n>","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 3C3CABE173\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  8 May 2026 08:16:00 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 1C0EF62FEC;\n\tFri,  8 May 2026 10:15:59 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 5ED2F62E6A\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  8 May 2026 10:15:57 +0200 (CEST)","from ideasonboard.com (net-93-65-100-155.cust.vodafonedsl.it\n\t[93.65.100.155])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id AD069ABF;\n\tFri,  8 May 2026 10:15:52 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"cNoa48go\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1778228152;\n\tbh=JOgkVenw0piF5jq7SllJAz73bN7gutssBNiB7D7vn+Q=;\n\th=Date:From:To:Cc:Subject:References:In-Reply-To:From;\n\tb=cNoa48goQuyHH0kaETAG2DfoaDaEgrlxW8YXYmHMWiuraiF+7mqLjhmccIF04hXbY\n\tdMzdQrP0P0tUolmwSHuhmGnu6F5I6+xzfLnp08/s/Q4CRzx9LNY89uG5sS6AHNfBka\n\tIs8k0Qh1T/19zvhVE0PDwDQU9F24LF4MOUQtGo3M=","Date":"Fri, 8 May 2026 10:15:54 +0200","From":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>","To":"David Plowman <david.plowman@raspberrypi.com>","Cc":"libcamera-devel@lists.libcamera.org, \n\tNaushir Patuck <naush@raspberrypi.com>","Subject":"Re: [PATCH v4 3/3] pipeline: rpi: Make control lists in requests\n\tproperly atomic","Message-ID":"<af2YA7vMA_wlfOKb@zed>","References":"<20260507125458.12140-1-david.plowman@raspberrypi.com>\n\t<20260507125458.12140-4-david.plowman@raspberrypi.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","In-Reply-To":"<20260507125458.12140-4-david.plowman@raspberrypi.com>","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>"}},{"id":38820,"web_url":"https://patchwork.libcamera.org/comment/38820/","msgid":"<CAHW6GY+9SELq0Yq99ZjveW4NUYCaP3GuLRWD_x3nxG9iaFSG6Q@mail.gmail.com>","date":"2026-05-08T11:42:39","subject":"Re: [PATCH v4 3/3] pipeline: rpi: Make control lists in requests\n\tproperly atomic","submitter":{"id":42,"url":"https://patchwork.libcamera.org/api/people/42/","name":"David Plowman","email":"david.plowman@raspberrypi.com"},"content":"Hi Jacopo\n\nThanks for the review.\n\nOn Fri, 8 May 2026 at 09:15, Jacopo Mondi <jacopo.mondi@ideasonboard.com> wrote:\n>\n> Hi David\n>\n> On Thu, May 07, 2026 at 01:20:36PM +0100, David Plowman wrote:\n> > When a request is about to be processed, this commit separates\n> > \"delayed controls\" (camera-related ones that take \"a few frames\" to\n> > apply) from \"immediate controls\" (ISP-related controls) that will\n> > happen instantly. It does so in a copy of the request's control list,\n> > so as to avoid disturbing the request's original version.\n> >\n> > The immediate controls are held back until the delayed controls that\n> > they were submitted with, have happened. This means all the controls\n> > submitted in a request happen atomically.\n> >\n> > We therefore already have the sequence number of the request whose\n> > controls have just been applied, so we additionally attach this to the\n> > current request as \"ControlId\" metadata.\n> >\n> > Signed-off-by: David Plowman <david.plowman@raspberrypi.com>\n> > Reviewed-by: Naushir Patuck <naush@raspberrypi.com>\n> > ---\n> >  src/ipa/rpi/common/ipa_base.cpp               | 24 ++++++---\n> >  .../pipeline/rpi/common/pipeline_base.cpp     | 49 +++++++++++++++++++\n> >  .../pipeline/rpi/common/pipeline_base.h       |  9 ++++\n> >  src/libcamera/pipeline/rpi/pisp/pisp.cpp      |  9 ++--\n> >  src/libcamera/pipeline/rpi/vc4/vc4.cpp        |  9 ++--\n> >  5 files changed, 86 insertions(+), 14 deletions(-)\n> >\n> > diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp\n> > index 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> >       rpiMetadata.clear();\n> >       fillDeviceStatus(params.sensorControls, ipaContext);\n> >\n> > +     /*\n> > +      * When there are controls, it's important that we don't skip running the\n> > +      * IPAs, as that can mess with synchronisation. Crucially though, we need\n> > +      * to know whether there were controls when this comes back as the\n> > +      * _delayed_ metadata, hence why we flag this in the metadata itself.\n> > +      */\n> > +     if (!params.requestControls.empty())\n> > +             rpiMetadata.set(\"ipa.request_controls\", true);\n> > +\n> >       if (params.buffers.embedded) {\n> >               /*\n> >                * Pipeline handler has supplied us with an embedded data buffer,\n> > @@ -451,7 +460,7 @@ void IpaBase::prepareIsp(const PrepareParams &params)\n> >        */\n> >       AgcStatus agcStatus;\n> >       bool hdrChange = false;\n> > -     RPiController::Metadata &delayedMetadata = rpiMetadata_[params.delayContext];\n> > +     RPiController::Metadata &delayedMetadata = rpiMetadata_[params.delayContext % rpiMetadata_.size()];\n> >       if (!delayedMetadata.get<AgcStatus>(\"agc.status\", agcStatus)) {\n> >               rpiMetadata.set(\"agc.delayed_status\", agcStatus);\n> >               hdrChange = agcStatus.hdr.mode != hdrStatus_.mode;\n> > @@ -464,9 +473,13 @@ void IpaBase::prepareIsp(const PrepareParams &params)\n> >        */\n> >       helper_->prepare(embeddedBuffer, rpiMetadata);\n> >\n> > +     bool delayedRequestControls = false;\n> > +     delayedMetadata.get<bool>(\"ipa.request_controls\", delayedRequestControls);\n> > +\n> >       /* Allow a 10% margin on the comparison below. */\n> >       Duration delta = (frameTimestamp - lastRunTimestamp_) * 1.0ns;\n> > -     if (lastRunTimestamp_ && frameCount_ > invalidCount_ &&\n> > +     if (!delayedRequestControls && params.requestControls.empty() &&\n> > +         lastRunTimestamp_ && frameCount_ > invalidCount_ &&\n> >           delta < controllerMinFrameDuration_ * 0.9 && !hdrChange) {\n> >               /*\n> >                * Ensure we merge the previous frame's metadata with the current\n> > @@ -535,7 +548,7 @@ void IpaBase::processStats(const ProcessParams &params)\n> >               ControlList ctrls(sensorCtrls_);\n> >               applyAGC(&agcStatus, ctrls);\n> >               rpiMetadata.set(\"agc.status\", agcStatus);\n> > -             setDelayedControls.emit(ctrls, ipaContext);\n> > +             setDelayedControls.emit(ctrls, params.ipaContext);\n> >               setCameraTimeoutValue();\n> >       }\n> >\n> > @@ -951,8 +964,6 @@ void IpaBase::applyControls(const ControlList &controls)\n> >\n> >                       /* The control provides units of microseconds. */\n> >                       agc->setFixedExposureTime(0, ctrl.second.get<int32_t>() * 1.0us);\n> > -\n> > -                     libcameraMetadata_.set(controls::ExposureTime, ctrl.second.get<int32_t>());\n> >                       break;\n> >               }\n> >\n> > @@ -976,9 +987,6 @@ void IpaBase::applyControls(const ControlList &controls)\n> >                               break;\n> >\n> >                       agc->setFixedGain(0, ctrl.second.get<float>());\n> > -\n> > -                     libcameraMetadata_.set(controls::AnalogueGain,\n> > -                                            ctrl.second.get<float>());\n> >                       break;\n> >               }\n> >\n> > diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\n> > index f6ef8a3b..0c82f549 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,53 @@ void CameraData::fillRequestMetadata(const ControlList &bufferControls, Request\n> >       }\n> >  }\n> >\n> > +static bool isControlDelayed(unsigned int id)\n> > +{\n> > +     return id == controls::ExposureTime ||\n> > +            id == controls::AnalogueGain ||\n> > +            id == controls::FrameDurationLimits ||\n> > +            id == controls::AeEnable ||\n> > +            id == controls::ExposureTimeMode ||\n> > +            id == controls::AnalogueGainMode;\n> > +}\n> > +\n> > +void CameraData::handleControlLists(uint32_t delayContext, ControlList &requestControls)\n> > +{\n> > +     /*\n> > +      * The delayContext is the sequence number after it's gone through the various\n> > +      * pipeline delays, so that's what gets reported as the \"ControlListSequence\"\n> > +      * in the metadata, being the sequence number of the request whose ControlList\n> > +      * has just been applied.\n> > +      */\n> > +     Request *request = requestQueue_.front();\n> > +     request->_d()->metadata().set(controls::rpi::ControlListSequence, delayContext);\n> > +\n> > +     /*\n> > +      * Controls that take effect immediately (typically ISP controls) have to be\n> > +      * delayed so as to synchronise with those controls that do get delayed. So we\n> > +      * must remove them from the current request, and push them onto a queue so\n> > +      * that they can be used later.\n> > +      *\n> > +      * Note that we are passed a copy of the request's controls, so that we can\n> > +      * avoid disturbing what was in the original request.\n> > +      */\n> > +     ControlList controls = std::move(requestControls);\n>\n> Does ControlList have a move() assignment operator ?\n> Is this going through a plain copy ?\n>\n> > +     requestControls.clear();\n>\n> Probably so, otherwise a proper move() would have emptied\n> requestControls.\n>\n> So this makes two copies overall\n>\n>         params.requestControls = request->controls();\n>         in the caller\n>\n>         ControlList controls = std::move(requestControls);\n>         here\n>\n> You could probably pass to this function\n>\n>         request->controls() as a const by reference\n>         params.requestControls as mutable by pointer\n>\n> and have here\n>\n>         params.requestControls->clear();\n>\n> > +     immediateControls_.push({ request->sequence(), {} });\n> > +     for (const auto &ctrl : controls) {\n>\n>         for (const auto &ctrl : request->controls()) {\n>\n> > +             if (isControlDelayed(ctrl.first))\n> > +                     requestControls.set(ctrl.first, ctrl.second);\n>                         params.requestControls->set()\n>\n> > +             else\n> > +                     immediateControls_.back().controls.set(ctrl.first, ctrl.second);\n> > +     }\n> > +\n> > +     /* \"Immediate\" controls that have become due are now merged back into this request. */\n> > +     while (!immediateControls_.empty() &&\n> > +            immediateControls_.front().controlListId <= delayContext) {\n> > +             requestControls.merge(immediateControls_.front().controls,\n>\n>                 params.requestControls->merge();\n>\n> > +                                   ControlList::MergePolicy::OverwriteExisting);\n> > +             immediateControls_.pop();\n> > +     }\n> > +}\n> > +\n>\n> This would be the resulting patch\n>\n> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\n> index 0c82f549675b..0318cf55a7b2 100644\n> --- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\n> +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp\n> @@ -1538,7 +1538,9 @@ static bool isControlDelayed(unsigned int id)\n>                id == controls::AnalogueGainMode;\n>  }\n>\n> -void CameraData::handleControlLists(uint32_t delayContext, ControlList &requestControls)\n> +void CameraData::handleControlLists(uint32_t delayContext,\n> +                                   const ControlList &requestControls,\n> +                                   ControlList *paramsControls)\n>  {\n>         /*\n>          * The delayContext is the sequence number after it's gone through the various\n> @@ -1558,12 +1560,11 @@ void CameraData::handleControlLists(uint32_t delayContext, ControlList &requestC\n>          * Note that we are passed a copy of the request's controls, so that we can\n>          * avoid disturbing what was in the original request.\n>          */\n> -       ControlList controls = std::move(requestControls);\n> -       requestControls.clear();\n>         immediateControls_.push({ request->sequence(), {} });\n> -       for (const auto &ctrl : controls) {\n> +       paramsControls->clear();\n> +       for (const auto &ctrl : requestControls) {\n>                 if (isControlDelayed(ctrl.first))\n> -                       requestControls.set(ctrl.first, ctrl.second);\n> +                       paramsControls->set(ctrl.first, ctrl.second);\n>                 else\n>                         immediateControls_.back().controls.set(ctrl.first, ctrl.second);\n>         }\n> @@ -1571,7 +1572,7 @@ void CameraData::handleControlLists(uint32_t delayContext, ControlList &requestC\n>         /* \"Immediate\" controls that have become due are now merged back into this request. */\n>         while (!immediateControls_.empty() &&\n>                immediateControls_.front().controlListId <= delayContext) {\n> -               requestControls.merge(immediateControls_.front().controls,\n> +               paramsControls->merge(immediateControls_.front().controls,\n>                                       ControlList::MergePolicy::OverwriteExisting);\n>                 immediateControls_.pop();\n>         }\n> diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h\n> index 134262b66a04..856aa4f632b4 100644\n> --- a/src/libcamera/pipeline/rpi/common/pipeline_base.h\n> +++ b/src/libcamera/pipeline/rpi/common/pipeline_base.h\n> @@ -191,7 +191,8 @@ protected:\n>                                  Request *request);\n>\n>         void handleControlLists(uint32_t delayContext,\n> -                               ControlList &requestControls);\n> +                               const ControlList &requestControls,\n> +                               ControlList *paramsControls);\n>\n>         virtual void tryRunPipeline() = 0;\n>\n> diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n> index 690d25118aba..4d2db33cd144 100644\n> --- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n> +++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n> @@ -2338,10 +2338,9 @@ void PiSPCameraData::tryRunPipeline()\n>         params.ipaContext = requestQueue_.front()->sequence();\n>         params.delayContext = job.delayContext;\n>         params.sensorControls = std::move(job.sensorControls);\n> -       params.requestControls = request->controls();\n>\n>         /* This sorts out synchronisation with ControlLists in earlier requests. */\n> -       handleControlLists(job.delayContext, params.requestControls);\n> +       handleControlLists(job.delayContext, request->controls(), &params.requestControls);\n>\n>         /* Set our state to say the pipeline is active. */\n>         state_ = State::Busy;\n> diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n> index 28e7ad4698bf..f010c877a927 100644\n> --- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n> +++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n> @@ -947,13 +947,12 @@ void Vc4CameraData::tryRunPipeline()\n>         ipa::RPi::PrepareParams params;\n>         params.buffers.bayer = RPi::MaskBayerData | bayer;\n>         params.sensorControls = std::move(bayerFrame.controls);\n> -       params.requestControls = request->controls();\n>         params.ipaContext = request->sequence();\n>         params.delayContext = bayerFrame.delayContext;\n>         params.buffers.embedded = 0;\n>\n>         /* This sorts out synchronisation with ControlLists in earlier requests. */\n> -       handleControlLists(bayerFrame.delayContext, params.requestControls);\n> +       handleControlLists(bayerFrame.delayContext, request->controls(), &params.requestControls);\n>\n>         /* Set our state to say the pipeline is active. */\n>         state_ = State::Busy;\n>\n> I admit I have onlt compiled it though but this should avoid the two\n> copies ?\n\nYes you're right that there's a copy that can be avoided (control\nlists can be moved, so don't think there's a second copy, though\nsomething in my head is nagging me you can't assume too much about the\nstate of the left-behind object after a move). Anyway I'll amend the\npatch along these lines (might not pass in the request->controls()\nexplicitly because it already has the request, but functionally the\nsame).\n\nThanks for pointing this out!\n\nDavid\n\n>\n>\n> >  } /* namespace libcamera */\n> > diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h\n> > index 7bfac33e..134262b6 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,19 @@ public:\n> >\n> >       ClockRecovery wallClockRecovery_;\n> >\n> > +     struct ImmediateControlsEntry {\n> > +             uint64_t controlListId;\n> > +             ControlList controls;\n> > +     };\n> > +     std::queue<ImmediateControlsEntry> immediateControls_;\n> > +\n> >  protected:\n> >       void fillRequestMetadata(const ControlList &bufferControls,\n> >                                Request *request);\n> >\n> > +     void handleControlLists(uint32_t delayContext,\n> > +                             ControlList &requestControls);\n> > +\n> >       virtual void tryRunPipeline() = 0;\n> >\n> >  private:\n> > diff --git a/src/libcamera/pipeline/rpi/pisp/pisp.cpp b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n> > index cc7e32c3..690d2511 100644\n> > --- a/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n> > +++ b/src/libcamera/pipeline/rpi/pisp/pisp.cpp\n> > @@ -2322,9 +2322,6 @@ void PiSPCameraData::tryRunPipeline()\n> >\n> >       fillRequestMetadata(job.sensorControls, request);\n> >\n> > -     /* Set our state to say the pipeline is active. */\n> > -     state_ = State::Busy;\n> > -\n> >       unsigned int bayerId = cfe_[Cfe::Output0].getBufferId(job.buffers[&cfe_[Cfe::Output0]]);\n> >       unsigned int statsId = cfe_[Cfe::Stats].getBufferId(job.buffers[&cfe_[Cfe::Stats]]);\n> >       ASSERT(bayerId && statsId);\n> > @@ -2343,6 +2340,12 @@ void PiSPCameraData::tryRunPipeline()\n> >       params.sensorControls = std::move(job.sensorControls);\n> >       params.requestControls = request->controls();\n> >\n> > +     /* This sorts out synchronisation with ControlLists in earlier requests. */\n> > +     handleControlLists(job.delayContext, params.requestControls);\n> > +\n> > +     /* Set our state to say the pipeline is active. */\n> > +     state_ = State::Busy;\n> > +\n> >       if (sensorMetadata_) {\n> >               unsigned int embeddedId =\n> >                       cfe_[Cfe::Embedded].getBufferId(job.buffers[&cfe_[Cfe::Embedded]]);\n> > diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n> > index f99cfdbc..28e7ad46 100644\n> > --- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n> > +++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp\n> > @@ -939,9 +939,6 @@ void Vc4CameraData::tryRunPipeline()\n> >\n> >       fillRequestMetadata(bayerFrame.controls, request);\n> >\n> > -     /* Set our state to say the pipeline is active. */\n> > -     state_ = State::Busy;\n> > -\n> >       unsigned int bayer = unicam_[Unicam::Image].getBufferId(bayerFrame.buffer);\n> >\n> >       LOG(RPI, Debug) << \"Signalling prepareIsp:\"\n> > @@ -955,6 +952,12 @@ void Vc4CameraData::tryRunPipeline()\n> >       params.delayContext = bayerFrame.delayContext;\n> >       params.buffers.embedded = 0;\n> >\n> > +     /* This sorts out synchronisation with ControlLists in earlier requests. */\n> > +     handleControlLists(bayerFrame.delayContext, params.requestControls);\n> > +\n> > +     /* Set our state to say the pipeline is active. */\n> > +     state_ = State::Busy;\n> > +\n>\n> With or without the proposed patch, as this is ultimately your\n> pipeline code:\n> Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>\n>\n> >       if (embeddedBuffer) {\n> >               unsigned int embeddedId = unicam_[Unicam::Embedded].getBufferId(embeddedBuffer);\n> >\n> > --\n> > 2.47.3\n> >","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 638C0BE173\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri,  8 May 2026 11:42:53 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 90B5362FE1;\n\tFri,  8 May 2026 13:42:52 +0200 (CEST)","from mail-ed1-x52f.google.com (mail-ed1-x52f.google.com\n\t[IPv6:2a00:1450:4864:20::52f])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id A5EAF62FD3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri,  8 May 2026 13:42:50 +0200 (CEST)","by mail-ed1-x52f.google.com with SMTP id\n\t4fb4d7f45d1cf-6746d0b2b4aso2901843a12.3\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 08 May 2026 04:42:50 -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=\"HB3AH4pL\"; dkim-atps=neutral","ARC-Seal":"i=1; a=rsa-sha256; t=1778240570; cv=none;\n\td=google.com; s=arc-20240605;\n\tb=RxUOhLL92MFT70mR1GcGbfAxuKJxAPs/vIuUv66if3rYxXbWGEixN3JftdI8kHbZN4\n\t4SzAScfzUFA8AXaElZgnvqGmRsf42LetuxbTKqB+7JF4pZ4Cf6yH1XoAFP0hwzsYZ66p\n\t2V5ZF5xcHrMrdRgEloo6DOIKfQYSNRD+k3WEp3XwUCKfDNLeWiDEJC7k1IjuONI2AiBG\n\tEWgO+1XDwBq3X/ACgpTqjELv3H/uvH8OCf78kK63g8z+fbhUjf07DfQTzAOW2QzJ1UA0\n\tLLeQ9HskdAtK35MaoFe5GU6tVURTWmTvpXT2Md4CLIZuaG3h35HBpt0VxMlDI09Jf1OW\n\tTzeA==","ARC-Message-Signature":"i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com;\n\ts=arc-20240605; \n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:dkim-signature;\n\tbh=7h5WhlrMNaz8NWhmV3NEOLpOUEfC7/6Wm2We4Is2H/A=;\n\tfh=6VfpC/KamyOzhucxK+8rfrs02fsbgubEbX+iqRBkw0E=;\n\tb=XwdlrbND3/vmtM0zzhCOVVF1rwMWPrcaPJeDcSr8+whmRhn9VMIS6sqBaWfFXldp3o\n\t9Tzjdk++x69QtbUGqnYyuV22k6ljQl0KvHeM7c7yp+ONf8K2EWY0i2anEuJR1MUPHR8h\n\trIHAjoduXxwEbJZSqtEuuMxbIeqbK8udS31mxzGry7mgBzbBA67ZmxMkCSADOVFXffWw\n\tBtON8iwfOi7cHlKhtei9K9TaVB/lk1lX62AFCOeN6emfNrJ/PWIz6HP8nbYzpxWIlHHw\n\tp+7RDUxod2fE53CQYx06cXnhlkHF4s0hsbOGG07Rd6xyrAMX+uVY6p3RDxvCUrBVm3yV\n\tMdiw==; darn=lists.libcamera.org","ARC-Authentication-Results":"i=1; mx.google.com; arc=none","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1778240570; x=1778845370;\n\tdarn=lists.libcamera.org; \n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:from:to:cc:subject:date:message-id:reply-to;\n\tbh=7h5WhlrMNaz8NWhmV3NEOLpOUEfC7/6Wm2We4Is2H/A=;\n\tb=HB3AH4pLZm8iysD+QQSMgzb+wCRCCDQ8K8TuT8DBBw+EfLPcXNUqNhl2HcaPwIi26y\n\tRWqVwsVFJlW2br3Wha8tNdndKX8V+2abaWnZz7PsFNXmEaQB9I+RE6ysJFbtbSACc6Jr\n\trsDa0T02Il5O4yh7P7yww2lGHx8mVrmPfeRKoSmgVAnxLB9PhLH5u7mPJ25tWe155WQ2\n\trh95FSqmUoBe+DJ8DPDxn/X65nOK/+1ZdJkrvstWJ440Ilba/WeYS7OC6utgoMbESvsf\n\tvpX8XgL8khNlQiYugmWkiWv4gKA+4tYuS1VasmPLfqIsjrEQ9CGPl2AlV8k9rP5qmYT9\n\t8G2A==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20251104; t=1778240570; x=1778845370;\n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:x-gm-gg:x-gm-message-state:from:to:cc:subject:date\n\t:message-id:reply-to;\n\tbh=7h5WhlrMNaz8NWhmV3NEOLpOUEfC7/6Wm2We4Is2H/A=;\n\tb=QJSXOnmVzlvv7B4z7daaw0BCKAY5Qxz7G9LRFZu/+KLj0NgW7sLg4Ht2UJAL7aFs6i\n\tHQRNv50Md7nlirf5VlcOoW4KnCS2aYybdW73ImOv0/EKlP5cX5Y2pTXKm9iyOsLysZzm\n\tBDM0kVj5rRJjFmYAbhDJdorzihGq6F11skOkXrmjVjg0Erla86WtZzOWmE5eiPzHauC9\n\theAF/cH397IPobh4yLTAOynshaiLGJnwqsD7CtY6O4Mo1gIktOoKLiFu23tktHoI5wXH\n\tMFmM37aI3pE4/epsbGBZRwJnzB2J0Ok6px2Wr5ZwMCBrN45DghNS7MLCcMtELWICuY7V\n\tXpNw==","X-Gm-Message-State":"AOJu0YwHkIO2Y5VjezugIqlOXQsRuXmBHR9uNyhNGB7VvmsbP/leKt5/\n\tABdUwAYI4E7+sUnbrPt7F8EiXMNBnr7jYbHf/vU0PzX4JtNLQBP0H7wMF3jj+AjgyyLntBzwIBM\n\tNyv+aWv+uP2i6xadG1FR46+NY4RBHms0Okr2rsmlNU0D0mLIp8hbU","X-Gm-Gg":"AeBDiesnr4HOAe52bdByru2uJ0XLSeZwSy+mbpIgMZuG3TERGdm+GcByJEFigYRGHpZ\n\ts+xQE7gNcvp1J1cQxWtu1ca/KCsplTKWn7aEXpgCKpQBOuWvHKt/zbcmiDhQIOTR7QZ/t8CEpa/\n\tkeWkofNW7AutMLUEM6KGzAz/TJSvbYgQSTU3bhjx8J0A4G7bmjucy1zm7BKOw7l17a0qkUldTWq\n\tTSzZgJ9aam4nr2jZCrpxWBKLJHwHadoTuU6fdC77O26I7mby63rTOXV5cqlKQ94fbucHrvpQTQJ\n\tw5TDwdRRtKJiZkEFZHeJhvHlr0XpPv9MZpb5+OAwpyBpd418AKSdEUr+MiIa82zZyTRT0XEnWoS\n\t0Q7ROFSftKie0MJ8seOcDqlk=","X-Received":"by 2002:a17:907:e104:b0:bca:4e24:b27b with SMTP id\n\ta640c23a62f3a-bca4e24b383mr179644166b.26.1778240569826;\n\tFri, 08 May 2026 04:42:49 -0700 (PDT)","MIME-Version":"1.0","References":"<20260507125458.12140-1-david.plowman@raspberrypi.com>\n\t<20260507125458.12140-4-david.plowman@raspberrypi.com>\n\t<af2YA7vMA_wlfOKb@zed>","In-Reply-To":"<af2YA7vMA_wlfOKb@zed>","From":"David Plowman <david.plowman@raspberrypi.com>","Date":"Fri, 8 May 2026 12:42:39 +0100","X-Gm-Features":"AVHnY4Iwu1NQ6NEd6lLqRIWRF5uIZj9eyBsXY6oRN-bwPmVmZ9G5aEaIQXa-BDg","Message-ID":"<CAHW6GY+9SELq0Yq99ZjveW4NUYCaP3GuLRWD_x3nxG9iaFSG6Q@mail.gmail.com>","Subject":"Re: [PATCH v4 3/3] pipeline: rpi: Make control lists in requests\n\tproperly atomic","To":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org, \n\tNaushir Patuck <naush@raspberrypi.com>","Content-Type":"text/plain; charset=\"UTF-8\"","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>"}}]