{"id":23589,"url":"https://patchwork.libcamera.org/api/1.1/patches/23589/?format=json","web_url":"https://patchwork.libcamera.org/patch/23589/","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":"<20250617082956.5699-7-david.plowman@raspberrypi.com>","date":"2025-06-17T08:29:54","name":"[6/7] ipa: rpi: Update digital gain handling in IPA base and derived classes","commit_ref":null,"pull_url":null,"state":"superseded","archived":false,"hash":"364bec2f2a07a0e05ff25ae801fb6dd895d006ae","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/23589/mbox/","series":[{"id":5225,"url":"https://patchwork.libcamera.org/api/1.1/series/5225/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5225","date":"2025-06-17T08:29:48","name":"Raspberry Pi AEC/AGC update","version":1,"mbox":"https://patchwork.libcamera.org/series/5225/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/23589/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/23589/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 63987BDE6B\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 17 Jun 2025 08:30:19 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id B881C68DD4;\n\tTue, 17 Jun 2025 10:30:15 +0200 (CEST)","from mail-wr1-x430.google.com (mail-wr1-x430.google.com\n\t[IPv6:2a00:1450:4864:20::430])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id B7B0D68DD8\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 17 Jun 2025 10:30:06 +0200 (CEST)","by mail-wr1-x430.google.com with SMTP id\n\tffacd0b85a97d-3a548a73ff2so4976803f8f.0\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 17 Jun 2025 01:30:06 -0700 (PDT)","from raspberrypi.pitowers.org\n\t([2a00:1098:3142:1f:ffc9:aff6:7f7f:893b])\n\tby smtp.gmail.com with ESMTPSA id\n\t5b1f17b1804b1-4532e259108sm166062955e9.32.2025.06.17.01.30.04\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tTue, 17 Jun 2025 01:30:05 -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=\"ippmbL5n\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1750149006; x=1750753806;\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=Ka6w8XlXuyTMARzl8Nb760HyjvSz5j6FevThoPwy1Bo=;\n\tb=ippmbL5nQsnxFthvtSSkfZtIyCBhYiLex/J2ISEwnDcDFYcS0Kxz3oOim+MzbgZWX2\n\tBuvZRQkDSdPQMu4fuOiDatVFLaWEPRfgbsCIJCCZ3lnutyF9hgwtDxSd02m8gELqoUvh\n\tjIoGr5pojQ7RWG66GG0PkX7mkil+TLvwqx27YEJwxgYel88EbEFu7qvYn1vLUZ2i5iZe\n\tGGy1rPHWhCc7MC4vNN/QjA3jHYYYZ/yLXwX4KYUY4v7sWEo0VdT1me80cSr4yhb3AT9T\n\tiROV8Qpz4aNPvL0pyXg5dwIfCpYPiWjG+2yTklqntO7NtqFV7nf3awUnF4NCW4iaJV4Z\n\tOO7A==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1750149006; x=1750753806;\n\th=content-transfer-encoding:mime-version:references:in-reply-to\n\t:message-id:date:subject:cc:to:from:x-gm-message-state:from:to:cc\n\t:subject:date:message-id:reply-to;\n\tbh=Ka6w8XlXuyTMARzl8Nb760HyjvSz5j6FevThoPwy1Bo=;\n\tb=JLSCTMRnzT9z9AKoZFKs8wHJUTKoOOVkUzsFYzzypX5UOmZqkyVh0MJnl9fJFNdJfo\n\tdoNSjeHQYCKJFE8lysEXvyck+MAm2hiHJyJtuf+rCjQv1tgpG6gBoL/NwXEykmHFdC1Z\n\tWIEQjWUJwm5kPd97G4zILxXcasEV6U3z9HJ1Uv1f48MQSNnhg2SWbv8+XAthGwS8yrdG\n\td9Arfbqef4FlXMDVaoJkWCNQqLrMk2AE5t6sqcghqxirJ/J0rVmgdlr3Ge/WWmypXNZy\n\tEfBWjyVTW4FoQOz9ZemSZhSReeAjcZG1UC0Zayd8wDyWDWqyuUJ0sKyjkCLW7XbTO2Ae\n\tXHpw==","X-Gm-Message-State":"AOJu0YxzxRyp910Pp7qUCL5RBVwBXHw9sUuvh9WgTsp9Po5+NbVtXHy9\n\tP9d+VVLoHq9wwryboD588HXOKIYJL9JFRp07RHEAtZuw29Oq6b2mjlSn1ExLgkhRo3rnTph7rzR\n\tAzY8V","X-Gm-Gg":"ASbGnctZKctboSULaUOjYHxPMessMD8v6AQm4fvMpJS2p7fI5QUhCw+JaVvEF6cor+Y\n\tr1pa631c4fo3bihgyGTADQ6uZVzwMNEc/YS8dV8cul48N97SdWYfi5SAzvOMtPnK/Z4I7RNwMBK\n\tPG39TqgB9zjMbcJIgqfYrCVqUgvk5kWZp9gkae7NSsBcXs6i+QIJiMjtzugbVdGnw20T5Oiaui3\n\txOCH2zKmwdrdKDwhYvJa2Lj23m1ue6s3yx8czHmwT6pNg40aGfg1kosbafgmrMLN3R9Udia4MbW\n\tmZO/RJQgnsDsGhZp/+/jQ9H/MQ8ADAX1g+BPJfz9hxjvHoWPbYM5y2n1ipOt//Uyqfpy44mLDoZ\n\tjO4RWq13/cmU7Xj8VQQ==","X-Google-Smtp-Source":"AGHT+IGYHsSHGmlki6gW/buxyX97m5rntrA5Bo0C08oXWbswIRROrtJ9wXtFnwzTBJObNc+c4iGkCA==","X-Received":"by 2002:a05:6000:25eb:b0:3a3:65b5:51d7 with SMTP id\n\tffacd0b85a97d-3a572e2df7bmr11058480f8f.26.1750149005514; \n\tTue, 17 Jun 2025 01:30:05 -0700 (PDT)","From":"David Plowman <david.plowman@raspberrypi.com>","To":"libcamera-devel@lists.libcamera.org","Cc":"David Plowman <david.plowman@raspberrypi.com>","Subject":"[PATCH 6/7] ipa: rpi: Update digital gain handling in IPA base and\n\tderived classes","Date":"Tue, 17 Jun 2025 09:29:54 +0100","Message-Id":"<20250617082956.5699-7-david.plowman@raspberrypi.com>","X-Mailer":"git-send-email 2.39.5","In-Reply-To":"<20250617082956.5699-1-david.plowman@raspberrypi.com>","References":"<20250617082956.5699-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":"Here we update the digital gain handling to use the value computed by\nprocess() in the AgcStatus, not the version that was previously in the\nAgcPrepareStatus.\n\nBecause we apply this digital gain directly with no further\nmodification, we have to update it to reflect any exposure/gain\nquantisation that happens (in IpaBase::applyAGC).\n\nWe must also run the new platformPrepareAgc() even when we're skipping\nplatformPrepareIsp(), which has been split out of the previous\nplatformPrepareIsp() implementation.\n\nSigned-off-by: David Plowman <david.plowman@raspberrypi.com>\n---\n src/ipa/rpi/common/ipa_base.cpp | 50 +++++++++++++++++++--------\n src/ipa/rpi/common/ipa_base.h   |  6 +++-\n src/ipa/rpi/pisp/pisp.cpp       | 61 +++++++++++++++++++++------------\n src/ipa/rpi/vc4/vc4.cpp         | 28 +++++++++------\n 4 files changed, 98 insertions(+), 47 deletions(-)","diff":"diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp\nindex 7af4ea5e..22aadeba 100644\n--- a/src/ipa/rpi/common/ipa_base.cpp\n+++ b/src/ipa/rpi/common/ipa_base.cpp\n@@ -309,20 +309,23 @@ void IpaBase::start(const ControlList &controls, StartResult *result)\n \tframeLengths_.clear();\n \tframeLengths_.resize(FrameLengthsQueueSize, 0s);\n \n-\t/* SwitchMode may supply updated exposure/gain values to use. */\n-\tAgcStatus agcStatus;\n-\tagcStatus.exposureTime = 0.0s;\n-\tagcStatus.analogueGain = 0.0;\n+\t/*\n+\t * SwitchMode may supply updated exposure/gain values to use.\n+\t * agcStatus_ will store these values for us to use until delayed_status values\n+\t * start to appear.\n+\t */\n+\tagcStatus_.exposureTime = 0.0s;\n+\tagcStatus_.analogueGain = 0.0;\n \n-\tmetadata.get(\"agc.status\", agcStatus);\n-\tif (agcStatus.exposureTime && agcStatus.analogueGain) {\n+\tmetadata.get(\"agc.status\", agcStatus_);\n+\tif (agcStatus_.exposureTime && agcStatus_.analogueGain) {\n \t\tControlList ctrls(sensorCtrls_);\n-\t\tapplyAGC(&agcStatus, ctrls);\n+\t\tapplyAGC(&agcStatus_, ctrls);\n \t\tresult->controls = std::move(ctrls);\n \t\tsetCameraTimeoutValue();\n \t}\n \t/* Make a note of this as it tells us the HDR status of the first few frames. */\n-\thdrStatus_ = agcStatus.hdr;\n+\thdrStatus_ = agcStatus_.hdr;\n \n \t/*\n \t * Initialise frame counts, and decide how many frames must be hidden or\n@@ -476,7 +479,9 @@ void IpaBase::prepareIsp(const PrepareParams &params)\n \t\tcontroller_.prepare(&rpiMetadata);\n \t\t/* Actually prepare the ISP parameters for the frame. */\n \t\tplatformPrepareIsp(params, rpiMetadata);\n-\t}\n+\t\tplatformPrepareAgc(rpiMetadata);\n+\t} else\n+\t\tplatformPrepareAgc(rpiMetadata);\n \n \tframeCount_++;\n \n@@ -528,6 +533,7 @@ void IpaBase::processStats(const ProcessParams &params)\n \tif (rpiMetadata.get(\"agc.status\", agcStatus) == 0) {\n \t\tControlList ctrls(sensorCtrls_);\n \t\tapplyAGC(&agcStatus, ctrls, offset);\n+\t\trpiMetadata.set(\"agc.status\", agcStatus);\n \t\tsetDelayedControls.emit(ctrls, ipaContext);\n \t\tsetCameraTimeoutValue();\n \t}\n@@ -1472,9 +1478,6 @@ void IpaBase::reportMetadata(unsigned int ipaContext)\n \t}\n \n \tAgcPrepareStatus *agcPrepareStatus = rpiMetadata.getLocked<AgcPrepareStatus>(\"agc.prepare_status\");\n-\tif (agcPrepareStatus)\n-\t\tlibcameraMetadata_.set(controls::DigitalGain, agcPrepareStatus->digitalGain);\n-\n \tRPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(\n \t\tcontroller_.getAlgorithm(\"agc\"));\n \tif (agc) {\n@@ -1487,6 +1490,13 @@ void IpaBase::reportMetadata(unsigned int ipaContext)\n \t\t\t\t\t\t       : controls::AeStateSearching);\n \t}\n \n+\tconst AgcStatus *agcStatus = rpiMetadata.getLocked<AgcStatus>(\"agc.delayed_status\");\n+\tif (agcStatus)\n+\t\tlibcameraMetadata_.set(controls::DigitalGain, agcStatus->digitalGain);\n+\telse\n+\t\tlibcameraMetadata_.set(controls::DigitalGain, agcStatus_.digitalGain);\n+\t/* The HDR metadata reporting will use this agcStatus too. */\n+\n \tLuxStatus *luxStatus = rpiMetadata.getLocked<LuxStatus>(\"lux.status\");\n \tif (luxStatus)\n \t\tlibcameraMetadata_.set(controls::Lux, luxStatus->lux);\n@@ -1576,7 +1586,6 @@ void IpaBase::reportMetadata(unsigned int ipaContext)\n \t * delayed_status to be available, we use the HDR status that came out of the\n \t * switchMode call.\n \t */\n-\tconst AgcStatus *agcStatus = rpiMetadata.getLocked<AgcStatus>(\"agc.delayed_status\");\n \tconst HdrStatus &hdrStatus = agcStatus ? agcStatus->hdr : hdrStatus_;\n \tif (!hdrStatus.mode.empty() && hdrStatus.mode != \"Off\") {\n \t\tint32_t hdrMode = controls::HdrModeOff;\n@@ -1686,7 +1695,7 @@ void IpaBase::applyFrameDurations(Duration minFrameDuration, Duration maxFrameDu\n \t}\n }\n \n-void IpaBase::applyAGC(const struct AgcStatus *agcStatus, ControlList &ctrls, Duration frameDurationOffset)\n+void IpaBase::applyAGC(struct AgcStatus *agcStatus, ControlList &ctrls, Duration frameDurationOffset)\n {\n \tconst int32_t minGainCode = helper_->gainCode(mode_.minAnalogueGain);\n \tconst int32_t maxGainCode = helper_->gainCode(mode_.maxAnalogueGain);\n@@ -1716,6 +1725,19 @@ void IpaBase::applyAGC(const struct AgcStatus *agcStatus, ControlList &ctrls, Du\n \tctrls.set(V4L2_CID_EXPOSURE, exposureLines);\n \tctrls.set(V4L2_CID_ANALOGUE_GAIN, gainCode);\n \n+\t/*\n+\t * We must update the digital gain to make up for any quantisation that happens, and\n+\t * communicate that back into the metadata so that it will appear as the \"delayed\" status.\n+\t * (Note that \"exposure\" is already the \"actual\" exposure.)\n+\t */\n+\tdouble actualGain = helper_->gain(gainCode);\n+\tdouble ratio = agcStatus->analogueGain / actualGain;\n+\tratio *= agcStatus->exposureTime / exposure;\n+\tdouble newDigitalGain = agcStatus->digitalGain * ratio;\n+\tagcStatus->digitalGain = newDigitalGain;\n+\tagcStatus->analogueGain = actualGain;\n+\tagcStatus->exposureTime = exposure;\n+\n \t/*\n \t * At present, there is no way of knowing if a control is read-only.\n \t * As a workaround, assume that if the minimum and maximum values of\ndiff --git a/src/ipa/rpi/common/ipa_base.h b/src/ipa/rpi/common/ipa_base.h\nindex ed378f45..943a3c91 100644\n--- a/src/ipa/rpi/common/ipa_base.h\n+++ b/src/ipa/rpi/common/ipa_base.h\n@@ -73,6 +73,9 @@ protected:\n \t/* Remember the HDR status after a mode switch. */\n \tHdrStatus hdrStatus_;\n \n+\t/* Remember the AGC status after a mode switch. */\n+\tAgcStatus agcStatus_;\n+\n \t/* Whether the stitch block (if available) needs to swap buffers. */\n \tbool stitchSwapBuffers_;\n \n@@ -86,6 +89,7 @@ private:\n \n \tvirtual void platformPrepareIsp(const PrepareParams &params,\n \t\t\t\t\tRPiController::Metadata &rpiMetadata) = 0;\n+\tvirtual void platformPrepareAgc(RPiController::Metadata &rpiMetadata) = 0;\n \tvirtual RPiController::StatisticsPtr platformProcessStats(Span<uint8_t> mem) = 0;\n \n \tvoid setMode(const IPACameraSensorInfo &sensorInfo);\n@@ -98,7 +102,7 @@ private:\n \tvoid fillSyncParams(const PrepareParams &params, unsigned int ipaContext);\n \tvoid reportMetadata(unsigned int ipaContext);\n \tvoid applyFrameDurations(utils::Duration minFrameDuration, utils::Duration maxFrameDuration);\n-\tvoid applyAGC(const struct AgcStatus *agcStatus, ControlList &ctrls,\n+\tvoid applyAGC(struct AgcStatus *agcStatus, ControlList &ctrls,\n \t\t      utils::Duration frameDurationOffset = utils::Duration(0));\n \n \tstd::map<unsigned int, MappedFrameBuffer> buffers_;\ndiff --git a/src/ipa/rpi/pisp/pisp.cpp b/src/ipa/rpi/pisp/pisp.cpp\nindex e1a804f5..ab70d8f4 100644\n--- a/src/ipa/rpi/pisp/pisp.cpp\n+++ b/src/ipa/rpi/pisp/pisp.cpp\n@@ -218,13 +218,14 @@ private:\n \n \tvoid platformPrepareIsp(const PrepareParams &params,\n \t\t\t\tRPiController::Metadata &rpiMetadata) override;\n+\tvoid platformPrepareAgc(RPiController::Metadata &rpiMetadata) override;\n \tRPiController::StatisticsPtr platformProcessStats(Span<uint8_t> mem) override;\n \n \tvoid handleControls(const ControlList &controls) override;\n \n-\tvoid applyWBG(const AwbStatus *awbStatus, const AgcPrepareStatus *agcStatus,\n+\tvoid applyWBG(const AwbStatus *awbStatus, double digitalGain,\n \t\t      pisp_be_global_config &global);\n-\tvoid applyDgOnly(const AgcPrepareStatus *agcPrepareStatus, pisp_be_global_config &global);\n+\tvoid applyDgOnly(double digitalGain, pisp_be_global_config &global);\n \tvoid applyCAC(const CacStatus *cacStatus, pisp_be_global_config &global);\n \tvoid applyContrast(const ContrastStatus *contrastStatus,\n \t\t\t   pisp_be_global_config &global);\n@@ -341,7 +342,6 @@ void IpaPiSP::platformPrepareIsp([[maybe_unused]] const PrepareParams &params,\n \t\t\t\tPISP_BE_RGB_ENABLE_SHARPEN + PISP_BE_RGB_ENABLE_SAT_CONTROL);\n \n \tNoiseStatus *noiseStatus = rpiMetadata.getLocked<NoiseStatus>(\"noise.status\");\n-\tAgcPrepareStatus *agcPrepareStatus = rpiMetadata.getLocked<AgcPrepareStatus>(\"agc.prepare_status\");\n \n \t{\n \t\t/* All Frontend config goes first, we do not want to hold the FE lock for long! */\n@@ -355,14 +355,6 @@ void IpaPiSP::platformPrepareIsp([[maybe_unused]] const PrepareParams &params,\n \t\tif (blackLevelStatus)\n \t\t\tapplyBlackLevel(blackLevelStatus, global);\n \n-\t\tAwbStatus *awbStatus = rpiMetadata.getLocked<AwbStatus>(\"awb.status\");\n-\t\tif (awbStatus && agcPrepareStatus) {\n-\t\t\t/* Applies digital gain as well. */\n-\t\t\tapplyWBG(awbStatus, agcPrepareStatus, global);\n-\t\t} else if (agcPrepareStatus) {\n-\t\t\t/* Mono sensor fallback for digital gain. */\n-\t\t\tapplyDgOnly(agcPrepareStatus, global);\n-\t\t}\n \t}\n \n \tCacStatus *cacStatus = rpiMetadata.getLocked<CacStatus>(\"cac.status\");\n@@ -443,6 +435,34 @@ void IpaPiSP::platformPrepareIsp([[maybe_unused]] const PrepareParams &params,\n \t}\n }\n \n+void IpaPiSP::platformPrepareAgc(RPiController::Metadata &rpiMetadata)\n+{\n+\tstd::scoped_lock<RPiController::Metadata> l(rpiMetadata);\n+\n+\tAgcStatus *delayedAgcStatus = rpiMetadata.getLocked<AgcStatus>(\"agc.delayed_status\");\n+\t/* If no delayed status, use the gain from the last mode switch. */\n+\tdouble digitalGain = delayedAgcStatus ? delayedAgcStatus->digitalGain : agcStatus_.digitalGain;\n+\tAwbStatus *awbStatus = rpiMetadata.getLocked<AwbStatus>(\"awb.status\");\n+\n+\tpisp_be_global_config global;\n+\tbe_->GetGlobal(global);\n+\n+\t{\n+\t\t/* All Frontend config goes first, we do not want to hold the FE lock for long! */\n+\t\tstd::scoped_lock<FrontEnd> lf(*fe_);\n+\n+\t\tif (awbStatus) {\n+\t\t\t/* Applies digital gain as well. */\n+\t\t\tapplyWBG(awbStatus, digitalGain, global);\n+\t\t} else {\n+\t\t\t/* Mono sensor fallback for digital gain. */\n+\t\t\tapplyDgOnly(digitalGain, global);\n+\t\t}\n+\t}\n+\n+\tbe_->SetGlobal(global);\n+}\n+\n RPiController::StatisticsPtr IpaPiSP::platformProcessStats(Span<uint8_t> mem)\n {\n \tusing namespace RPiController;\n@@ -515,12 +535,11 @@ void IpaPiSP::handleControls(const ControlList &controls)\n \t}\n }\n \n-void IpaPiSP::applyWBG(const AwbStatus *awbStatus, const AgcPrepareStatus *agcPrepareStatus,\n+void IpaPiSP::applyWBG(const AwbStatus *awbStatus, double digitalGain,\n \t\t       pisp_be_global_config &global)\n {\n \tpisp_wbg_config wbg;\n \tpisp_fe_rgby_config rgby = {};\n-\tdouble dg = agcPrepareStatus ? agcPrepareStatus->digitalGain : 1.0;\n \tdouble minColourGain = std::min({ awbStatus->gainR, awbStatus->gainG, awbStatus->gainB, 1.0 });\n \t/* The 0.1 here doesn't mean much, but just stops arithmetic errors and extreme behaviour. */\n \tdouble extraGain = 1.0 / std::max({ minColourGain, 0.1 });\n@@ -536,9 +555,9 @@ void IpaPiSP::applyWBG(const AwbStatus *awbStatus, const AgcPrepareStatus *agcPr\n \tdouble gainG = awbStatus->gainG * extraGain;\n \tdouble gainB = awbStatus->gainB * extraGain;\n \n-\twbg.gain_r = clampField(dg * gainR, 14, 10);\n-\twbg.gain_g = clampField(dg * gainG, 14, 10);\n-\twbg.gain_b = clampField(dg * gainB, 14, 10);\n+\twbg.gain_r = clampField(digitalGain * gainR, 14, 10);\n+\twbg.gain_g = clampField(digitalGain * gainG, 14, 10);\n+\twbg.gain_b = clampField(digitalGain * gainB, 14, 10);\n \n \t/*\n \t * The YCbCr conversion block should contain the appropriate YCbCr\n@@ -561,15 +580,15 @@ void IpaPiSP::applyWBG(const AwbStatus *awbStatus, const AgcPrepareStatus *agcPr\n \tglobal.bayer_enables |= PISP_BE_BAYER_ENABLE_WBG;\n }\n \n-void IpaPiSP::applyDgOnly(const AgcPrepareStatus *agcPrepareStatus, pisp_be_global_config &global)\n+void IpaPiSP::applyDgOnly(double digitalGain, pisp_be_global_config &global)\n {\n \tpisp_wbg_config wbg;\n \n-\twbg.gain_r = clampField(agcPrepareStatus->digitalGain, 14, 10);\n-\twbg.gain_g = clampField(agcPrepareStatus->digitalGain, 14, 10);\n-\twbg.gain_b = clampField(agcPrepareStatus->digitalGain, 14, 10);\n+\twbg.gain_r = clampField(digitalGain, 14, 10);\n+\twbg.gain_g = clampField(digitalGain, 14, 10);\n+\twbg.gain_b = clampField(digitalGain, 14, 10);\n \n-\tLOG(IPARPI, Debug) << \"Applying DG (only) : \" << agcPrepareStatus->digitalGain;\n+\tLOG(IPARPI, Debug) << \"Applying DG (only) : \" << digitalGain;\n \n \tbe_->SetWbg(wbg);\n \tglobal.bayer_enables |= PISP_BE_BAYER_ENABLE_WBG;\ndiff --git a/src/ipa/rpi/vc4/vc4.cpp b/src/ipa/rpi/vc4/vc4.cpp\nindex 8a7a37c8..b2fec934 100644\n--- a/src/ipa/rpi/vc4/vc4.cpp\n+++ b/src/ipa/rpi/vc4/vc4.cpp\n@@ -57,14 +57,14 @@ private:\n \tint32_t platformConfigure(const ConfigParams &params, ConfigResult *result) override;\n \n \tvoid platformPrepareIsp(const PrepareParams &params, RPiController::Metadata &rpiMetadata) override;\n+\tvoid platformPrepareAgc([[maybe_unused]] RPiController::Metadata &rpiMetadata) override;\n \tRPiController::StatisticsPtr platformProcessStats(Span<uint8_t> mem) override;\n \n \tvoid handleControls(const ControlList &controls) override;\n \tbool validateIspControls();\n \n \tvoid applyAWB(const struct AwbStatus *awbStatus, ControlList &ctrls);\n-\tvoid applyDG(const struct AgcPrepareStatus *dgStatus,\n-\t\t     const struct AwbStatus *awbStatus, ControlList &ctrls);\n+\tvoid applyDG(double digitalGain, const struct AwbStatus *awbStatus, ControlList &ctrls);\n \tvoid applyCCM(const struct CcmStatus *ccmStatus, ControlList &ctrls);\n \tvoid applyBlackLevel(const struct BlackLevelStatus *blackLevelStatus, ControlList &ctrls);\n \tvoid applyGamma(const struct ContrastStatus *contrastStatus, ControlList &ctrls);\n@@ -78,6 +78,7 @@ private:\n \n \t/* VC4 ISP controls. */\n \tControlInfoMap ispCtrls_;\n+\tControlList ctrls_;\n \n \t/* LS table allocation passed in from the pipeline handler. */\n \tSharedFD lsTableHandle_;\n@@ -107,6 +108,7 @@ int32_t IpaVc4::platformStart([[maybe_unused]] const ControlList &controls,\n int32_t IpaVc4::platformConfigure(const ConfigParams &params, [[maybe_unused]] ConfigResult *result)\n {\n \tispCtrls_ = params.ispControls;\n+\tctrls_ = ControlList(ispCtrls_);\n \tif (!validateIspControls()) {\n \t\tLOG(IPARPI, Error) << \"ISP control validation failed.\";\n \t\treturn -1;\n@@ -139,7 +141,7 @@ int32_t IpaVc4::platformConfigure(const ConfigParams &params, [[maybe_unused]] C\n void IpaVc4::platformPrepareIsp([[maybe_unused]] const PrepareParams &params,\n \t\t\t\tRPiController::Metadata &rpiMetadata)\n {\n-\tControlList ctrls(ispCtrls_);\n+\tControlList &ctrls = ctrls_;\n \n \t/* Lock the metadata buffer to avoid constant locks/unlocks. */\n \tstd::unique_lock<RPiController::Metadata> lock(rpiMetadata);\n@@ -152,9 +154,6 @@ void IpaVc4::platformPrepareIsp([[maybe_unused]] const PrepareParams &params,\n \tif (ccmStatus)\n \t\tapplyCCM(ccmStatus, ctrls);\n \n-\tAgcPrepareStatus *dgStatus = rpiMetadata.getLocked<AgcPrepareStatus>(\"agc.prepare_status\");\n-\tapplyDG(dgStatus, awbStatus, ctrls);\n-\n \tAlscStatus *lsStatus = rpiMetadata.getLocked<AlscStatus>(\"alsc.status\");\n \tif (lsStatus)\n \t\tapplyLS(lsStatus, ctrls);\n@@ -190,9 +189,18 @@ void IpaVc4::platformPrepareIsp([[maybe_unused]] const PrepareParams &params,\n \t\tif (!lensctrls.empty())\n \t\t\tsetLensControls.emit(lensctrls);\n \t}\n+}\n \n-\tif (!ctrls.empty())\n-\t\tsetIspControls.emit(ctrls);\n+void IpaVc4::platformPrepareAgc(RPiController::Metadata &rpiMetadata)\n+{\n+\tAgcStatus *delayedAgcStatus = rpiMetadata.getLocked<AgcStatus>(\"agc.delayed_status\");\n+\tdouble digitalGain = delayedAgcStatus ? delayedAgcStatus->digitalGain : agcStatus_.digitalGain;\n+\tAwbStatus *awbStatus = rpiMetadata.getLocked<AwbStatus>(\"awb.status\");\n+\n+\tapplyDG(digitalGain, awbStatus, ctrls_);\n+\n+\tsetIspControls.emit(ctrls_);\n+\tctrls_ = ControlList(ispCtrls_);\n }\n \n RPiController::StatisticsPtr IpaVc4::platformProcessStats(Span<uint8_t> mem)\n@@ -329,11 +337,9 @@ void IpaVc4::applyAWB(const struct AwbStatus *awbStatus, ControlList &ctrls)\n \t\t  static_cast<int32_t>(awbStatus->gainB * 1000));\n }\n \n-void IpaVc4::applyDG(const struct AgcPrepareStatus *dgStatus,\n+void IpaVc4::applyDG(double digitalGain,\n \t\t     const struct AwbStatus *awbStatus, ControlList &ctrls)\n {\n-\tdouble digitalGain = dgStatus ? dgStatus->digitalGain : 1.0;\n-\n \tif (awbStatus) {\n \t\t/*\n \t\t * We must apply sufficient extra digital gain to stop any of the channel gains being\n","prefixes":["6/7"]}