{"id":23864,"url":"https://patchwork.libcamera.org/api/1.1/patches/23864/?format=json","web_url":"https://patchwork.libcamera.org/patch/23864/","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":"<20250721074853.1463358-6-naush@raspberrypi.com>","date":"2025-07-21T07:47:25","name":"[v2,5/7] ipa: rpi: agc: Calculate digital gain in process()","commit_ref":null,"pull_url":null,"state":"accepted","archived":false,"hash":"2c65e907fb7e3b56d96fe72a21301168c4fe1c2e","submitter":{"id":34,"url":"https://patchwork.libcamera.org/api/1.1/people/34/?format=json","name":"Naushir Patuck","email":"naush@raspberrypi.com"},"delegate":null,"mbox":"https://patchwork.libcamera.org/patch/23864/mbox/","series":[{"id":5303,"url":"https://patchwork.libcamera.org/api/1.1/series/5303/?format=json","web_url":"https://patchwork.libcamera.org/project/libcamera/list/?series=5303","date":"2025-07-21T07:47:20","name":"Raspberry Pi AEC/AGC update","version":2,"mbox":"https://patchwork.libcamera.org/series/5303/mbox/"}],"comments":"https://patchwork.libcamera.org/api/patches/23864/comments/","check":"pending","checks":"https://patchwork.libcamera.org/api/patches/23864/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 A02B8C3323\n\tfor <parsemail@patchwork.libcamera.org>;\n\tMon, 21 Jul 2025 07:49:10 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 18C4468FDE;\n\tMon, 21 Jul 2025 09:49:09 +0200 (CEST)","from mail-wr1-x42a.google.com (mail-wr1-x42a.google.com\n\t[IPv6:2a00:1450:4864:20::42a])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 61E2768FCE\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 21 Jul 2025 09:48:59 +0200 (CEST)","by mail-wr1-x42a.google.com with SMTP id\n\tffacd0b85a97d-3a4e749d7b2so660864f8f.0\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tMon, 21 Jul 2025 00:48:59 -0700 (PDT)","from NAUSH-P-DELL.pitowers.org ([93.93.133.154])\n\tby smtp.gmail.com with ESMTPSA id\n\t5b1f17b1804b1-4562e802afasm151101765e9.12.2025.07.21.00.48.57\n\t(version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);\n\tMon, 21 Jul 2025 00:48:58 -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=\"Wu/Nv8zC\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1753084138; x=1753688938;\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=BGJsTePNOrgIui+pOXQicmAtP5q9l21Oj649LL5oFl8=;\n\tb=Wu/Nv8zCOgGamHseKgJDBKNiNolkId9fAU7UrQAMoOVa3dn8m+NE9UZ/BkIIHMCfLH\n\tx17wAEmmL4jnd/f0B0iJsYgOkm0DdI/ZcjPy7CApNx+nHvpkaIkF6SOXcq0RyHx9d/O7\n\ty1z33Rv2qVehiSF3dcBovdecFVBFdUwtkETXbTCznETJECcJmfpIe228IFWUlHJpSEJK\n\tZ/e7Elk4xoA8rSRB9+lVpVFrZo7NQk6uRfIa3NdaVn8e+bsbKob20lFg7PgBC6Nwcna4\n\tZeSKjOhNzh30uPEIVdx3OrxW9Df72f7O6ZjXxxQRCBkwzlOPKpZu0P5V+lm4xV3SO3Yd\n\tOYvA==","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20230601; t=1753084138; x=1753688938;\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=BGJsTePNOrgIui+pOXQicmAtP5q9l21Oj649LL5oFl8=;\n\tb=b9XbiK1ydYbK4lixVeCF9YSpiyR7p+sRaN2HsjzigGAC3Ej/omy1geLl2EAjpW712b\n\tRlNPhP7eNW2y1WvdnlPrjZGOocge8wBYwbBFYwL2r8UWeIyfSUZtkq/FbYcVLA7a7elA\n\tHY2gk32K2EWqBTP5KYdZX7NgbYHphdrxJjHihsyAYGP0jC0um+FrItDghcVVwQuk1toQ\n\tc9T/UjpBkzReIyotGj6yVL517lsKisCXMatir6ydVKaRZvRgw/49+v874LjtqJ+YRcZX\n\tOncl+M2bdApGSTP5S3wfpgJvfLYmWgIHNEPr6/8UYcuJo7N657v65aHFwJmE7yLPxcDj\n\tlhGA==","X-Gm-Message-State":"AOJu0YyFRiW409MWaGM4jptQ1MZ7F2f8rSWil/3GYza2keoOqgtVRsdI\n\tgJbiGuRFQ945N3PZ1Bknsz5IpdrhtxPKi7u0tLpvIzMFTzoe890anZfcTNYgLQRycwjdv57z1wl\n\tvm5wl2VE=","X-Gm-Gg":"ASbGncuxnoO65IuJeDT//XiWZedqUSVXLy/32qkJ7TUhyfUm8VJrc8+qCSZgcOUA4aG\n\tp5dYxxtibC6d6J9ZIPBu+c2bAOS6cPQajTK7JcN8SW3oC2RcHI5aoEd0XqVEtqNu/nCtowpzYAX\n\toq3i3TZvdYE9e0BJOmc/8rxTR8xzSd6G3mTFd4szktl/MixwkzPy96yZFEj68IpT633osAPri/H\n\t1hkLsZQJJ69MoKPzPgaZgVgwjYFBCm+jTSBG01PQIUQlNm3WHb3LRGSZ+8a9iUBib6KB8Xoxq2H\n\t2fzsXxkwyHcRGKNu4RamxoyZAIfzOIfiLMJiTjDtP8miCX8RZmyubF6ueWuupo+q14izmITzPjd\n\tqXP7BJc01coM5KVb0cUADtkOzn3Mh56Wfd+ltCsGEDw==","X-Google-Smtp-Source":"AGHT+IHSjNsYTMgVxEuC/RBdJ23Pfo9VGVH4d83MFxJ1R/fuA+VxwgGT1hXql0gklPeNPavgW+kv1Q==","X-Received":"by 2002:adf:eec7:0:b0:3a4:dbdf:7152 with SMTP id\n\tffacd0b85a97d-3b60dd87cc9mr5469220f8f.14.1753084138335; \n\tMon, 21 Jul 2025 00:48:58 -0700 (PDT)","From":"Naushir Patuck <naush@raspberrypi.com>","To":"libcamera-devel@lists.libcamera.org","Cc":"David Plowman <david.plowman@raspberrypi.com>,\n\tNaushir Patuck <naush@raspberrypi.com>","Subject":"[PATCH v2 5/7] ipa: rpi: agc: Calculate digital gain in process()","Date":"Mon, 21 Jul 2025 08:47:25 +0100","Message-ID":"<20250721074853.1463358-6-naush@raspberrypi.com>","X-Mailer":"git-send-email 2.43.0","In-Reply-To":"<20250721074853.1463358-1-naush@raspberrypi.com>","References":"<20250721074853.1463358-1-naush@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":"From: David Plowman <david.plowman@raspberrypi.com>\n\nPreviously we let prepare() do the work by comparing the desired total\nexposure against the shutter time and analogue gain. This can cause\nthe image to \"wink\" at high framerates because we may skip running\nprepare() to get the new digital gain even when the delayed AGC status\n(which came out of an earlier call to process()) shows that a change\nwas required.\n\nNow we're taking explicit control of the digital gain by calculating\nit ourselves so that we can output it in the standard AgcStatus\nobject. This means that whenever the delayed AGC status changes, we\nhave the correct digital gain to go with it.\n\nSigned-off-by: David Plowman <david.plowman@raspberrypi.com>\nSigned-off-by: Naushir Patuck <naush@raspberrypi.com>\nReviewed-by: Naushir Patuck <naush@raspberrypi.com>\n---\n src/ipa/rpi/controller/agc_status.h        |   1 +\n src/ipa/rpi/controller/rpi/agc_channel.cpp | 163 +++++++++------------\n src/ipa/rpi/controller/rpi/agc_channel.h   |   1 +\n 3 files changed, 75 insertions(+), 90 deletions(-)","diff":"diff --git a/src/ipa/rpi/controller/agc_status.h b/src/ipa/rpi/controller/agc_status.h\nindex d4cedcf49c3c..956d6abf398c 100644\n--- a/src/ipa/rpi/controller/agc_status.h\n+++ b/src/ipa/rpi/controller/agc_status.h\n@@ -30,6 +30,7 @@ struct AgcStatus {\n \tlibcamera::utils::Duration targetExposureValue; /* (unfiltered) target total exposure AGC is aiming for */\n \tlibcamera::utils::Duration exposureTime;\n \tdouble analogueGain;\n+\tdouble digitalGain;\n \tstd::string exposureMode;\n \tstd::string constraintMode;\n \tstd::string meteringMode;\ndiff --git a/src/ipa/rpi/controller/rpi/agc_channel.cpp b/src/ipa/rpi/controller/rpi/agc_channel.cpp\nindex 9e4661616051..7da1c6dbbec7 100644\n--- a/src/ipa/rpi/controller/rpi/agc_channel.cpp\n+++ b/src/ipa/rpi/controller/rpi/agc_channel.cpp\n@@ -266,7 +266,7 @@ int AgcConfig::read(const libcamera::YamlObject &params)\n }\n \n AgcChannel::ExposureValues::ExposureValues()\n-\t: exposureTime(0s), analogueGain(0),\n+\t: exposureTime(0s), analogueGain(0), digitalGain(0),\n \t  totalExposure(0s), totalExposureNoDG(0s)\n {\n }\n@@ -434,17 +434,10 @@ void AgcChannel::switchMode(CameraMode const &cameraMode,\n \tmode_ = cameraMode;\n \n \tDuration fixedExposureTime = limitExposureTime(fixedExposureTime_);\n+\tdouble fixedGain = limitGain(fixedGain_);\n \tif (fixedExposureTime && fixedGain_) {\n-\t\t/* This is the equivalent of computeTargetExposure and applyDigitalGain. */\n-\t\ttarget_.totalExposureNoDG = fixedExposureTime_ * fixedGain_;\n-\t\ttarget_.totalExposure = target_.totalExposureNoDG;\n-\n-\t\t/* Equivalent of filterExposure. This resets any \"history\". */\n-\t\tfiltered_ = target_;\n-\n-\t\t/* Equivalent of divideUpExposure. */\n-\t\tfiltered_.exposureTime = fixedExposureTime;\n-\t\tfiltered_.analogueGain = fixedGain_;\n+\t\tfiltered_.totalExposureNoDG = fixedExposureTime * fixedGain;\n+\t\tfiltered_.totalExposure = filtered_.totalExposureNoDG;\n \t} else if (status_.totalExposureValue) {\n \t\t/*\n \t\t * On a mode switch, various things could happen:\n@@ -457,12 +450,8 @@ void AgcChannel::switchMode(CameraMode const &cameraMode,\n \t\t */\n \n \t\tdouble ratio = lastSensitivity / cameraMode.sensitivity;\n-\t\ttarget_.totalExposure *= ratio;\n-\t\ttarget_.totalExposureNoDG = target_.totalExposure;\n \t\tfiltered_.totalExposure *= ratio;\n \t\tfiltered_.totalExposureNoDG = filtered_.totalExposure;\n-\n-\t\tdivideUpExposure();\n \t} else {\n \t\t/*\n \t\t * We come through here on startup, when at least one of the\n@@ -472,55 +461,46 @@ void AgcChannel::switchMode(CameraMode const &cameraMode,\n \t\t * weren't set.\n \t\t */\n \n-\t\t/* Equivalent of divideUpExposure. */\n-\t\tfiltered_.exposureTime = fixedExposureTime ? fixedExposureTime : config_.defaultExposureTime;\n-\t\tfiltered_.analogueGain = fixedGain_ ? fixedGain_ : config_.defaultAnalogueGain;\n+\t\tDuration exposureTime = fixedExposureTime ? fixedExposureTime : config_.defaultExposureTime;\n+\t\tdouble gain = fixedGain ? fixedGain : config_.defaultAnalogueGain;\n+\t\tfiltered_.totalExposure = exposureTime * gain;\n+\t\tfiltered_.totalExposureNoDG = filtered_.totalExposure;\n \t}\n \n+\t/* Setting target_ to filtered_ removes any history from before the mode switch. */\n+\ttarget_ = filtered_;\n+\tdivideUpExposure();\n+\n \twriteAndFinish(metadata, false);\n }\n \n void AgcChannel::prepare(Metadata *imageMetadata)\n {\n-\tDuration totalExposureValue = status_.totalExposureValue;\n-\tAgcStatus delayedStatus;\n+\tDeviceStatus deviceStatus;\n \tAgcPrepareStatus prepareStatus;\n \n-\t/* Fetch the AWB status now because AWB also sets it in the prepare method. */\n-\tfetchAwbStatus(imageMetadata);\n-\n-\tif (!imageMetadata->get(\"agc.delayed_status\", delayedStatus))\n-\t\ttotalExposureValue = delayedStatus.totalExposureValue;\n-\n-\tprepareStatus.digitalGain = 1.0;\n \tprepareStatus.locked = false;\n+\tprepareStatus.digitalGain = 1.0;\n \n-\tif (status_.totalExposureValue) {\n-\t\t/* Process has run, so we have meaningful values. */\n-\t\tDeviceStatus deviceStatus;\n-\t\tif (imageMetadata->get(\"device.status\", deviceStatus) == 0) {\n-\t\t\tDuration actualExposure = deviceStatus.exposureTime *\n-\t\t\t\t\t\t  deviceStatus.analogueGain;\n-\t\t\tif (actualExposure) {\n-\t\t\t\tdouble digitalGain = totalExposureValue / actualExposure;\n-\t\t\t\tLOG(RPiAgc, Debug) << \"Want total exposure \" << totalExposureValue;\n-\t\t\t\t/*\n-\t\t\t\t * Never ask for a gain < 1.0, and also impose\n-\t\t\t\t * some upper limit. Make it customisable?\n-\t\t\t\t */\n-\t\t\t\tprepareStatus.digitalGain = std::max(1.0, std::min(digitalGain,\n-\t\t\t\t\t\t\t\t\t\t   config_.maxDigitalGain));\n-\t\t\t\tLOG(RPiAgc, Debug) << \"Actual exposure \" << actualExposure;\n-\t\t\t\tLOG(RPiAgc, Debug) << \"Use digitalGain \" << prepareStatus.digitalGain;\n-\t\t\t\tLOG(RPiAgc, Debug) << \"Effective exposure \"\n-\t\t\t\t\t\t   << actualExposure * prepareStatus.digitalGain;\n-\t\t\t\t/* Decide whether AEC/AGC has converged. */\n-\t\t\t\tprepareStatus.locked = updateLockStatus(deviceStatus);\n-\t\t\t}\n-\t\t} else\n-\t\t\tLOG(RPiAgc, Warning) << \"AgcChannel: no device metadata\";\n-\t\timageMetadata->set(\"agc.prepare_status\", prepareStatus);\n+\tif (!imageMetadata->get(\"device.status\", deviceStatus)) {\n+\t\tprepareStatus.locked = updateLockStatus(deviceStatus);\n+\n+\t\t/*\n+\t\t * For now, the IPA code is still expecting the digital gain to come back in\n+\t\t * the prepare_status. To keep things happy, we'll just fill in the value that\n+\t\t * we calculated previously and put in the AgcStatus (which comes back as the\n+\t\t * \"delayed\" status). Once the rest of the IPA code is updated, we'll be able\n+\t\t * to remove this, and indeed remove the digitalGain from the AgcPrepareStatus.\n+\t\t */\n+\t\tAgcStatus delayedStatus;\n+\t\tif (!imageMetadata->get(\"agc.delayed_status\", delayedStatus))\n+\t\t\tprepareStatus.digitalGain = delayedStatus.digitalGain;\n+\t\telse\n+\t\t\t/* After a mode switch, this must be correct until new values come through. */\n+\t\t\tprepareStatus.digitalGain = status_.digitalGain;\n \t}\n+\n+\timageMetadata->set(\"agc.prepare_status\", prepareStatus);\n }\n \n void AgcChannel::process(StatisticsPtr &stats, DeviceStatus const &deviceStatus,\n@@ -606,7 +586,7 @@ void AgcChannel::housekeepConfig()\n \t/* First fetch all the up-to-date settings, so no one else has to do it. */\n \tstatus_.ev = ev_;\n \tstatus_.fixedExposureTime = limitExposureTime(fixedExposureTime_);\n-\tstatus_.fixedGain = fixedGain_;\n+\tstatus_.fixedGain = limitGain(fixedGain_);\n \tstatus_.flickerPeriod = flickerPeriod_;\n \tLOG(RPiAgc, Debug) << \"ev \" << status_.ev << \" fixedExposureTime \"\n \t\t\t   << status_.fixedExposureTime << \" fixedGain \"\n@@ -657,6 +637,9 @@ void AgcChannel::fetchCurrentExposure(DeviceStatus const &deviceStatus)\n \tcurrent_.analogueGain = deviceStatus.analogueGain;\n \tcurrent_.totalExposure = 0s; /* this value is unused */\n \tcurrent_.totalExposureNoDG = current_.exposureTime * current_.analogueGain;\n+\tLOG(RPiAgc, Debug) << \"Current frame: exposure time \" << current_.exposureTime\n+\t\t\t   << \" ag \" << current_.analogueGain\n+\t\t\t   << \" (total \" << current_.totalExposureNoDG << \")\";\n }\n \n void AgcChannel::fetchAwbStatus(Metadata *imageMetadata)\n@@ -808,14 +791,12 @@ void AgcChannel::computeTargetExposure(double gain)\n \t\ttarget_.totalExposure = current_.totalExposureNoDG * gain;\n \t\t/* The final target exposure is also limited to what the exposure mode allows. */\n \t\tDuration maxExposureTime = status_.fixedExposureTime\n-\t\t\t\t\t ? status_.fixedExposureTime\n-\t\t\t\t\t : exposureMode_->exposureTime.back();\n+\t\t\t\t\t      ? status_.fixedExposureTime\n+\t\t\t\t\t      : exposureMode_->exposureTime.back();\n \t\tmaxExposureTime = limitExposureTime(maxExposureTime);\n-\t\tDuration maxTotalExposure =\n-\t\t\tmaxExposureTime *\n-\t\t\t(status_.fixedGain != 0.0\n-\t\t\t\t ? status_.fixedGain\n-\t\t\t\t : exposureMode_->gain.back());\n+\t\tdouble maxGain = status_.fixedGain ? status_.fixedGain : exposureMode_->gain.back();\n+\t\tmaxGain = limitGain(maxGain);\n+\t\tDuration maxTotalExposure = maxExposureTime * maxGain;\n \t\ttarget_.totalExposure = std::min(target_.totalExposure, maxTotalExposure);\n \t}\n \tLOG(RPiAgc, Debug) << \"Target totalExposure \" << target_.totalExposure;\n@@ -824,8 +805,6 @@ void AgcChannel::computeTargetExposure(double gain)\n bool AgcChannel::applyChannelConstraints(const AgcChannelTotalExposures &channelTotalExposures)\n {\n \tbool channelBound = false;\n-\tLOG(RPiAgc, Debug)\n-\t\t<< \"Total exposure before channel constraints \" << filtered_.totalExposure;\n \n \tfor (const auto &constraint : config_.channelConstraints) {\n \t\tLOG(RPiAgc, Debug)\n@@ -860,7 +839,7 @@ bool AgcChannel::applyChannelConstraints(const AgcChannelTotalExposures &channel\n \n bool AgcChannel::applyDigitalGain(double gain, double targetY, bool channelBound)\n {\n-\tdouble dg = 1.0;\n+\tfiltered_.totalExposureNoDG = filtered_.totalExposure;\n \n \t/*\n \t * Finally, if we're trying to reduce exposure but the target_Y is\n@@ -871,15 +850,14 @@ bool AgcChannel::applyDigitalGain(double gain, double targetY, bool channelBound\n \t * quickly (and we then approach the correct value more quickly from\n \t * below).\n \t */\n-\tbool desaturate = false;\n-\tif (config_.desaturate)\n-\t\tdesaturate = !channelBound &&\n-\t\t\t     targetY > config_.fastReduceThreshold && gain < sqrt(targetY);\n-\tif (desaturate)\n-\t\tdg /= config_.fastReduceThreshold;\n-\tLOG(RPiAgc, Debug) << \"Digital gain \" << dg << \" desaturate? \" << desaturate;\n-\tfiltered_.totalExposureNoDG = filtered_.totalExposure / dg;\n-\tLOG(RPiAgc, Debug) << \"Target totalExposureNoDG \" << filtered_.totalExposureNoDG;\n+\tbool desaturate = config_.desaturate && !channelBound &&\n+\t\t\t  targetY > config_.fastReduceThreshold && gain < sqrt(targetY);\n+\n+\tif (desaturate) {\n+\t\tfiltered_.totalExposureNoDG *= config_.fastReduceThreshold;\n+\t\tLOG(RPiAgc, Debug) << \"Desaturating, exposure no dg \" << filtered_.totalExposureNoDG;\n+\t}\n+\n \treturn desaturate;\n }\n \n@@ -915,8 +893,7 @@ void AgcChannel::filterExposure()\n \t\tfiltered_.totalExposure = speed * target_.totalExposure +\n \t\t\t\t\t  filtered_.totalExposure * (1.0 - speed);\n \t}\n-\tLOG(RPiAgc, Debug) << \"After filtering, totalExposure \" << filtered_.totalExposure\n-\t\t\t   << \" no dg \" << filtered_.totalExposureNoDG;\n+\tLOG(RPiAgc, Debug) << \"After filtering, totalExposure \" << filtered_.totalExposure;\n }\n \n void AgcChannel::divideUpExposure()\n@@ -957,9 +934,7 @@ void AgcChannel::divideUpExposure()\n \t\t\t}\n \t\t}\n \t}\n-\tLOG(RPiAgc, Debug)\n-\t\t<< \"Divided up exposure time and gain are \" << exposureTime\n-\t\t<< \" and \" << gain;\n+\n \t/*\n \t * Finally adjust exposure time for flicker avoidance (require both\n \t * exposure time and gain not to be fixed).\n@@ -970,22 +945,30 @@ void AgcChannel::divideUpExposure()\n \t\tif (flickerPeriods) {\n \t\t\tDuration newExposureTime = flickerPeriods * status_.flickerPeriod;\n \t\t\tgain *= exposureTime / newExposureTime;\n-\t\t\t/*\n-\t\t\t * We should still not allow the ag to go over the\n-\t\t\t * largest value in the exposure mode. Note that this\n-\t\t\t * may force more of the total exposure into the digital\n-\t\t\t * gain as a side-effect.\n-\t\t\t */\n-\t\t\tgain = std::min(gain, exposureMode_->gain.back());\n-\t\t\tgain = limitGain(gain);\n \t\t\texposureTime = newExposureTime;\n \t\t}\n \t\tLOG(RPiAgc, Debug) << \"After flicker avoidance, exposure time \"\n \t\t\t\t   << exposureTime << \" gain \" << gain;\n \t}\n+\n+\t/* Limit analogue gain to maximum allowed. */\n+\tdouble analogueGain = std::min(gain, mode_.maxAnalogueGain);\n+\n+\t/* Finally work out the digital gain that we will need. */\n+\tfiltered_.totalExposureNoDG = analogueGain * exposureTime;\n+\tdouble digitalGain = filtered_.totalExposure / filtered_.totalExposureNoDG;\n+\t/* Limit dg by what is allowed. */\n+\tdigitalGain = std::min(digitalGain, config_.maxDigitalGain);\n+\t/* Update total exposure, in case the dg went down. */\n+\tfiltered_.totalExposure = filtered_.totalExposureNoDG * digitalGain;\n+\n \tfiltered_.exposureTime = exposureTime;\n-\t/* We ask for all the gain as analogue gain; prepare() will be told what we got. */\n-\tfiltered_.analogueGain = gain;\n+\tfiltered_.analogueGain = analogueGain;\n+\tfiltered_.digitalGain = digitalGain;\n+\tLOG(RPiAgc, Debug) << \"DivideUpExposure: total \" << filtered_.totalExposure\n+\t\t\t   << \" no dg \" << filtered_.totalExposureNoDG;\n+\tLOG(RPiAgc, Debug) << \"DivideUpExposure: exp \" << exposureTime\n+\t\t\t   << \" ag \" << gain << \" dg \" << digitalGain;\n }\n \n void AgcChannel::writeAndFinish(Metadata *imageMetadata, bool desaturate)\n@@ -994,6 +977,7 @@ void AgcChannel::writeAndFinish(Metadata *imageMetadata, bool desaturate)\n \tstatus_.targetExposureValue = desaturate ? 0s : target_.totalExposure;\n \tstatus_.exposureTime = filtered_.exposureTime;\n \tstatus_.analogueGain = filtered_.analogueGain;\n+\tstatus_.digitalGain = filtered_.digitalGain;\n \t/*\n \t * Write to metadata as well, in case anyone wants to update the camera\n \t * immediately.\n@@ -1001,8 +985,6 @@ void AgcChannel::writeAndFinish(Metadata *imageMetadata, bool desaturate)\n \timageMetadata->set(\"agc.status\", status_);\n \tLOG(RPiAgc, Debug) << \"Output written, total exposure requested is \"\n \t\t\t   << filtered_.totalExposure;\n-\tLOG(RPiAgc, Debug) << \"Camera exposure update: exposure time \" << filtered_.exposureTime\n-\t\t\t   << \" analogue gain \" << filtered_.analogueGain;\n }\n \n Duration AgcChannel::limitExposureTime(Duration exposureTime)\n@@ -1031,6 +1013,7 @@ double AgcChannel::limitGain(double gain) const\n \tif (!gain)\n \t\treturn gain;\n \n-\tgain = std::max(gain, mode_.minAnalogueGain);\n+\tgain = std::clamp(gain, mode_.minAnalogueGain,\n+\t\t\t  mode_.maxAnalogueGain * config_.maxDigitalGain);\n \treturn gain;\n }\ndiff --git a/src/ipa/rpi/controller/rpi/agc_channel.h b/src/ipa/rpi/controller/rpi/agc_channel.h\nindex 93229128abf1..42d85ec15e8d 100644\n--- a/src/ipa/rpi/controller/rpi/agc_channel.h\n+++ b/src/ipa/rpi/controller/rpi/agc_channel.h\n@@ -135,6 +135,7 @@ private:\n \n \t\tlibcamera::utils::Duration exposureTime;\n \t\tdouble analogueGain;\n+\t\tdouble digitalGain;\n \t\tlibcamera::utils::Duration totalExposure;\n \t\tlibcamera::utils::Duration totalExposureNoDG; /* without digital gain */\n \t};\n","prefixes":["v2","5/7"]}