From patchwork Mon Oct 26 13:20:45 2026 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= X-Patchwork-Id: 28640 Return-Path: X-Original-To: parsemail@patchwork.libcamera.org Delivered-To: parsemail@patchwork.libcamera.org Received: from lancelot.ideasonboard.com (lancelot.ideasonboard.com [92.243.16.209]) by patchwork.libcamera.org (Postfix) with ESMTPS id C4BF7C335A for ; Mon, 26 Oct 2026 13:21:24 +0000 (UTC) Received: from lancelot.ideasonboard.com (localhost [IPv6:::1]) by lancelot.ideasonboard.com (Postfix) with ESMTP id BF8B669045; Mon, 26 Oct 2026 14:21:23 +0100 (CET) Authentication-Results: lancelot.ideasonboard.com; dkim=pass (1024-bit key; unprotected) header.d=ideasonboard.com header.i=@ideasonboard.com header.b="LbsJufF8"; dkim-atps=neutral Received: from perceval.ideasonboard.com (perceval.ideasonboard.com [IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647]) by lancelot.ideasonboard.com (Postfix) with ESMTPS id 7551A68FDC for ; Mon, 26 Oct 2026 14:20:54 +0100 (CET) Received: from pb-laptop.local (185.221.143.99.nat.pool.zt.hu [185.221.143.99]) by perceval.ideasonboard.com (Postfix) with ESMTPSA id 42DCDD0; Mon, 26 Oct 2026 14:18:43 +0100 (CET) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com; s=mail; t=1793020723; bh=xBMyJ/AK3TfFCJY0KlzP7pKmY/Bp9H2x+6M4mWS6LKY=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=LbsJufF8ahKeuutx/uY2YnWNjRicHe4Lwu2TRmWbk6hJOWDT/CflGHaRVez7SpgGM ML4LoAVZzS+ZWEuoLtKGIfEUFDGcuoIzDuzgTRgP0f9VnOum5/7uclvbTXp/ShX5oT gToYDGxq3Dhj84yx3hELvLIq9Cu0P1y9U9MYyiD0= From: =?utf-8?q?Barnab=C3=A1s_P=C5=91cze?= To: libcamera-devel@lists.libcamera.org Cc: Jacopo Mondi Subject: [RFC PATCH v4 23/23] ipa: libipa: agc: Take exposure margin into account Date: Mon, 26 Oct 2026 14:20:45 +0100 Message-ID: <20261026132045.269007-24-barnabas.pocze@ideasonboard.com> X-Mailer: git-send-email 2.56.0 In-Reply-To: <20261026132045.269007-1-barnabas.pocze@ideasonboard.com> References: <20261026132045.269007-1-barnabas.pocze@ideasonboard.com> MIME-Version: 1.0 X-BeenThere: libcamera-devel@lists.libcamera.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: libcamera-devel-bounces@lists.libcamera.org Sender: "libcamera-devel" The sensor exposure time depends on the vertical blanking amount and the exposure margin. Instead of relying on the maximum exposure available at onfiguration, use the frame duration limits (~ vblank) and exposure margin to dynamically calculate the max available exposure time. Signed-off-by: Barnabás Pőcze Tested-by: Jacopo Mondi --- src/ipa/libipa/agc.cpp | 92 +++++++++++++++++++++++++++++++----------- src/ipa/libipa/agc.h | 3 +- 2 files changed, 71 insertions(+), 24 deletions(-) diff --git a/src/ipa/libipa/agc.cpp b/src/ipa/libipa/agc.cpp index 90f51ff035..a8eb74c680 100644 --- a/src/ipa/libipa/agc.cpp +++ b/src/ipa/libipa/agc.cpp @@ -72,12 +72,12 @@ namespace agc { * \struct Session * \brief Session configuration for AgcAlgorithm * + * \var Session::minExposure + * \brief Minimum exposure (in lines) for the streaming session + * * \var Session::minExposureTime * \brief Minimum exposure time for the streaming session * - * \var Session::maxExposureTime - * \brief Maximum exposure time for the streaming session - * * \var Session::minAnalogueGain * \brief Minimum analogue gain for the streaming session * @@ -102,6 +102,11 @@ namespace agc { * \var Session::sensor.outputSize * \brief Configured output size of the sensor * + * \var Session::sensor.exposureMargin + * \brief Exposure margin of the sensor + * + * \sa CameraSensorHelper::exposureMargin() + * * \var Session::autoAllowed * \copybrief AgcAlgorithm::ConfigurationParams::autoAllowed * \sa AgcAlgorithm::ConfigurationParams::autoAllowed @@ -473,6 +478,23 @@ void fillMissing(AgcMeanLuminance::Configuration &config) } } +[[nodiscard]] +uint32_t clampExposure(const agc::Session &session, const agc::ActiveState &state, + uint32_t exposure) +{ + const uint32_t maxFrame = state.maxFrameDuration / session.lineDuration; + const uint32_t maxExposure = maxFrame - session.sensor.exposureMargin; + + return std::clamp(exposure, session.minExposure, maxExposure); +} + +[[nodiscard]] +uint32_t clampExposure(const agc::Session &session, const agc::ActiveState &state, + utils::Duration exposureTime) +{ + return clampExposure(session, state, exposureTime / session.lineDuration); +} + } /* namespace */ /** @@ -620,7 +642,12 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, session.lineDuration = lineLength * 1.0s / config.sensorInfo.pixelRate; session.sensor.outputSize = config.sensorInfo.outputSize; - const double lineDurationUs = session.lineDuration.get(); + auto exposureMargin = sensor_ ? sensor_->exposureMargin() : std::nullopt; + session.sensor.exposureMargin = exposureMargin.value_or(4); + if (!exposureMargin) + LOG(Agc, Warning) + << "Sensor exposure margin not available, using " + << session.sensor.exposureMargin; /* * Compute exposure time limits from the V4L2_CID_EXPOSURE control @@ -629,7 +656,6 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, const ControlInfo &v4l2Exposure = config.sensorControls.find(V4L2_CID_EXPOSURE)->second; int32_t minExposure = v4l2Exposure.min().get(); - int32_t maxExposure = v4l2Exposure.max().get(); int32_t defExposure = v4l2Exposure.def().get(); /* Compute the analogue gain limits. */ @@ -642,28 +668,46 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, float maxGain = extractGain(v4l2Gain.max()); float defGain = extractGain(v4l2Gain.def()); - LOG(Agc, Debug) - << "exposure: [" << minExposure << ',' << maxExposure << "], " - << "gain: [" << minGain << ',' << maxGain << "], " - << "line-duration: " << session.lineDuration << ", " - << "sensor-output: " << session.sensor.outputSize; - - /* * When the AGC computes the new exposure values for a frame, it needs * to know the limits for exposure time and analogue gain. As it depends * on the sensor, update it with the controls. - * - * \todo take VBLANK into account for maximum exposure time */ + session.minExposure = minExposure; session.minExposureTime = minExposure * session.lineDuration; - session.maxExposureTime = maxExposure * session.lineDuration; session.minAnalogueGain = minGain; session.maxAnalogueGain = maxGain; session.defAnalogueGain = defGain; session.minFrameDuration = config.sensorInfo.minFrameLength * session.lineDuration; session.maxFrameDuration = config.sensorInfo.maxFrameLength * session.lineDuration; + /* + * The assumption is that the minimum exposure time is independent of vblank, + * only the maximum exposure time is affected by it. + */ + const uint32_t maxExposure = config.sensorInfo.maxFrameLength - session.sensor.exposureMargin; + const utils::Duration maxExposureTime = maxExposure * session.lineDuration; + + LOG(Agc, Debug) + << "exposure: [" << session.minExposure << ',' << maxExposure << "], " + << "exposure-time: [" << session.minExposureTime << ',' << maxExposureTime << "], " + << "gain: [" << session.minAnalogueGain << ',' << session.maxAnalogueGain << "], " + << "line-length: " << lineLength << ", " + << "line-duration: " << session.lineDuration << ", " + << "frame-height: [" << config.sensorInfo.minFrameLength << ',' << config.sensorInfo.maxFrameLength << "], " + << "sensor-output: " << session.sensor.outputSize << ", " + << "sensor-exposure-margin: " << session.sensor.exposureMargin; + + /* + * This is a sanity check. If this fails, something is very likely off. + * Equality could theoretically be allowed, but it is not so that + * `(frameDuration / lineDuration) - exposureMargin` is no less + * than the minimum exposure time even if the division is slightly + * inaccurate. + */ + if (session.minExposure + session.sensor.exposureMargin >= config.sensorInfo.minFrameLength) + return -EINVAL; + /* Configure the default exposure and gain. */ state = {}; state.automatic.gain = session.minAnalogueGain; @@ -755,9 +799,10 @@ int AgcAlgorithm::configure(agc::Session &session, agc::ActiveState &state, minGain, maxGain, defGain }; config.ctrlMap[&controls::ExposureTime] = ControlInfo{ - static_cast(minExposure * lineDurationUs), - static_cast(maxExposure * lineDurationUs), - static_cast(defExposure * lineDurationUs), + static_cast(session.minExposureTime.get()), + static_cast(maxExposureTime.get()), + static_cast(state.automatic.exposure * + session.lineDuration.get()), }; config.ctrlMap[&controls::FrameDurationLimits] = ControlInfo{ static_cast(session.minFrameDuration.get()), @@ -864,7 +909,7 @@ void AgcAlgorithm::queueRequest(const agc::Session &session, agc::ActiveState &s const auto &exposure = controls.get(controls::ExposureTime); if (exposure && !state.autoExposureEnabled) { - state.manual.exposure = *exposure * 1.0us / session.lineDuration; + state.manual.exposure = clampExposure(session, state, *exposure * 1.0us); LOG(Agc, Debug) << "Set exposure to " << state.manual.exposure; } @@ -1002,9 +1047,8 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state, if (state.autoExposureEnabled) { minExposureTime = session.minExposureTime; - maxExposureTime = std::clamp(state.maxFrameDuration, - session.minExposureTime, - session.maxExposureTime); + maxExposureTime = state.maxFrameDuration - + session.sensor.exposureMargin * lineDuration; } else { minExposureTime = lineDuration * state.manual.exposure; maxExposureTime = minExposureTime; @@ -1076,6 +1120,8 @@ void AgcAlgorithm::process(const agc::Session &session, agc::ActiveState &state, }, }, impl_); + state.automatic.exposure = clampExposure(session, state, state.automatic.exposure); + const utils::Duration newExposureTime = state.automatic.exposure * lineDuration; LOG(Agc, Debug) @@ -1106,7 +1152,7 @@ void AgcAlgorithm::processFrameDuration(const agc::Session &session, const auto frameLength = std::max( std::min(std::ceil(frameContext.minFrameDuration / lineDuration), frameContext.maxFrameDuration / lineDuration), - frameContext.exposure); + frameContext.exposure + session.sensor.exposureMargin); frameContext.vblank = frameLength - session.sensor.outputSize.height; diff --git a/src/ipa/libipa/agc.h b/src/ipa/libipa/agc.h index 44f5218b0c..7bfc85189f 100644 --- a/src/ipa/libipa/agc.h +++ b/src/ipa/libipa/agc.h @@ -36,8 +36,8 @@ class Histogram; namespace agc { struct Session { + uint32_t minExposure; utils::Duration minExposureTime; - utils::Duration maxExposureTime; double minAnalogueGain; double maxAnalogueGain; double defAnalogueGain; @@ -47,6 +47,7 @@ struct Session { struct { Size outputSize; + uint32_t exposureMargin; } sensor; bool autoAllowed;