[{"id":27682,"web_url":"https://patchwork.libcamera.org/comment/27682/","msgid":"<CAEmqJPoY91riFPHedVsh0D-x4-VGx599EB-jkb_yNbMmv6890g@mail.gmail.com>","date":"2023-08-22T10:25:55","subject":"Re: [libcamera-devel] [PATCH 2/5] ipa: rpi: agc: Reorganise code\n\tfor multi-channel AGC","submitter":{"id":34,"url":"https://patchwork.libcamera.org/api/people/34/","name":"Naushir Patuck","email":"naush@raspberrypi.com"},"content":"Hi David,\n\nThank you for this work.\n\nOn Mon, 31 Jul 2023 at 10:47, David Plowman via libcamera-devel\n<libcamera-devel@lists.libcamera.org> wrote:\n>\n> This commit does the basic reorganisation of the code in order to\n> implement multi-channel AGC. The main changes are:\n>\n> * The previous Agc class (in agc.cpp) has become the AgcChannel class\n>   in (agc_channel.cpp).\n>\n> * A new Agc class is introduced which is a wrapper round a number of\n>   AgcChannels.\n>\n> * The basic plumbing from ipa_base.cpp to Agc is updated to include a\n>   channel number. All the existing controls are hardwired to talk\n>   directly to channel 0.\n>\n> There are a couple of limitations which we expect to apply to\n> multi-channel AGC. We're not allowing different frame durations to be\n> applied to the channels, nor are we allowing separate metering\n> modes. To be fair, supporting these things is not impossible, but\n> there are reasons why it may be tricky so they remain \"TBD\" for now.\n>\n> This patch only includes the basic reorganisation and plumbing. It\n> does not yet update the important methods (switchMode, prepare and\n> process) to implement multi-channel AGC properly. This will appear in\n> a subsequent commit. For now, these functions are hard-coded just to\n> use channel 0, thereby preserving the existing behaviour.\n>\n> Signed-off-by: David Plowman <david.plowman@raspberrypi.com>\n\nThis big patch seems to really be moving code around (thankfully!)\n\nReviewed-by: Naushir Patuck <naush@raspberrypi.com>\n\n> ---\n>  src/ipa/rpi/common/ipa_base.cpp            |  14 +-\n>  src/ipa/rpi/controller/agc_algorithm.h     |  19 +-\n>  src/ipa/rpi/controller/meson.build         |   1 +\n>  src/ipa/rpi/controller/rpi/agc.cpp         | 910 +++-----------------\n>  src/ipa/rpi/controller/rpi/agc.h           | 122 +--\n>  src/ipa/rpi/controller/rpi/agc_channel.cpp | 927 +++++++++++++++++++++\n>  src/ipa/rpi/controller/rpi/agc_channel.h   | 135 +++\n>  7 files changed, 1216 insertions(+), 912 deletions(-)\n>  create mode 100644 src/ipa/rpi/controller/rpi/agc_channel.cpp\n>  create mode 100644 src/ipa/rpi/controller/rpi/agc_channel.h\n>\n> diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp\n> index 6ae84cc6..f29c32fd 100644\n> --- a/src/ipa/rpi/common/ipa_base.cpp\n> +++ b/src/ipa/rpi/common/ipa_base.cpp\n> @@ -692,9 +692,9 @@ void IpaBase::applyControls(const ControlList &controls)\n>                         }\n>\n>                         if (ctrl.second.get<bool>() == false)\n> -                               agc->disableAuto();\n> +                               agc->disableAuto(0);\n>                         else\n> -                               agc->enableAuto();\n> +                               agc->enableAuto(0);\n>\n>                         libcameraMetadata_.set(controls::AeEnable, ctrl.second.get<bool>());\n>                         break;\n> @@ -710,7 +710,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>                         }\n>\n>                         /* The control provides units of microseconds. */\n> -                       agc->setFixedShutter(ctrl.second.get<int32_t>() * 1.0us);\n> +                       agc->setFixedShutter(0, ctrl.second.get<int32_t>() * 1.0us);\n>\n>                         libcameraMetadata_.set(controls::ExposureTime, ctrl.second.get<int32_t>());\n>                         break;\n> @@ -725,7 +725,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>                                 break;\n>                         }\n>\n> -                       agc->setFixedAnalogueGain(ctrl.second.get<float>());\n> +                       agc->setFixedAnalogueGain(0, ctrl.second.get<float>());\n>\n>                         libcameraMetadata_.set(controls::AnalogueGain,\n>                                                ctrl.second.get<float>());\n> @@ -763,7 +763,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>\n>                         int32_t idx = ctrl.second.get<int32_t>();\n>                         if (ConstraintModeTable.count(idx)) {\n> -                               agc->setConstraintMode(ConstraintModeTable.at(idx));\n> +                               agc->setConstraintMode(0, ConstraintModeTable.at(idx));\n>                                 libcameraMetadata_.set(controls::AeConstraintMode, idx);\n>                         } else {\n>                                 LOG(IPARPI, Error) << \"Constraint mode \" << idx\n> @@ -783,7 +783,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>\n>                         int32_t idx = ctrl.second.get<int32_t>();\n>                         if (ExposureModeTable.count(idx)) {\n> -                               agc->setExposureMode(ExposureModeTable.at(idx));\n> +                               agc->setExposureMode(0, ExposureModeTable.at(idx));\n>                                 libcameraMetadata_.set(controls::AeExposureMode, idx);\n>                         } else {\n>                                 LOG(IPARPI, Error) << \"Exposure mode \" << idx\n> @@ -806,7 +806,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>                          * So convert to 2^EV\n>                          */\n>                         double ev = pow(2.0, ctrl.second.get<float>());\n> -                       agc->setEv(ev);\n> +                       agc->setEv(0, ev);\n>                         libcameraMetadata_.set(controls::ExposureValue,\n>                                                ctrl.second.get<float>());\n>                         break;\n> diff --git a/src/ipa/rpi/controller/agc_algorithm.h b/src/ipa/rpi/controller/agc_algorithm.h\n> index b6949daa..b8986560 100644\n> --- a/src/ipa/rpi/controller/agc_algorithm.h\n> +++ b/src/ipa/rpi/controller/agc_algorithm.h\n> @@ -21,16 +21,19 @@ public:\n>         /* An AGC algorithm must provide the following: */\n>         virtual unsigned int getConvergenceFrames() const = 0;\n>         virtual std::vector<double> const &getWeights() const = 0;\n> -       virtual void setEv(double ev) = 0;\n> -       virtual void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) = 0;\n> -       virtual void setFixedShutter(libcamera::utils::Duration fixedShutter) = 0;\n> +       virtual void setEv(unsigned int channel, double ev) = 0;\n> +       virtual void setFlickerPeriod(unsigned int channel,\n> +                                     libcamera::utils::Duration flickerPeriod) = 0;\n> +       virtual void setFixedShutter(unsigned int channel,\n> +                                    libcamera::utils::Duration fixedShutter) = 0;\n>         virtual void setMaxShutter(libcamera::utils::Duration maxShutter) = 0;\n> -       virtual void setFixedAnalogueGain(double fixedAnalogueGain) = 0;\n> +       virtual void setFixedAnalogueGain(unsigned int channel, double fixedAnalogueGain) = 0;\n>         virtual void setMeteringMode(std::string const &meteringModeName) = 0;\n> -       virtual void setExposureMode(std::string const &exposureModeName) = 0;\n> -       virtual void setConstraintMode(std::string const &contraintModeName) = 0;\n> -       virtual void enableAuto() = 0;\n> -       virtual void disableAuto() = 0;\n> +       virtual void setExposureMode(unsigned int channel, std::string const &exposureModeName) = 0;\n> +       virtual void setConstraintMode(unsigned int channel, std::string const &contraintModeName) = 0;\n> +       virtual void enableAuto(unsigned int channel) = 0;\n> +       virtual void disableAuto(unsigned int channel) = 0;\n> +       virtual void setActiveChannels(const std::vector<unsigned int> &activeChannels) = 0;\n>  };\n>\n>  } /* namespace RPiController */\n> diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build\n> index feb0334e..20b9cda9 100644\n> --- a/src/ipa/rpi/controller/meson.build\n> +++ b/src/ipa/rpi/controller/meson.build\n> @@ -8,6 +8,7 @@ rpi_ipa_controller_sources = files([\n>      'pwl.cpp',\n>      'rpi/af.cpp',\n>      'rpi/agc.cpp',\n> +    'rpi/agc_channel.cpp',\n>      'rpi/alsc.cpp',\n>      'rpi/awb.cpp',\n>      'rpi/black_level.cpp',\n> diff --git a/src/ipa/rpi/controller/rpi/agc.cpp b/src/ipa/rpi/controller/rpi/agc.cpp\n> index 7b02972a..c9c9c297 100644\n> --- a/src/ipa/rpi/controller/rpi/agc.cpp\n> +++ b/src/ipa/rpi/controller/rpi/agc.cpp\n> @@ -5,20 +5,12 @@\n>   * agc.cpp - AGC/AEC control algorithm\n>   */\n>\n> -#include <algorithm>\n> -#include <map>\n> -#include <tuple>\n> +#include \"agc.h\"\n>\n>  #include <libcamera/base/log.h>\n>\n> -#include \"../awb_status.h\"\n> -#include \"../device_status.h\"\n> -#include \"../histogram.h\"\n> -#include \"../lux_status.h\"\n>  #include \"../metadata.h\"\n>\n> -#include \"agc.h\"\n> -\n>  using namespace RPiController;\n>  using namespace libcamera;\n>  using libcamera::utils::Duration;\n> @@ -28,881 +20,203 @@ LOG_DEFINE_CATEGORY(RPiAgc)\n>\n>  #define NAME \"rpi.agc\"\n>\n> -int AgcMeteringMode::read(const libcamera::YamlObject &params)\n> +Agc::Agc(Controller *controller)\n> +       : AgcAlgorithm(controller),\n> +         activeChannels_({ 0 })\n>  {\n> -       const YamlObject &yamlWeights = params[\"weights\"];\n> -\n> -       for (const auto &p : yamlWeights.asList()) {\n> -               auto value = p.get<double>();\n> -               if (!value)\n> -                       return -EINVAL;\n> -               weights.push_back(*value);\n> -       }\n> -\n> -       return 0;\n>  }\n>\n> -static std::tuple<int, std::string>\n> -readMeteringModes(std::map<std::string, AgcMeteringMode> &metering_modes,\n> -                 const libcamera::YamlObject &params)\n> +char const *Agc::name() const\n>  {\n> -       std::string first;\n> -       int ret;\n> -\n> -       for (const auto &[key, value] : params.asDict()) {\n> -               AgcMeteringMode meteringMode;\n> -               ret = meteringMode.read(value);\n> -               if (ret)\n> -                       return { ret, {} };\n> -\n> -               metering_modes[key] = std::move(meteringMode);\n> -               if (first.empty())\n> -                       first = key;\n> -       }\n> -\n> -       return { 0, first };\n> +       return NAME;\n>  }\n>\n> -int AgcExposureMode::read(const libcamera::YamlObject &params)\n> +int Agc::read(const libcamera::YamlObject &params)\n>  {\n> -       auto value = params[\"shutter\"].getList<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       std::transform(value->begin(), value->end(), std::back_inserter(shutter),\n> -                      [](double v) { return v * 1us; });\n> -\n> -       value = params[\"gain\"].getList<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       gain = std::move(*value);\n> -\n> -       if (shutter.size() < 2 || gain.size() < 2) {\n> -               LOG(RPiAgc, Error)\n> -                       << \"AgcExposureMode: must have at least two entries in exposure profile\";\n> -               return -EINVAL;\n> -       }\n> -\n> -       if (shutter.size() != gain.size()) {\n> -               LOG(RPiAgc, Error)\n> -                       << \"AgcExposureMode: expect same number of exposure and gain entries in exposure profile\";\n> -               return -EINVAL;\n> +       /*\n> +        * When there is only a single channel we can read the old style syntax.\n> +        * Otherwise we expect a \"channels\" keyword followed by a list of configurations.\n> +        */\n> +       if (!params.contains(\"channels\")) {\n> +               LOG(RPiAgc, Debug) << \"Single channel only\";\n> +               channelData_.emplace_back();\n> +               return channelData_.back().channel.read(params, getHardwareConfig());\n>         }\n>\n> -       return 0;\n> -}\n> -\n> -static std::tuple<int, std::string>\n> -readExposureModes(std::map<std::string, AgcExposureMode> &exposureModes,\n> -                 const libcamera::YamlObject &params)\n> -{\n> -       std::string first;\n> -       int ret;\n> -\n> -       for (const auto &[key, value] : params.asDict()) {\n> -               AgcExposureMode exposureMode;\n> -               ret = exposureMode.read(value);\n> +       const auto &channels = params[\"channels\"].asList();\n> +       for (auto ch = channels.begin(); ch != channels.end(); ch++) {\n> +               LOG(RPiAgc, Debug) << \"Read AGC channel\";\n> +               channelData_.emplace_back();\n> +               int ret = channelData_.back().channel.read(*ch, getHardwareConfig());\n>                 if (ret)\n> -                       return { ret, {} };\n> -\n> -               exposureModes[key] = std::move(exposureMode);\n> -               if (first.empty())\n> -                       first = key;\n> +                       return ret;\n>         }\n>\n> -       return { 0, first };\n> -}\n> -\n> -int AgcConstraint::read(const libcamera::YamlObject &params)\n> -{\n> -       std::string boundString = params[\"bound\"].get<std::string>(\"\");\n> -       transform(boundString.begin(), boundString.end(),\n> -                 boundString.begin(), ::toupper);\n> -       if (boundString != \"UPPER\" && boundString != \"LOWER\") {\n> -               LOG(RPiAgc, Error) << \"AGC constraint type should be UPPER or LOWER\";\n> -               return -EINVAL;\n> +       LOG(RPiAgc, Debug) << \"Read \" << channelData_.size() << \" channel(s)\";\n> +       if (channelData_.empty()) {\n> +               LOG(RPiAgc, Error) << \"No AGC channels provided\";\n> +               return -1;\n>         }\n> -       bound = boundString == \"UPPER\" ? Bound::UPPER : Bound::LOWER;\n> -\n> -       auto value = params[\"q_lo\"].get<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       qLo = *value;\n> -\n> -       value = params[\"q_hi\"].get<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       qHi = *value;\n> -\n> -       return yTarget.read(params[\"y_target\"]);\n> -}\n>\n> -static std::tuple<int, AgcConstraintMode>\n> -readConstraintMode(const libcamera::YamlObject &params)\n> -{\n> -       AgcConstraintMode mode;\n> -       int ret;\n> -\n> -       for (const auto &p : params.asList()) {\n> -               AgcConstraint constraint;\n> -               ret = constraint.read(p);\n> -               if (ret)\n> -                       return { ret, {} };\n> -\n> -               mode.push_back(std::move(constraint));\n> -       }\n> -\n> -       return { 0, mode };\n> +       return 0;\n>  }\n>\n> -static std::tuple<int, std::string>\n> -readConstraintModes(std::map<std::string, AgcConstraintMode> &constraintModes,\n> -                   const libcamera::YamlObject &params)\n> +int Agc::checkChannel(unsigned int channelIndex) const\n>  {\n> -       std::string first;\n> -       int ret;\n> -\n> -       for (const auto &[key, value] : params.asDict()) {\n> -               std::tie(ret, constraintModes[key]) = readConstraintMode(value);\n> -               if (ret)\n> -                       return { ret, {} };\n> -\n> -               if (first.empty())\n> -                       first = key;\n> +       if (channelIndex >= channelData_.size()) {\n> +               LOG(RPiAgc, Warning) << \"AGC channel \" << channelIndex << \" not available\";\n> +               return -1;\n>         }\n>\n> -       return { 0, first };\n> -}\n> -\n> -int AgcConfig::read(const libcamera::YamlObject &params)\n> -{\n> -       LOG(RPiAgc, Debug) << \"AgcConfig\";\n> -       int ret;\n> -\n> -       std::tie(ret, defaultMeteringMode) =\n> -               readMeteringModes(meteringModes, params[\"metering_modes\"]);\n> -       if (ret)\n> -               return ret;\n> -       std::tie(ret, defaultExposureMode) =\n> -               readExposureModes(exposureModes, params[\"exposure_modes\"]);\n> -       if (ret)\n> -               return ret;\n> -       std::tie(ret, defaultConstraintMode) =\n> -               readConstraintModes(constraintModes, params[\"constraint_modes\"]);\n> -       if (ret)\n> -               return ret;\n> -\n> -       ret = yTarget.read(params[\"y_target\"]);\n> -       if (ret)\n> -               return ret;\n> -\n> -       speed = params[\"speed\"].get<double>(0.2);\n> -       startupFrames = params[\"startup_frames\"].get<uint16_t>(10);\n> -       convergenceFrames = params[\"convergence_frames\"].get<unsigned int>(6);\n> -       fastReduceThreshold = params[\"fast_reduce_threshold\"].get<double>(0.4);\n> -       baseEv = params[\"base_ev\"].get<double>(1.0);\n> -\n> -       /* Start with quite a low value as ramping up is easier than ramping down. */\n> -       defaultExposureTime = params[\"default_exposure_time\"].get<double>(1000) * 1us;\n> -       defaultAnalogueGain = params[\"default_analogue_gain\"].get<double>(1.0);\n> -\n>         return 0;\n>  }\n>\n> -Agc::ExposureValues::ExposureValues()\n> -       : shutter(0s), analogueGain(0),\n> -         totalExposure(0s), totalExposureNoDG(0s)\n> +void Agc::disableAuto(unsigned int channelIndex)\n>  {\n> -}\n> -\n> -Agc::Agc(Controller *controller)\n> -       : AgcAlgorithm(controller), meteringMode_(nullptr),\n> -         exposureMode_(nullptr), constraintMode_(nullptr),\n> -         frameCount_(0), lockCount_(0),\n> -         lastTargetExposure_(0s), ev_(1.0), flickerPeriod_(0s),\n> -         maxShutter_(0s), fixedShutter_(0s), fixedAnalogueGain_(0.0)\n> -{\n> -       memset(&awb_, 0, sizeof(awb_));\n> -       /*\n> -        * Setting status_.totalExposureValue_ to zero initially tells us\n> -        * it's not been calculated yet (i.e. Process hasn't yet run).\n> -        */\n> -       status_ = {};\n> -       status_.ev = ev_;\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -char const *Agc::name() const\n> -{\n> -       return NAME;\n> +       LOG(RPiAgc, Debug) << \"disableAuto for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.disableAuto();\n>  }\n>\n> -int Agc::read(const libcamera::YamlObject &params)\n> +void Agc::enableAuto(unsigned int channelIndex)\n>  {\n> -       LOG(RPiAgc, Debug) << \"Agc\";\n> -\n> -       int ret = config_.read(params);\n> -       if (ret)\n> -               return ret;\n> -\n> -       const Size &size = getHardwareConfig().agcZoneWeights;\n> -       for (auto const &modes : config_.meteringModes) {\n> -               if (modes.second.weights.size() != size.width * size.height) {\n> -                       LOG(RPiAgc, Error) << \"AgcMeteringMode: Incorrect number of weights\";\n> -                       return -EINVAL;\n> -               }\n> -       }\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -       /*\n> -        * Set the config's defaults (which are the first ones it read) as our\n> -        * current modes, until someone changes them.  (they're all known to\n> -        * exist at this point)\n> -        */\n> -       meteringModeName_ = config_.defaultMeteringMode;\n> -       meteringMode_ = &config_.meteringModes[meteringModeName_];\n> -       exposureModeName_ = config_.defaultExposureMode;\n> -       exposureMode_ = &config_.exposureModes[exposureModeName_];\n> -       constraintModeName_ = config_.defaultConstraintMode;\n> -       constraintMode_ = &config_.constraintModes[constraintModeName_];\n> -       /* Set up the \"last shutter/gain\" values, in case AGC starts \"disabled\". */\n> -       status_.shutterTime = config_.defaultExposureTime;\n> -       status_.analogueGain = config_.defaultAnalogueGain;\n> -       return 0;\n> -}\n> -\n> -void Agc::disableAuto()\n> -{\n> -       fixedShutter_ = status_.shutterTime;\n> -       fixedAnalogueGain_ = status_.analogueGain;\n> -}\n> -\n> -void Agc::enableAuto()\n> -{\n> -       fixedShutter_ = 0s;\n> -       fixedAnalogueGain_ = 0;\n> +       LOG(RPiAgc, Debug) << \"enableAuto for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.enableAuto();\n>  }\n>\n>  unsigned int Agc::getConvergenceFrames() const\n>  {\n> -       /*\n> -        * If shutter and gain have been explicitly set, there is no\n> -        * convergence to happen, so no need to drop any frames - return zero.\n> -        */\n> -       if (fixedShutter_ && fixedAnalogueGain_)\n> -               return 0;\n> -       else\n> -               return config_.convergenceFrames;\n> +       /* If there are n channels, it presumably takes n times as long to converge. */\n> +       return channelData_[0].channel.getConvergenceFrames() * activeChannels_.size();\n>  }\n>\n>  std::vector<double> const &Agc::getWeights() const\n>  {\n>         /*\n> -        * In case someone calls setMeteringMode and then this before the\n> -        * algorithm has run and updated the meteringMode_ pointer.\n> +        * A limitation is that we're going to have to use the same weights across\n> +        * all channels.\n>          */\n> -       auto it = config_.meteringModes.find(meteringModeName_);\n> -       if (it == config_.meteringModes.end())\n> -               return meteringMode_->weights;\n> -       return it->second.weights;\n> +       return channelData_[0].channel.getWeights();\n>  }\n>\n> -void Agc::setEv(double ev)\n> +void Agc::setEv(unsigned int channelIndex, double ev)\n>  {\n> -       ev_ = ev;\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -void Agc::setFlickerPeriod(Duration flickerPeriod)\n> -{\n> -       flickerPeriod_ = flickerPeriod;\n> +       LOG(RPiAgc, Debug) << \"setEv \" << ev << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setEv(ev);\n>  }\n>\n> -void Agc::setMaxShutter(Duration maxShutter)\n> +void Agc::setFlickerPeriod(unsigned int channelIndex, Duration flickerPeriod)\n>  {\n> -       maxShutter_ = maxShutter;\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -void Agc::setFixedShutter(Duration fixedShutter)\n> -{\n> -       fixedShutter_ = fixedShutter;\n> -       /* Set this in case someone calls disableAuto() straight after. */\n> -       status_.shutterTime = limitShutter(fixedShutter_);\n> +       LOG(RPiAgc, Debug) << \"setFlickerPeriod \" << flickerPeriod\n> +                          << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setFlickerPeriod(flickerPeriod);\n>  }\n>\n> -void Agc::setFixedAnalogueGain(double fixedAnalogueGain)\n> -{\n> -       fixedAnalogueGain_ = fixedAnalogueGain;\n> -       /* Set this in case someone calls disableAuto() straight after. */\n> -       status_.analogueGain = limitGain(fixedAnalogueGain);\n> -}\n> -\n> -void Agc::setMeteringMode(std::string const &meteringModeName)\n> -{\n> -       meteringModeName_ = meteringModeName;\n> -}\n> -\n> -void Agc::setExposureMode(std::string const &exposureModeName)\n> -{\n> -       exposureModeName_ = exposureModeName;\n> -}\n> -\n> -void Agc::setConstraintMode(std::string const &constraintModeName)\n> -{\n> -       constraintModeName_ = constraintModeName;\n> -}\n> -\n> -void Agc::switchMode(CameraMode const &cameraMode,\n> -                    Metadata *metadata)\n> +void Agc::setMaxShutter(Duration maxShutter)\n>  {\n> -       /* AGC expects the mode sensitivity always to be non-zero. */\n> -       ASSERT(cameraMode.sensitivity);\n> -\n> -       housekeepConfig();\n> -\n> -       /*\n> -        * Store the mode in the local state. We must cache the sensitivity of\n> -        * of the previous mode for the calculations below.\n> -        */\n> -       double lastSensitivity = mode_.sensitivity;\n> -       mode_ = cameraMode;\n> -\n> -       Duration fixedShutter = limitShutter(fixedShutter_);\n> -       if (fixedShutter && fixedAnalogueGain_) {\n> -               /* We're going to reset the algorithm here with these fixed values. */\n> -\n> -               fetchAwbStatus(metadata);\n> -               double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> -               ASSERT(minColourGain != 0.0);\n> -\n> -               /* This is the equivalent of computeTargetExposure and applyDigitalGain. */\n> -               target_.totalExposureNoDG = fixedShutter_ * fixedAnalogueGain_;\n> -               target_.totalExposure = target_.totalExposureNoDG / minColourGain;\n> -\n> -               /* Equivalent of filterExposure. This resets any \"history\". */\n> -               filtered_ = target_;\n> -\n> -               /* Equivalent of divideUpExposure. */\n> -               filtered_.shutter = fixedShutter;\n> -               filtered_.analogueGain = fixedAnalogueGain_;\n> -       } else if (status_.totalExposureValue) {\n> -               /*\n> -                * On a mode switch, various things could happen:\n> -                * - the exposure profile might change\n> -                * - a fixed exposure or gain might be set\n> -                * - the new mode's sensitivity might be different\n> -                * We cope with the last of these by scaling the target values. After\n> -                * that we just need to re-divide the exposure/gain according to the\n> -                * current exposure profile, which takes care of everything else.\n> -                */\n> -\n> -               double ratio = lastSensitivity / cameraMode.sensitivity;\n> -               target_.totalExposureNoDG *= ratio;\n> -               target_.totalExposure *= ratio;\n> -               filtered_.totalExposureNoDG *= ratio;\n> -               filtered_.totalExposure *= ratio;\n> -\n> -               divideUpExposure();\n> -       } else {\n> -               /*\n> -                * We come through here on startup, when at least one of the shutter\n> -                * or gain has not been fixed. We must still write those values out so\n> -                * that they will be applied immediately. We supply some arbitrary defaults\n> -                * for any that weren't set.\n> -                */\n> -\n> -               /* Equivalent of divideUpExposure. */\n> -               filtered_.shutter = fixedShutter ? fixedShutter : config_.defaultExposureTime;\n> -               filtered_.analogueGain = fixedAnalogueGain_ ? fixedAnalogueGain_ : config_.defaultAnalogueGain;\n> -       }\n> -\n> -       writeAndFinish(metadata, false);\n> +       /* Frame durations will be the same across all channels too. */\n> +       for (auto &data : channelData_)\n> +               data.channel.setMaxShutter(maxShutter);\n>  }\n>\n> -void Agc::prepare(Metadata *imageMetadata)\n> +void Agc::setFixedShutter(unsigned int channelIndex, Duration fixedShutter)\n>  {\n> -       Duration totalExposureValue = status_.totalExposureValue;\n> -       AgcStatus delayedStatus;\n> -       AgcPrepareStatus prepareStatus;\n> -\n> -       if (!imageMetadata->get(\"agc.delayed_status\", delayedStatus))\n> -               totalExposureValue = delayedStatus.totalExposureValue;\n> -\n> -       prepareStatus.digitalGain = 1.0;\n> -       prepareStatus.locked = false;\n> -\n> -       if (status_.totalExposureValue) {\n> -               /* Process has run, so we have meaningful values. */\n> -               DeviceStatus deviceStatus;\n> -               if (imageMetadata->get(\"device.status\", deviceStatus) == 0) {\n> -                       Duration actualExposure = deviceStatus.shutterSpeed *\n> -                                                 deviceStatus.analogueGain;\n> -                       if (actualExposure) {\n> -                               double digitalGain = totalExposureValue / actualExposure;\n> -                               LOG(RPiAgc, Debug) << \"Want total exposure \" << totalExposureValue;\n> -                               /*\n> -                                * Never ask for a gain < 1.0, and also impose\n> -                                * some upper limit. Make it customisable?\n> -                                */\n> -                               prepareStatus.digitalGain = std::max(1.0, std::min(digitalGain, 4.0));\n> -                               LOG(RPiAgc, Debug) << \"Actual exposure \" << actualExposure;\n> -                               LOG(RPiAgc, Debug) << \"Use digitalGain \" << prepareStatus.digitalGain;\n> -                               LOG(RPiAgc, Debug) << \"Effective exposure \"\n> -                                                  << actualExposure * prepareStatus.digitalGain;\n> -                               /* Decide whether AEC/AGC has converged. */\n> -                               prepareStatus.locked = updateLockStatus(deviceStatus);\n> -                       }\n> -               } else\n> -                       LOG(RPiAgc, Warning) << name() << \": no device metadata\";\n> -               imageMetadata->set(\"agc.prepare_status\", prepareStatus);\n> -       }\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -void Agc::process(StatisticsPtr &stats, Metadata *imageMetadata)\n> -{\n> -       frameCount_++;\n> -       /*\n> -        * First a little bit of housekeeping, fetching up-to-date settings and\n> -        * configuration, that kind of thing.\n> -        */\n> -       housekeepConfig();\n> -       /* Fetch the AWB status immediately, so that we can assume it's there. */\n> -       fetchAwbStatus(imageMetadata);\n> -       /* Get the current exposure values for the frame that's just arrived. */\n> -       fetchCurrentExposure(imageMetadata);\n> -       /* Compute the total gain we require relative to the current exposure. */\n> -       double gain, targetY;\n> -       computeGain(stats, imageMetadata, gain, targetY);\n> -       /* Now compute the target (final) exposure which we think we want. */\n> -       computeTargetExposure(gain);\n> -       /* The results have to be filtered so as not to change too rapidly. */\n> -       filterExposure();\n> -       /*\n> -        * Some of the exposure has to be applied as digital gain, so work out\n> -        * what that is. This function also tells us whether it's decided to\n> -        * \"desaturate\" the image more quickly.\n> -        */\n> -       bool desaturate = applyDigitalGain(gain, targetY);\n> -       /*\n> -        * The last thing is to divide up the exposure value into a shutter time\n> -        * and analogue gain, according to the current exposure mode.\n> -        */\n> -       divideUpExposure();\n> -       /* Finally advertise what we've done. */\n> -       writeAndFinish(imageMetadata, desaturate);\n> +       LOG(RPiAgc, Debug) << \"setFixedShutter \" << fixedShutter\n> +                          << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setFixedShutter(fixedShutter);\n>  }\n>\n> -bool Agc::updateLockStatus(DeviceStatus const &deviceStatus)\n> +void Agc::setFixedAnalogueGain(unsigned int channelIndex, double fixedAnalogueGain)\n>  {\n> -       const double errorFactor = 0.10; /* make these customisable? */\n> -       const int maxLockCount = 5;\n> -       /* Reset \"lock count\" when we exceed this multiple of errorFactor */\n> -       const double resetMargin = 1.5;\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -       /* Add 200us to the exposure time error to allow for line quantisation. */\n> -       Duration exposureError = lastDeviceStatus_.shutterSpeed * errorFactor + 200us;\n> -       double gainError = lastDeviceStatus_.analogueGain * errorFactor;\n> -       Duration targetError = lastTargetExposure_ * errorFactor;\n> -\n> -       /*\n> -        * Note that we don't know the exposure/gain limits of the sensor, so\n> -        * the values we keep requesting may be unachievable. For this reason\n> -        * we only insist that we're close to values in the past few frames.\n> -        */\n> -       if (deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed - exposureError &&\n> -           deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed + exposureError &&\n> -           deviceStatus.analogueGain > lastDeviceStatus_.analogueGain - gainError &&\n> -           deviceStatus.analogueGain < lastDeviceStatus_.analogueGain + gainError &&\n> -           status_.targetExposureValue > lastTargetExposure_ - targetError &&\n> -           status_.targetExposureValue < lastTargetExposure_ + targetError)\n> -               lockCount_ = std::min(lockCount_ + 1, maxLockCount);\n> -       else if (deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed - resetMargin * exposureError ||\n> -                deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed + resetMargin * exposureError ||\n> -                deviceStatus.analogueGain < lastDeviceStatus_.analogueGain - resetMargin * gainError ||\n> -                deviceStatus.analogueGain > lastDeviceStatus_.analogueGain + resetMargin * gainError ||\n> -                status_.targetExposureValue < lastTargetExposure_ - resetMargin * targetError ||\n> -                status_.targetExposureValue > lastTargetExposure_ + resetMargin * targetError)\n> -               lockCount_ = 0;\n> -\n> -       lastDeviceStatus_ = deviceStatus;\n> -       lastTargetExposure_ = status_.targetExposureValue;\n> -\n> -       LOG(RPiAgc, Debug) << \"Lock count updated to \" << lockCount_;\n> -       return lockCount_ == maxLockCount;\n> +       LOG(RPiAgc, Debug) << \"setFixedAnalogueGain \" << fixedAnalogueGain\n> +                          << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setFixedAnalogueGain(fixedAnalogueGain);\n>  }\n>\n> -void Agc::housekeepConfig()\n> +void Agc::setMeteringMode(std::string const &meteringModeName)\n>  {\n> -       /* First fetch all the up-to-date settings, so no one else has to do it. */\n> -       status_.ev = ev_;\n> -       status_.fixedShutter = limitShutter(fixedShutter_);\n> -       status_.fixedAnalogueGain = fixedAnalogueGain_;\n> -       status_.flickerPeriod = flickerPeriod_;\n> -       LOG(RPiAgc, Debug) << \"ev \" << status_.ev << \" fixedShutter \"\n> -                          << status_.fixedShutter << \" fixedAnalogueGain \"\n> -                          << status_.fixedAnalogueGain;\n> -       /*\n> -        * Make sure the \"mode\" pointers point to the up-to-date things, if\n> -        * they've changed.\n> -        */\n> -       if (meteringModeName_ != status_.meteringMode) {\n> -               auto it = config_.meteringModes.find(meteringModeName_);\n> -               if (it == config_.meteringModes.end()) {\n> -                       LOG(RPiAgc, Warning) << \"No metering mode \" << meteringModeName_;\n> -                       meteringModeName_ = status_.meteringMode;\n> -               } else {\n> -                       meteringMode_ = &it->second;\n> -                       status_.meteringMode = meteringModeName_;\n> -               }\n> -       }\n> -       if (exposureModeName_ != status_.exposureMode) {\n> -               auto it = config_.exposureModes.find(exposureModeName_);\n> -               if (it == config_.exposureModes.end()) {\n> -                       LOG(RPiAgc, Warning) << \"No exposure profile \" << exposureModeName_;\n> -                       exposureModeName_ = status_.exposureMode;\n> -               } else {\n> -                       exposureMode_ = &it->second;\n> -                       status_.exposureMode = exposureModeName_;\n> -               }\n> -       }\n> -       if (constraintModeName_ != status_.constraintMode) {\n> -               auto it = config_.constraintModes.find(constraintModeName_);\n> -               if (it == config_.constraintModes.end()) {\n> -                       LOG(RPiAgc, Warning) << \"No constraint list \" << constraintModeName_;\n> -                       constraintModeName_ = status_.constraintMode;\n> -               } else {\n> -                       constraintMode_ = &it->second;\n> -                       status_.constraintMode = constraintModeName_;\n> -               }\n> -       }\n> -       LOG(RPiAgc, Debug) << \"exposureMode \"\n> -                          << exposureModeName_ << \" constraintMode \"\n> -                          << constraintModeName_ << \" meteringMode \"\n> -                          << meteringModeName_;\n> +       /* Metering modes will be the same across all channels too. */\n> +       for (auto &data : channelData_)\n> +               data.channel.setMeteringMode(meteringModeName);\n>  }\n>\n> -void Agc::fetchCurrentExposure(Metadata *imageMetadata)\n> +void Agc::setExposureMode(unsigned int channelIndex, std::string const &exposureModeName)\n>  {\n> -       std::unique_lock<Metadata> lock(*imageMetadata);\n> -       DeviceStatus *deviceStatus =\n> -               imageMetadata->getLocked<DeviceStatus>(\"device.status\");\n> -       if (!deviceStatus)\n> -               LOG(RPiAgc, Fatal) << \"No device metadata\";\n> -       current_.shutter = deviceStatus->shutterSpeed;\n> -       current_.analogueGain = deviceStatus->analogueGain;\n> -       AgcStatus *agcStatus =\n> -               imageMetadata->getLocked<AgcStatus>(\"agc.status\");\n> -       current_.totalExposure = agcStatus ? agcStatus->totalExposureValue : 0s;\n> -       current_.totalExposureNoDG = current_.shutter * current_.analogueGain;\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -void Agc::fetchAwbStatus(Metadata *imageMetadata)\n> -{\n> -       awb_.gainR = 1.0; /* in case not found in metadata */\n> -       awb_.gainG = 1.0;\n> -       awb_.gainB = 1.0;\n> -       if (imageMetadata->get(\"awb.status\", awb_) != 0)\n> -               LOG(RPiAgc, Debug) << \"No AWB status found\";\n> +       LOG(RPiAgc, Debug) << \"setExposureMode \" << exposureModeName\n> +                          << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setExposureMode(exposureModeName);\n>  }\n>\n> -static double computeInitialY(StatisticsPtr &stats, AwbStatus const &awb,\n> -                             std::vector<double> &weights, double gain)\n> +void Agc::setConstraintMode(unsigned int channelIndex, std::string const &constraintModeName)\n>  {\n> -       constexpr uint64_t maxVal = 1 << Statistics::NormalisationFactorPow2;\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -       ASSERT(weights.size() == stats->agcRegions.numRegions());\n> -\n> -       /*\n> -        * Note that the weights are applied by the IPA to the statistics directly,\n> -        * before they are given to us here.\n> -        */\n> -       double rSum = 0, gSum = 0, bSum = 0, pixelSum = 0;\n> -       for (unsigned int i = 0; i < stats->agcRegions.numRegions(); i++) {\n> -               auto &region = stats->agcRegions.get(i);\n> -               rSum += std::min<double>(region.val.rSum * gain, (maxVal - 1) * region.counted);\n> -               gSum += std::min<double>(region.val.gSum * gain, (maxVal - 1) * region.counted);\n> -               bSum += std::min<double>(region.val.bSum * gain, (maxVal - 1) * region.counted);\n> -               pixelSum += region.counted;\n> -       }\n> -       if (pixelSum == 0.0) {\n> -               LOG(RPiAgc, Warning) << \"computeInitialY: pixelSum is zero\";\n> -               return 0;\n> -       }\n> -       double ySum = rSum * awb.gainR * .299 +\n> -                     gSum * awb.gainG * .587 +\n> -                     bSum * awb.gainB * .114;\n> -       return ySum / pixelSum / maxVal;\n> +       channelData_[channelIndex].channel.setConstraintMode(constraintModeName);\n>  }\n>\n> -/*\n> - * We handle extra gain through EV by adjusting our Y targets. However, you\n> - * simply can't monitor histograms once they get very close to (or beyond!)\n> - * saturation, so we clamp the Y targets to this value. It does mean that EV\n> - * increases don't necessarily do quite what you might expect in certain\n> - * (contrived) cases.\n> - */\n> -\n> -static constexpr double EvGainYTargetLimit = 0.9;\n> -\n> -static double constraintComputeGain(AgcConstraint &c, const Histogram &h, double lux,\n> -                                   double evGain, double &targetY)\n> +template<typename T>\n> +std::ostream &operator<<(std::ostream &os, const std::vector<T> &v)\n>  {\n> -       targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));\n> -       targetY = std::min(EvGainYTargetLimit, targetY * evGain);\n> -       double iqm = h.interQuantileMean(c.qLo, c.qHi);\n> -       return (targetY * h.bins()) / iqm;\n> +       os << \"{\";\n> +       for (const auto &e : v)\n> +               os << \" \" << e;\n> +       os << \" }\";\n> +       return os;\n>  }\n>\n> -void Agc::computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,\n> -                     double &gain, double &targetY)\n> +void Agc::setActiveChannels(const std::vector<unsigned int> &activeChannels)\n>  {\n> -       struct LuxStatus lux = {};\n> -       lux.lux = 400; /* default lux level to 400 in case no metadata found */\n> -       if (imageMetadata->get(\"lux.status\", lux) != 0)\n> -               LOG(RPiAgc, Warning) << \"No lux level found\";\n> -       const Histogram &h = statistics->yHist;\n> -       double evGain = status_.ev * config_.baseEv;\n> -       /*\n> -        * The initial gain and target_Y come from some of the regions. After\n> -        * that we consider the histogram constraints.\n> -        */\n> -       targetY = config_.yTarget.eval(config_.yTarget.domain().clip(lux.lux));\n> -       targetY = std::min(EvGainYTargetLimit, targetY * evGain);\n> -\n> -       /*\n> -        * Do this calculation a few times as brightness increase can be\n> -        * non-linear when there are saturated regions.\n> -        */\n> -       gain = 1.0;\n> -       for (int i = 0; i < 8; i++) {\n> -               double initialY = computeInitialY(statistics, awb_, meteringMode_->weights, gain);\n> -               double extraGain = std::min(10.0, targetY / (initialY + .001));\n> -               gain *= extraGain;\n> -               LOG(RPiAgc, Debug) << \"Initial Y \" << initialY << \" target \" << targetY\n> -                                  << \" gives gain \" << gain;\n> -               if (extraGain < 1.01) /* close enough */\n> -                       break;\n> -       }\n> -\n> -       for (auto &c : *constraintMode_) {\n> -               double newTargetY;\n> -               double newGain = constraintComputeGain(c, h, lux.lux, evGain, newTargetY);\n> -               LOG(RPiAgc, Debug) << \"Constraint has target_Y \"\n> -                                  << newTargetY << \" giving gain \" << newGain;\n> -               if (c.bound == AgcConstraint::Bound::LOWER && newGain > gain) {\n> -                       LOG(RPiAgc, Debug) << \"Lower bound constraint adopted\";\n> -                       gain = newGain;\n> -                       targetY = newTargetY;\n> -               } else if (c.bound == AgcConstraint::Bound::UPPER && newGain < gain) {\n> -                       LOG(RPiAgc, Debug) << \"Upper bound constraint adopted\";\n> -                       gain = newGain;\n> -                       targetY = newTargetY;\n> -               }\n> +       if (activeChannels.empty()) {\n> +               LOG(RPiAgc, Warning) << \"No active AGC channels supplied\";\n> +               return;\n>         }\n> -       LOG(RPiAgc, Debug) << \"Final gain \" << gain << \" (target_Y \" << targetY << \" ev \"\n> -                          << status_.ev << \" base_ev \" << config_.baseEv\n> -                          << \")\";\n> -}\n> -\n> -void Agc::computeTargetExposure(double gain)\n> -{\n> -       if (status_.fixedShutter && status_.fixedAnalogueGain) {\n> -               /*\n> -                * When ag and shutter are both fixed, we need to drive the\n> -                * total exposure so that we end up with a digital gain of at least\n> -                * 1/minColourGain. Otherwise we'd desaturate channels causing\n> -                * white to go cyan or magenta.\n> -                */\n> -               double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> -               ASSERT(minColourGain != 0.0);\n> -               target_.totalExposure =\n> -                       status_.fixedShutter * status_.fixedAnalogueGain / minColourGain;\n> -       } else {\n> -               /*\n> -                * The statistics reflect the image without digital gain, so the final\n> -                * total exposure we're aiming for is:\n> -                */\n> -               target_.totalExposure = current_.totalExposureNoDG * gain;\n> -               /* The final target exposure is also limited to what the exposure mode allows. */\n> -               Duration maxShutter = status_.fixedShutter\n> -                                             ? status_.fixedShutter\n> -                                             : exposureMode_->shutter.back();\n> -               maxShutter = limitShutter(maxShutter);\n> -               Duration maxTotalExposure =\n> -                       maxShutter *\n> -                       (status_.fixedAnalogueGain != 0.0\n> -                                ? status_.fixedAnalogueGain\n> -                                : exposureMode_->gain.back());\n> -               target_.totalExposure = std::min(target_.totalExposure, maxTotalExposure);\n> -       }\n> -       LOG(RPiAgc, Debug) << \"Target totalExposure \" << target_.totalExposure;\n> -}\n>\n> -bool Agc::applyDigitalGain(double gain, double targetY)\n> -{\n> -       double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> -       ASSERT(minColourGain != 0.0);\n> -       double dg = 1.0 / minColourGain;\n> -       /*\n> -        * I think this pipeline subtracts black level and rescales before we\n> -        * get the stats, so no need to worry about it.\n> -        */\n> -       LOG(RPiAgc, Debug) << \"after AWB, target dg \" << dg << \" gain \" << gain\n> -                          << \" target_Y \" << targetY;\n> -       /*\n> -        * Finally, if we're trying to reduce exposure but the target_Y is\n> -        * \"close\" to 1.0, then the gain computed for that constraint will be\n> -        * only slightly less than one, because the measured Y can never be\n> -        * larger than 1.0. When this happens, demand a large digital gain so\n> -        * that the exposure can be reduced, de-saturating the image much more\n> -        * quickly (and we then approach the correct value more quickly from\n> -        * below).\n> -        */\n> -       bool desaturate = targetY > config_.fastReduceThreshold &&\n> -                         gain < sqrt(targetY);\n> -       if (desaturate)\n> -               dg /= config_.fastReduceThreshold;\n> -       LOG(RPiAgc, Debug) << \"Digital gain \" << dg << \" desaturate? \" << desaturate;\n> -       filtered_.totalExposureNoDG = filtered_.totalExposure / dg;\n> -       LOG(RPiAgc, Debug) << \"Target totalExposureNoDG \" << filtered_.totalExposureNoDG;\n> -       return desaturate;\n> -}\n> -\n> -void Agc::filterExposure()\n> -{\n> -       double speed = config_.speed;\n> -       /*\n> -        * AGC adapts instantly if both shutter and gain are directly specified\n> -        * or we're in the startup phase.\n> -        */\n> -       if ((status_.fixedShutter && status_.fixedAnalogueGain) ||\n> -           frameCount_ <= config_.startupFrames)\n> -               speed = 1.0;\n> -       if (!filtered_.totalExposure) {\n> -               filtered_.totalExposure = target_.totalExposure;\n> -       } else {\n> -               /*\n> -                * If close to the result go faster, to save making so many\n> -                * micro-adjustments on the way. (Make this customisable?)\n> -                */\n> -               if (filtered_.totalExposure < 1.2 * target_.totalExposure &&\n> -                   filtered_.totalExposure > 0.8 * target_.totalExposure)\n> -                       speed = sqrt(speed);\n> -               filtered_.totalExposure = speed * target_.totalExposure +\n> -                                         filtered_.totalExposure * (1.0 - speed);\n> -       }\n> -       LOG(RPiAgc, Debug) << \"After filtering, totalExposure \" << filtered_.totalExposure\n> -                          << \" no dg \" << filtered_.totalExposureNoDG;\n> -}\n> +       for (auto index : activeChannels)\n> +               if (checkChannel(index))\n> +                       return;\n>\n> -void Agc::divideUpExposure()\n> -{\n> -       /*\n> -        * Sending the fixed shutter/gain cases through the same code may seem\n> -        * unnecessary, but it will make more sense when extend this to cover\n> -        * variable aperture.\n> -        */\n> -       Duration exposureValue = filtered_.totalExposureNoDG;\n> -       Duration shutterTime;\n> -       double analogueGain;\n> -       shutterTime = status_.fixedShutter ? status_.fixedShutter\n> -                                          : exposureMode_->shutter[0];\n> -       shutterTime = limitShutter(shutterTime);\n> -       analogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain\n> -                                                       : exposureMode_->gain[0];\n> -       analogueGain = limitGain(analogueGain);\n> -       if (shutterTime * analogueGain < exposureValue) {\n> -               for (unsigned int stage = 1;\n> -                    stage < exposureMode_->gain.size(); stage++) {\n> -                       if (!status_.fixedShutter) {\n> -                               Duration stageShutter =\n> -                                       limitShutter(exposureMode_->shutter[stage]);\n> -                               if (stageShutter * analogueGain >= exposureValue) {\n> -                                       shutterTime = exposureValue / analogueGain;\n> -                                       break;\n> -                               }\n> -                               shutterTime = stageShutter;\n> -                       }\n> -                       if (status_.fixedAnalogueGain == 0.0) {\n> -                               if (exposureMode_->gain[stage] * shutterTime >= exposureValue) {\n> -                                       analogueGain = exposureValue / shutterTime;\n> -                                       break;\n> -                               }\n> -                               analogueGain = exposureMode_->gain[stage];\n> -                               analogueGain = limitGain(analogueGain);\n> -                       }\n> -               }\n> -       }\n> -       LOG(RPiAgc, Debug) << \"Divided up shutter and gain are \" << shutterTime << \" and \"\n> -                          << analogueGain;\n> -       /*\n> -        * Finally adjust shutter time for flicker avoidance (require both\n> -        * shutter and gain not to be fixed).\n> -        */\n> -       if (!status_.fixedShutter && !status_.fixedAnalogueGain &&\n> -           status_.flickerPeriod) {\n> -               int flickerPeriods = shutterTime / status_.flickerPeriod;\n> -               if (flickerPeriods) {\n> -                       Duration newShutterTime = flickerPeriods * status_.flickerPeriod;\n> -                       analogueGain *= shutterTime / newShutterTime;\n> -                       /*\n> -                        * We should still not allow the ag to go over the\n> -                        * largest value in the exposure mode. Note that this\n> -                        * may force more of the total exposure into the digital\n> -                        * gain as a side-effect.\n> -                        */\n> -                       analogueGain = std::min(analogueGain, exposureMode_->gain.back());\n> -                       analogueGain = limitGain(analogueGain);\n> -                       shutterTime = newShutterTime;\n> -               }\n> -               LOG(RPiAgc, Debug) << \"After flicker avoidance, shutter \"\n> -                                  << shutterTime << \" gain \" << analogueGain;\n> -       }\n> -       filtered_.shutter = shutterTime;\n> -       filtered_.analogueGain = analogueGain;\n> +       LOG(RPiAgc, Debug) << \"setActiveChannels \" << activeChannels;\n> +       activeChannels_ = activeChannels;\n>  }\n>\n> -void Agc::writeAndFinish(Metadata *imageMetadata, bool desaturate)\n> +void Agc::switchMode(CameraMode const &cameraMode,\n> +                    Metadata *metadata)\n>  {\n> -       status_.totalExposureValue = filtered_.totalExposure;\n> -       status_.targetExposureValue = desaturate ? 0s : target_.totalExposureNoDG;\n> -       status_.shutterTime = filtered_.shutter;\n> -       status_.analogueGain = filtered_.analogueGain;\n> -       /*\n> -        * Write to metadata as well, in case anyone wants to update the camera\n> -        * immediately.\n> -        */\n> -       imageMetadata->set(\"agc.status\", status_);\n> -       LOG(RPiAgc, Debug) << \"Output written, total exposure requested is \"\n> -                          << filtered_.totalExposure;\n> -       LOG(RPiAgc, Debug) << \"Camera exposure update: shutter time \" << filtered_.shutter\n> -                          << \" analogue gain \" << filtered_.analogueGain;\n> +       LOG(RPiAgc, Debug) << \"switchMode for channel 0\";\n> +       channelData_[0].channel.switchMode(cameraMode, metadata);\n>  }\n>\n> -Duration Agc::limitShutter(Duration shutter)\n> +void Agc::prepare(Metadata *imageMetadata)\n>  {\n> -       /*\n> -        * shutter == 0 is a special case for fixed shutter values, and must pass\n> -        * through unchanged\n> -        */\n> -       if (!shutter)\n> -               return shutter;\n> -\n> -       shutter = std::clamp(shutter, mode_.minShutter, maxShutter_);\n> -       return shutter;\n> +       LOG(RPiAgc, Debug) << \"prepare for channel 0\";\n> +       channelData_[0].channel.prepare(imageMetadata);\n>  }\n>\n> -double Agc::limitGain(double gain) const\n> +void Agc::process(StatisticsPtr &stats, Metadata *imageMetadata)\n>  {\n> -       /*\n> -        * Only limit the lower bounds of the gain value to what the sensor limits.\n> -        * The upper bound on analogue gain will be made up with additional digital\n> -        * gain applied by the ISP.\n> -        *\n> -        * gain == 0.0 is a special case for fixed shutter values, and must pass\n> -        * through unchanged\n> -        */\n> -       if (!gain)\n> -               return gain;\n> -\n> -       gain = std::max(gain, mode_.minAnalogueGain);\n> -       return gain;\n> +       LOG(RPiAgc, Debug) << \"process for channel 0\";\n> +       channelData_[0].channel.process(stats, imageMetadata);\n>  }\n>\n>  /* Register algorithm with the system. */\n> diff --git a/src/ipa/rpi/controller/rpi/agc.h b/src/ipa/rpi/controller/rpi/agc.h\n> index aaf77c8f..a9158910 100644\n> --- a/src/ipa/rpi/controller/rpi/agc.h\n> +++ b/src/ipa/rpi/controller/rpi/agc.h\n> @@ -6,60 +6,19 @@\n>   */\n>  #pragma once\n>\n> +#include <optional>\n>  #include <vector>\n> -#include <mutex>\n> -\n> -#include <libcamera/base/utils.h>\n>\n>  #include \"../agc_algorithm.h\"\n> -#include \"../agc_status.h\"\n> -#include \"../pwl.h\"\n>\n> -/* This is our implementation of AGC. */\n> +#include \"agc_channel.h\"\n>\n>  namespace RPiController {\n>\n> -struct AgcMeteringMode {\n> -       std::vector<double> weights;\n> -       int read(const libcamera::YamlObject &params);\n> -};\n> -\n> -struct AgcExposureMode {\n> -       std::vector<libcamera::utils::Duration> shutter;\n> -       std::vector<double> gain;\n> -       int read(const libcamera::YamlObject &params);\n> -};\n> -\n> -struct AgcConstraint {\n> -       enum class Bound { LOWER = 0, UPPER = 1 };\n> -       Bound bound;\n> -       double qLo;\n> -       double qHi;\n> -       Pwl yTarget;\n> -       int read(const libcamera::YamlObject &params);\n> -};\n> -\n> -typedef std::vector<AgcConstraint> AgcConstraintMode;\n> -\n> -struct AgcConfig {\n> -       int read(const libcamera::YamlObject &params);\n> -       std::map<std::string, AgcMeteringMode> meteringModes;\n> -       std::map<std::string, AgcExposureMode> exposureModes;\n> -       std::map<std::string, AgcConstraintMode> constraintModes;\n> -       Pwl yTarget;\n> -       double speed;\n> -       uint16_t startupFrames;\n> -       unsigned int convergenceFrames;\n> -       double maxChange;\n> -       double minChange;\n> -       double fastReduceThreshold;\n> -       double speedUpThreshold;\n> -       std::string defaultMeteringMode;\n> -       std::string defaultExposureMode;\n> -       std::string defaultConstraintMode;\n> -       double baseEv;\n> -       libcamera::utils::Duration defaultExposureTime;\n> -       double defaultAnalogueGain;\n> +struct AgcChannelData {\n> +       AgcChannel channel;\n> +       std::optional<DeviceStatus> deviceStatus;\n> +       StatisticsPtr statistics;\n>  };\n>\n>  class Agc : public AgcAlgorithm\n> @@ -70,65 +29,30 @@ public:\n>         int read(const libcamera::YamlObject &params) override;\n>         unsigned int getConvergenceFrames() const override;\n>         std::vector<double> const &getWeights() const override;\n> -       void setEv(double ev) override;\n> -       void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) override;\n> +       void setEv(unsigned int channel, double ev) override;\n> +       void setFlickerPeriod(unsigned int channelIndex,\n> +                             libcamera::utils::Duration flickerPeriod) override;\n>         void setMaxShutter(libcamera::utils::Duration maxShutter) override;\n> -       void setFixedShutter(libcamera::utils::Duration fixedShutter) override;\n> -       void setFixedAnalogueGain(double fixedAnalogueGain) override;\n> +       void setFixedShutter(unsigned int channelIndex,\n> +                            libcamera::utils::Duration fixedShutter) override;\n> +       void setFixedAnalogueGain(unsigned int channelIndex,\n> +                                 double fixedAnalogueGain) override;\n>         void setMeteringMode(std::string const &meteringModeName) override;\n> -       void setExposureMode(std::string const &exposureModeName) override;\n> -       void setConstraintMode(std::string const &contraintModeName) override;\n> -       void enableAuto() override;\n> -       void disableAuto() override;\n> +       void setExposureMode(unsigned int channelIndex,\n> +                            std::string const &exposureModeName) override;\n> +       void setConstraintMode(unsigned int channelIndex,\n> +                              std::string const &contraintModeName) override;\n> +       void enableAuto(unsigned int channelIndex) override;\n> +       void disableAuto(unsigned int channelIndex) override;\n>         void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;\n>         void prepare(Metadata *imageMetadata) override;\n>         void process(StatisticsPtr &stats, Metadata *imageMetadata) override;\n> +       void setActiveChannels(const std::vector<unsigned int> &activeChannels) override;\n>\n>  private:\n> -       bool updateLockStatus(DeviceStatus const &deviceStatus);\n> -       AgcConfig config_;\n> -       void housekeepConfig();\n> -       void fetchCurrentExposure(Metadata *imageMetadata);\n> -       void fetchAwbStatus(Metadata *imageMetadata);\n> -       void computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,\n> -                        double &gain, double &targetY);\n> -       void computeTargetExposure(double gain);\n> -       void filterExposure();\n> -       bool applyDigitalGain(double gain, double targetY);\n> -       void divideUpExposure();\n> -       void writeAndFinish(Metadata *imageMetadata, bool desaturate);\n> -       libcamera::utils::Duration limitShutter(libcamera::utils::Duration shutter);\n> -       double limitGain(double gain) const;\n> -       AgcMeteringMode *meteringMode_;\n> -       AgcExposureMode *exposureMode_;\n> -       AgcConstraintMode *constraintMode_;\n> -       CameraMode mode_;\n> -       uint64_t frameCount_;\n> -       AwbStatus awb_;\n> -       struct ExposureValues {\n> -               ExposureValues();\n> -\n> -               libcamera::utils::Duration shutter;\n> -               double analogueGain;\n> -               libcamera::utils::Duration totalExposure;\n> -               libcamera::utils::Duration totalExposureNoDG; /* without digital gain */\n> -       };\n> -       ExposureValues current_;  /* values for the current frame */\n> -       ExposureValues target_;   /* calculate the values we want here */\n> -       ExposureValues filtered_; /* these values are filtered towards target */\n> -       AgcStatus status_;\n> -       int lockCount_;\n> -       DeviceStatus lastDeviceStatus_;\n> -       libcamera::utils::Duration lastTargetExposure_;\n> -       /* Below here the \"settings\" that applications can change. */\n> -       std::string meteringModeName_;\n> -       std::string exposureModeName_;\n> -       std::string constraintModeName_;\n> -       double ev_;\n> -       libcamera::utils::Duration flickerPeriod_;\n> -       libcamera::utils::Duration maxShutter_;\n> -       libcamera::utils::Duration fixedShutter_;\n> -       double fixedAnalogueGain_;\n> +       int checkChannel(unsigned int channel) const;\n> +       std::vector<AgcChannelData> channelData_;\n> +       std::vector<unsigned int> activeChannels_;\n>  };\n>\n>  } /* namespace RPiController */\n> diff --git a/src/ipa/rpi/controller/rpi/agc_channel.cpp b/src/ipa/rpi/controller/rpi/agc_channel.cpp\n> new file mode 100644\n> index 00000000..d6e30ef2\n> --- /dev/null\n> +++ b/src/ipa/rpi/controller/rpi/agc_channel.cpp\n> @@ -0,0 +1,927 @@\n> +/* SPDX-License-Identifier: BSD-2-Clause */\n> +/*\n> + * Copyright (C) 2019, Raspberry Pi Ltd\n> + *\n> + * agc.cpp - AGC/AEC control algorithm\n> + */\n> +\n> +#include <algorithm>\n> +#include <map>\n> +#include <tuple>\n> +\n> +#include <libcamera/base/log.h>\n> +\n> +#include \"../awb_status.h\"\n> +#include \"../device_status.h\"\n> +#include \"../histogram.h\"\n> +#include \"../lux_status.h\"\n> +#include \"../metadata.h\"\n> +\n> +#include \"agc.h\"\n> +\n> +using namespace RPiController;\n> +using namespace libcamera;\n> +using libcamera::utils::Duration;\n> +using namespace std::literals::chrono_literals;\n> +\n> +LOG_DECLARE_CATEGORY(RPiAgc)\n> +\n> +#define NAME \"rpi.agc\"\n> +\n> +int AgcMeteringMode::read(const libcamera::YamlObject &params)\n> +{\n> +       const YamlObject &yamlWeights = params[\"weights\"];\n> +\n> +       for (const auto &p : yamlWeights.asList()) {\n> +               auto value = p.get<double>();\n> +               if (!value)\n> +                       return -EINVAL;\n> +               weights.push_back(*value);\n> +       }\n> +\n> +       return 0;\n> +}\n> +\n> +static std::tuple<int, std::string>\n> +readMeteringModes(std::map<std::string, AgcMeteringMode> &metering_modes,\n> +                 const libcamera::YamlObject &params)\n> +{\n> +       std::string first;\n> +       int ret;\n> +\n> +       for (const auto &[key, value] : params.asDict()) {\n> +               AgcMeteringMode meteringMode;\n> +               ret = meteringMode.read(value);\n> +               if (ret)\n> +                       return { ret, {} };\n> +\n> +               metering_modes[key] = std::move(meteringMode);\n> +               if (first.empty())\n> +                       first = key;\n> +       }\n> +\n> +       return { 0, first };\n> +}\n> +\n> +int AgcExposureMode::read(const libcamera::YamlObject &params)\n> +{\n> +       auto value = params[\"shutter\"].getList<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       std::transform(value->begin(), value->end(), std::back_inserter(shutter),\n> +                      [](double v) { return v * 1us; });\n> +\n> +       value = params[\"gain\"].getList<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       gain = std::move(*value);\n> +\n> +       if (shutter.size() < 2 || gain.size() < 2) {\n> +               LOG(RPiAgc, Error)\n> +                       << \"AgcExposureMode: must have at least two entries in exposure profile\";\n> +               return -EINVAL;\n> +       }\n> +\n> +       if (shutter.size() != gain.size()) {\n> +               LOG(RPiAgc, Error)\n> +                       << \"AgcExposureMode: expect same number of exposure and gain entries in exposure profile\";\n> +               return -EINVAL;\n> +       }\n> +\n> +       return 0;\n> +}\n> +\n> +static std::tuple<int, std::string>\n> +readExposureModes(std::map<std::string, AgcExposureMode> &exposureModes,\n> +                 const libcamera::YamlObject &params)\n> +{\n> +       std::string first;\n> +       int ret;\n> +\n> +       for (const auto &[key, value] : params.asDict()) {\n> +               AgcExposureMode exposureMode;\n> +               ret = exposureMode.read(value);\n> +               if (ret)\n> +                       return { ret, {} };\n> +\n> +               exposureModes[key] = std::move(exposureMode);\n> +               if (first.empty())\n> +                       first = key;\n> +       }\n> +\n> +       return { 0, first };\n> +}\n> +\n> +int AgcConstraint::read(const libcamera::YamlObject &params)\n> +{\n> +       std::string boundString = params[\"bound\"].get<std::string>(\"\");\n> +       transform(boundString.begin(), boundString.end(),\n> +                 boundString.begin(), ::toupper);\n> +       if (boundString != \"UPPER\" && boundString != \"LOWER\") {\n> +               LOG(RPiAgc, Error) << \"AGC constraint type should be UPPER or LOWER\";\n> +               return -EINVAL;\n> +       }\n> +       bound = boundString == \"UPPER\" ? Bound::UPPER : Bound::LOWER;\n> +\n> +       auto value = params[\"q_lo\"].get<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       qLo = *value;\n> +\n> +       value = params[\"q_hi\"].get<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       qHi = *value;\n> +\n> +       return yTarget.read(params[\"y_target\"]);\n> +}\n> +\n> +static std::tuple<int, AgcConstraintMode>\n> +readConstraintMode(const libcamera::YamlObject &params)\n> +{\n> +       AgcConstraintMode mode;\n> +       int ret;\n> +\n> +       for (const auto &p : params.asList()) {\n> +               AgcConstraint constraint;\n> +               ret = constraint.read(p);\n> +               if (ret)\n> +                       return { ret, {} };\n> +\n> +               mode.push_back(std::move(constraint));\n> +       }\n> +\n> +       return { 0, mode };\n> +}\n> +\n> +static std::tuple<int, std::string>\n> +readConstraintModes(std::map<std::string, AgcConstraintMode> &constraintModes,\n> +                   const libcamera::YamlObject &params)\n> +{\n> +       std::string first;\n> +       int ret;\n> +\n> +       for (const auto &[key, value] : params.asDict()) {\n> +               std::tie(ret, constraintModes[key]) = readConstraintMode(value);\n> +               if (ret)\n> +                       return { ret, {} };\n> +\n> +               if (first.empty())\n> +                       first = key;\n> +       }\n> +\n> +       return { 0, first };\n> +}\n> +\n> +int AgcConfig::read(const libcamera::YamlObject &params)\n> +{\n> +       LOG(RPiAgc, Debug) << \"AgcConfig\";\n> +       int ret;\n> +\n> +       std::tie(ret, defaultMeteringMode) =\n> +               readMeteringModes(meteringModes, params[\"metering_modes\"]);\n> +       if (ret)\n> +               return ret;\n> +       std::tie(ret, defaultExposureMode) =\n> +               readExposureModes(exposureModes, params[\"exposure_modes\"]);\n> +       if (ret)\n> +               return ret;\n> +       std::tie(ret, defaultConstraintMode) =\n> +               readConstraintModes(constraintModes, params[\"constraint_modes\"]);\n> +       if (ret)\n> +               return ret;\n> +\n> +       ret = yTarget.read(params[\"y_target\"]);\n> +       if (ret)\n> +               return ret;\n> +\n> +       speed = params[\"speed\"].get<double>(0.2);\n> +       startupFrames = params[\"startup_frames\"].get<uint16_t>(10);\n> +       convergenceFrames = params[\"convergence_frames\"].get<unsigned int>(6);\n> +       fastReduceThreshold = params[\"fast_reduce_threshold\"].get<double>(0.4);\n> +       baseEv = params[\"base_ev\"].get<double>(1.0);\n> +\n> +       /* Start with quite a low value as ramping up is easier than ramping down. */\n> +       defaultExposureTime = params[\"default_exposure_time\"].get<double>(1000) * 1us;\n> +       defaultAnalogueGain = params[\"default_analogue_gain\"].get<double>(1.0);\n> +\n> +       return 0;\n> +}\n> +\n> +AgcChannel::ExposureValues::ExposureValues()\n> +       : shutter(0s), analogueGain(0),\n> +         totalExposure(0s), totalExposureNoDG(0s)\n> +{\n> +}\n> +\n> +AgcChannel::AgcChannel()\n> +       : meteringMode_(nullptr), exposureMode_(nullptr), constraintMode_(nullptr),\n> +         frameCount_(0), lockCount_(0),\n> +         lastTargetExposure_(0s), ev_(1.0), flickerPeriod_(0s),\n> +         maxShutter_(0s), fixedShutter_(0s), fixedAnalogueGain_(0.0)\n> +{\n> +       memset(&awb_, 0, sizeof(awb_));\n> +       /*\n> +        * Setting status_.totalExposureValue_ to zero initially tells us\n> +        * it's not been calculated yet (i.e. Process hasn't yet run).\n> +        */\n> +       status_ = {};\n> +       status_.ev = ev_;\n> +}\n> +\n> +int AgcChannel::read(const libcamera::YamlObject &params,\n> +                    const Controller::HardwareConfig &hardwareConfig)\n> +{\n> +       int ret = config_.read(params);\n> +       if (ret)\n> +               return ret;\n> +\n> +       const Size &size = hardwareConfig.agcZoneWeights;\n> +       for (auto const &modes : config_.meteringModes) {\n> +               if (modes.second.weights.size() != size.width * size.height) {\n> +                       LOG(RPiAgc, Error) << \"AgcMeteringMode: Incorrect number of weights\";\n> +                       return -EINVAL;\n> +               }\n> +       }\n> +\n> +       /*\n> +        * Set the config's defaults (which are the first ones it read) as our\n> +        * current modes, until someone changes them.  (they're all known to\n> +        * exist at this point)\n> +        */\n> +       meteringModeName_ = config_.defaultMeteringMode;\n> +       meteringMode_ = &config_.meteringModes[meteringModeName_];\n> +       exposureModeName_ = config_.defaultExposureMode;\n> +       exposureMode_ = &config_.exposureModes[exposureModeName_];\n> +       constraintModeName_ = config_.defaultConstraintMode;\n> +       constraintMode_ = &config_.constraintModes[constraintModeName_];\n> +       /* Set up the \"last shutter/gain\" values, in case AGC starts \"disabled\". */\n> +       status_.shutterTime = config_.defaultExposureTime;\n> +       status_.analogueGain = config_.defaultAnalogueGain;\n> +       return 0;\n> +}\n> +\n> +void AgcChannel::disableAuto()\n> +{\n> +       fixedShutter_ = status_.shutterTime;\n> +       fixedAnalogueGain_ = status_.analogueGain;\n> +}\n> +\n> +void AgcChannel::enableAuto()\n> +{\n> +       fixedShutter_ = 0s;\n> +       fixedAnalogueGain_ = 0;\n> +}\n> +\n> +unsigned int AgcChannel::getConvergenceFrames() const\n> +{\n> +       /*\n> +        * If shutter and gain have been explicitly set, there is no\n> +        * convergence to happen, so no need to drop any frames - return zero.\n> +        */\n> +       if (fixedShutter_ && fixedAnalogueGain_)\n> +               return 0;\n> +       else\n> +               return config_.convergenceFrames;\n> +}\n> +\n> +std::vector<double> const &AgcChannel::getWeights() const\n> +{\n> +       /*\n> +        * In case someone calls setMeteringMode and then this before the\n> +        * algorithm has run and updated the meteringMode_ pointer.\n> +        */\n> +       auto it = config_.meteringModes.find(meteringModeName_);\n> +       if (it == config_.meteringModes.end())\n> +               return meteringMode_->weights;\n> +       return it->second.weights;\n> +}\n> +\n> +void AgcChannel::setEv(double ev)\n> +{\n> +       ev_ = ev;\n> +}\n> +\n> +void AgcChannel::setFlickerPeriod(Duration flickerPeriod)\n> +{\n> +       flickerPeriod_ = flickerPeriod;\n> +}\n> +\n> +void AgcChannel::setMaxShutter(Duration maxShutter)\n> +{\n> +       maxShutter_ = maxShutter;\n> +}\n> +\n> +void AgcChannel::setFixedShutter(Duration fixedShutter)\n> +{\n> +       fixedShutter_ = fixedShutter;\n> +       /* Set this in case someone calls disableAuto() straight after. */\n> +       status_.shutterTime = limitShutter(fixedShutter_);\n> +}\n> +\n> +void AgcChannel::setFixedAnalogueGain(double fixedAnalogueGain)\n> +{\n> +       fixedAnalogueGain_ = fixedAnalogueGain;\n> +       /* Set this in case someone calls disableAuto() straight after. */\n> +       status_.analogueGain = limitGain(fixedAnalogueGain);\n> +}\n> +\n> +void AgcChannel::setMeteringMode(std::string const &meteringModeName)\n> +{\n> +       meteringModeName_ = meteringModeName;\n> +}\n> +\n> +void AgcChannel::setExposureMode(std::string const &exposureModeName)\n> +{\n> +       exposureModeName_ = exposureModeName;\n> +}\n> +\n> +void AgcChannel::setConstraintMode(std::string const &constraintModeName)\n> +{\n> +       constraintModeName_ = constraintModeName;\n> +}\n> +\n> +void AgcChannel::switchMode(CameraMode const &cameraMode,\n> +                           Metadata *metadata)\n> +{\n> +       /* AGC expects the mode sensitivity always to be non-zero. */\n> +       ASSERT(cameraMode.sensitivity);\n> +\n> +       housekeepConfig();\n> +\n> +       /*\n> +        * Store the mode in the local state. We must cache the sensitivity of\n> +        * of the previous mode for the calculations below.\n> +        */\n> +       double lastSensitivity = mode_.sensitivity;\n> +       mode_ = cameraMode;\n> +\n> +       Duration fixedShutter = limitShutter(fixedShutter_);\n> +       if (fixedShutter && fixedAnalogueGain_) {\n> +               /* We're going to reset the algorithm here with these fixed values. */\n> +\n> +               fetchAwbStatus(metadata);\n> +               double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> +               ASSERT(minColourGain != 0.0);\n> +\n> +               /* This is the equivalent of computeTargetExposure and applyDigitalGain. */\n> +               target_.totalExposureNoDG = fixedShutter_ * fixedAnalogueGain_;\n> +               target_.totalExposure = target_.totalExposureNoDG / minColourGain;\n> +\n> +               /* Equivalent of filterExposure. This resets any \"history\". */\n> +               filtered_ = target_;\n> +\n> +               /* Equivalent of divideUpExposure. */\n> +               filtered_.shutter = fixedShutter;\n> +               filtered_.analogueGain = fixedAnalogueGain_;\n> +       } else if (status_.totalExposureValue) {\n> +               /*\n> +                * On a mode switch, various things could happen:\n> +                * - the exposure profile might change\n> +                * - a fixed exposure or gain might be set\n> +                * - the new mode's sensitivity might be different\n> +                * We cope with the last of these by scaling the target values. After\n> +                * that we just need to re-divide the exposure/gain according to the\n> +                * current exposure profile, which takes care of everything else.\n> +                */\n> +\n> +               double ratio = lastSensitivity / cameraMode.sensitivity;\n> +               target_.totalExposureNoDG *= ratio;\n> +               target_.totalExposure *= ratio;\n> +               filtered_.totalExposureNoDG *= ratio;\n> +               filtered_.totalExposure *= ratio;\n> +\n> +               divideUpExposure();\n> +       } else {\n> +               /*\n> +                * We come through here on startup, when at least one of the shutter\n> +                * or gain has not been fixed. We must still write those values out so\n> +                * that they will be applied immediately. We supply some arbitrary defaults\n> +                * for any that weren't set.\n> +                */\n> +\n> +               /* Equivalent of divideUpExposure. */\n> +               filtered_.shutter = fixedShutter ? fixedShutter : config_.defaultExposureTime;\n> +               filtered_.analogueGain = fixedAnalogueGain_ ? fixedAnalogueGain_ : config_.defaultAnalogueGain;\n> +       }\n> +\n> +       writeAndFinish(metadata, false);\n> +}\n> +\n> +void AgcChannel::prepare(Metadata *imageMetadata)\n> +{\n> +       Duration totalExposureValue = status_.totalExposureValue;\n> +       AgcStatus delayedStatus;\n> +       AgcPrepareStatus prepareStatus;\n> +\n> +       if (!imageMetadata->get(\"agc.delayed_status\", delayedStatus))\n> +               totalExposureValue = delayedStatus.totalExposureValue;\n> +\n> +       prepareStatus.digitalGain = 1.0;\n> +       prepareStatus.locked = false;\n> +\n> +       if (status_.totalExposureValue) {\n> +               /* Process has run, so we have meaningful values. */\n> +               DeviceStatus deviceStatus;\n> +               if (imageMetadata->get(\"device.status\", deviceStatus) == 0) {\n> +                       Duration actualExposure = deviceStatus.shutterSpeed *\n> +                                                 deviceStatus.analogueGain;\n> +                       if (actualExposure) {\n> +                               double digitalGain = totalExposureValue / actualExposure;\n> +                               LOG(RPiAgc, Debug) << \"Want total exposure \" << totalExposureValue;\n> +                               /*\n> +                                * Never ask for a gain < 1.0, and also impose\n> +                                * some upper limit. Make it customisable?\n> +                                */\n> +                               prepareStatus.digitalGain = std::max(1.0, std::min(digitalGain, 4.0));\n> +                               LOG(RPiAgc, Debug) << \"Actual exposure \" << actualExposure;\n> +                               LOG(RPiAgc, Debug) << \"Use digitalGain \" << prepareStatus.digitalGain;\n> +                               LOG(RPiAgc, Debug) << \"Effective exposure \"\n> +                                                  << actualExposure * prepareStatus.digitalGain;\n> +                               /* Decide whether AEC/AGC has converged. */\n> +                               prepareStatus.locked = updateLockStatus(deviceStatus);\n> +                       }\n> +               } else\n> +                       LOG(RPiAgc, Warning) << \"AgcChannel: no device metadata\";\n> +               imageMetadata->set(\"agc.prepare_status\", prepareStatus);\n> +       }\n> +}\n> +\n> +void AgcChannel::process(StatisticsPtr &stats, Metadata *imageMetadata)\n> +{\n> +       frameCount_++;\n> +       /*\n> +        * First a little bit of housekeeping, fetching up-to-date settings and\n> +        * configuration, that kind of thing.\n> +        */\n> +       housekeepConfig();\n> +       /* Fetch the AWB status immediately, so that we can assume it's there. */\n> +       fetchAwbStatus(imageMetadata);\n> +       /* Get the current exposure values for the frame that's just arrived. */\n> +       fetchCurrentExposure(imageMetadata);\n> +       /* Compute the total gain we require relative to the current exposure. */\n> +       double gain, targetY;\n> +       computeGain(stats, imageMetadata, gain, targetY);\n> +       /* Now compute the target (final) exposure which we think we want. */\n> +       computeTargetExposure(gain);\n> +       /* The results have to be filtered so as not to change too rapidly. */\n> +       filterExposure();\n> +       /*\n> +        * Some of the exposure has to be applied as digital gain, so work out\n> +        * what that is. This function also tells us whether it's decided to\n> +        * \"desaturate\" the image more quickly.\n> +        */\n> +       bool desaturate = applyDigitalGain(gain, targetY);\n> +       /*\n> +        * The last thing is to divide up the exposure value into a shutter time\n> +        * and analogue gain, according to the current exposure mode.\n> +        */\n> +       divideUpExposure();\n> +       /* Finally advertise what we've done. */\n> +       writeAndFinish(imageMetadata, desaturate);\n> +}\n> +\n> +bool AgcChannel::updateLockStatus(DeviceStatus const &deviceStatus)\n> +{\n> +       const double errorFactor = 0.10; /* make these customisable? */\n> +       const int maxLockCount = 5;\n> +       /* Reset \"lock count\" when we exceed this multiple of errorFactor */\n> +       const double resetMargin = 1.5;\n> +\n> +       /* Add 200us to the exposure time error to allow for line quantisation. */\n> +       Duration exposureError = lastDeviceStatus_.shutterSpeed * errorFactor + 200us;\n> +       double gainError = lastDeviceStatus_.analogueGain * errorFactor;\n> +       Duration targetError = lastTargetExposure_ * errorFactor;\n> +\n> +       /*\n> +        * Note that we don't know the exposure/gain limits of the sensor, so\n> +        * the values we keep requesting may be unachievable. For this reason\n> +        * we only insist that we're close to values in the past few frames.\n> +        */\n> +       if (deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed - exposureError &&\n> +           deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed + exposureError &&\n> +           deviceStatus.analogueGain > lastDeviceStatus_.analogueGain - gainError &&\n> +           deviceStatus.analogueGain < lastDeviceStatus_.analogueGain + gainError &&\n> +           status_.targetExposureValue > lastTargetExposure_ - targetError &&\n> +           status_.targetExposureValue < lastTargetExposure_ + targetError)\n> +               lockCount_ = std::min(lockCount_ + 1, maxLockCount);\n> +       else if (deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed - resetMargin * exposureError ||\n> +                deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed + resetMargin * exposureError ||\n> +                deviceStatus.analogueGain < lastDeviceStatus_.analogueGain - resetMargin * gainError ||\n> +                deviceStatus.analogueGain > lastDeviceStatus_.analogueGain + resetMargin * gainError ||\n> +                status_.targetExposureValue < lastTargetExposure_ - resetMargin * targetError ||\n> +                status_.targetExposureValue > lastTargetExposure_ + resetMargin * targetError)\n> +               lockCount_ = 0;\n> +\n> +       lastDeviceStatus_ = deviceStatus;\n> +       lastTargetExposure_ = status_.targetExposureValue;\n> +\n> +       LOG(RPiAgc, Debug) << \"Lock count updated to \" << lockCount_;\n> +       return lockCount_ == maxLockCount;\n> +}\n> +\n> +void AgcChannel::housekeepConfig()\n> +{\n> +       /* First fetch all the up-to-date settings, so no one else has to do it. */\n> +       status_.ev = ev_;\n> +       status_.fixedShutter = limitShutter(fixedShutter_);\n> +       status_.fixedAnalogueGain = fixedAnalogueGain_;\n> +       status_.flickerPeriod = flickerPeriod_;\n> +       LOG(RPiAgc, Debug) << \"ev \" << status_.ev << \" fixedShutter \"\n> +                          << status_.fixedShutter << \" fixedAnalogueGain \"\n> +                          << status_.fixedAnalogueGain;\n> +       /*\n> +        * Make sure the \"mode\" pointers point to the up-to-date things, if\n> +        * they've changed.\n> +        */\n> +       if (meteringModeName_ != status_.meteringMode) {\n> +               auto it = config_.meteringModes.find(meteringModeName_);\n> +               if (it == config_.meteringModes.end()) {\n> +                       LOG(RPiAgc, Warning) << \"No metering mode \" << meteringModeName_;\n> +                       meteringModeName_ = status_.meteringMode;\n> +               } else {\n> +                       meteringMode_ = &it->second;\n> +                       status_.meteringMode = meteringModeName_;\n> +               }\n> +       }\n> +       if (exposureModeName_ != status_.exposureMode) {\n> +               auto it = config_.exposureModes.find(exposureModeName_);\n> +               if (it == config_.exposureModes.end()) {\n> +                       LOG(RPiAgc, Warning) << \"No exposure profile \" << exposureModeName_;\n> +                       exposureModeName_ = status_.exposureMode;\n> +               } else {\n> +                       exposureMode_ = &it->second;\n> +                       status_.exposureMode = exposureModeName_;\n> +               }\n> +       }\n> +       if (constraintModeName_ != status_.constraintMode) {\n> +               auto it = config_.constraintModes.find(constraintModeName_);\n> +               if (it == config_.constraintModes.end()) {\n> +                       LOG(RPiAgc, Warning) << \"No constraint list \" << constraintModeName_;\n> +                       constraintModeName_ = status_.constraintMode;\n> +               } else {\n> +                       constraintMode_ = &it->second;\n> +                       status_.constraintMode = constraintModeName_;\n> +               }\n> +       }\n> +       LOG(RPiAgc, Debug) << \"exposureMode \"\n> +                          << exposureModeName_ << \" constraintMode \"\n> +                          << constraintModeName_ << \" meteringMode \"\n> +                          << meteringModeName_;\n> +}\n> +\n> +void AgcChannel::fetchCurrentExposure(Metadata *imageMetadata)\n> +{\n> +       std::unique_lock<Metadata> lock(*imageMetadata);\n> +       DeviceStatus *deviceStatus =\n> +               imageMetadata->getLocked<DeviceStatus>(\"device.status\");\n> +       if (!deviceStatus)\n> +               LOG(RPiAgc, Fatal) << \"No device metadata\";\n> +       current_.shutter = deviceStatus->shutterSpeed;\n> +       current_.analogueGain = deviceStatus->analogueGain;\n> +       AgcStatus *agcStatus =\n> +               imageMetadata->getLocked<AgcStatus>(\"agc.status\");\n> +       current_.totalExposure = agcStatus ? agcStatus->totalExposureValue : 0s;\n> +       current_.totalExposureNoDG = current_.shutter * current_.analogueGain;\n> +}\n> +\n> +void AgcChannel::fetchAwbStatus(Metadata *imageMetadata)\n> +{\n> +       awb_.gainR = 1.0; /* in case not found in metadata */\n> +       awb_.gainG = 1.0;\n> +       awb_.gainB = 1.0;\n> +       if (imageMetadata->get(\"awb.status\", awb_) != 0)\n> +               LOG(RPiAgc, Debug) << \"No AWB status found\";\n> +}\n> +\n> +static double computeInitialY(StatisticsPtr &stats, AwbStatus const &awb,\n> +                             std::vector<double> &weights, double gain)\n> +{\n> +       constexpr uint64_t maxVal = 1 << Statistics::NormalisationFactorPow2;\n> +\n> +       /*\n> +        * If we have no AGC region stats, but do have a a Y histogram, use that\n> +        * directly to caluclate the mean Y value of the image.\n> +        */\n> +       if (!stats->agcRegions.numRegions() && stats->yHist.bins()) {\n> +               /*\n> +                * When the gain is applied to the histogram, anything below minBin\n> +                * will scale up directly with the gain, but anything above that\n> +                * will saturate into the top bin.\n> +                */\n> +               auto &hist = stats->yHist;\n> +               double minBin = std::min(1.0, 1.0 / gain) * hist.bins();\n> +               double binMean = hist.interBinMean(0.0, minBin);\n> +               double numUnsaturated = hist.cumulativeFreq(minBin);\n> +               /* This term is from all the pixels that won't saturate. */\n> +               double ySum = binMean * gain * numUnsaturated;\n> +               /* And add the ones that will saturate. */\n> +               ySum += (hist.total() - numUnsaturated) * hist.bins();\n> +               return ySum / hist.total() / hist.bins();\n> +       }\n> +\n> +       ASSERT(weights.size() == stats->agcRegions.numRegions());\n> +\n> +       /*\n> +        * Note that the weights are applied by the IPA to the statistics directly,\n> +        * before they are given to us here.\n> +        */\n> +       double rSum = 0, gSum = 0, bSum = 0, pixelSum = 0;\n> +       for (unsigned int i = 0; i < stats->agcRegions.numRegions(); i++) {\n> +               auto &region = stats->agcRegions.get(i);\n> +               rSum += std::min<double>(region.val.rSum * gain, (maxVal - 1) * region.counted);\n> +               gSum += std::min<double>(region.val.gSum * gain, (maxVal - 1) * region.counted);\n> +               bSum += std::min<double>(region.val.bSum * gain, (maxVal - 1) * region.counted);\n> +               pixelSum += region.counted;\n> +       }\n> +       if (pixelSum == 0.0) {\n> +               LOG(RPiAgc, Warning) << \"computeInitialY: pixelSum is zero\";\n> +               return 0;\n> +       }\n> +\n> +       double ySum;\n> +       /* Factor in the AWB correction if needed. */\n> +       if (stats->agcStatsPos == Statistics::AgcStatsPos::PreWb) {\n> +               ySum = rSum * awb.gainR * .299 +\n> +                      gSum * awb.gainG * .587 +\n> +                      gSum * awb.gainB * .114;\n> +       } else\n> +               ySum = rSum * .299 + gSum * .587 + gSum * .114;\n> +\n> +       return ySum / pixelSum / (1 << 16);\n> +}\n> +\n> +/*\n> + * We handle extra gain through EV by adjusting our Y targets. However, you\n> + * simply can't monitor histograms once they get very close to (or beyond!)\n> + * saturation, so we clamp the Y targets to this value. It does mean that EV\n> + * increases don't necessarily do quite what you might expect in certain\n> + * (contrived) cases.\n> + */\n> +\n> +static constexpr double EvGainYTargetLimit = 0.9;\n> +\n> +static double constraintComputeGain(AgcConstraint &c, const Histogram &h, double lux,\n> +                                   double evGain, double &targetY)\n> +{\n> +       targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));\n> +       targetY = std::min(EvGainYTargetLimit, targetY * evGain);\n> +       double iqm = h.interQuantileMean(c.qLo, c.qHi);\n> +       return (targetY * h.bins()) / iqm;\n> +}\n> +\n> +void AgcChannel::computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,\n> +                            double &gain, double &targetY)\n> +{\n> +       struct LuxStatus lux = {};\n> +       lux.lux = 400; /* default lux level to 400 in case no metadata found */\n> +       if (imageMetadata->get(\"lux.status\", lux) != 0)\n> +               LOG(RPiAgc, Warning) << \"No lux level found\";\n> +       const Histogram &h = statistics->yHist;\n> +       double evGain = status_.ev * config_.baseEv;\n> +       /*\n> +        * The initial gain and target_Y come from some of the regions. After\n> +        * that we consider the histogram constraints.\n> +        */\n> +       targetY = config_.yTarget.eval(config_.yTarget.domain().clip(lux.lux));\n> +       targetY = std::min(EvGainYTargetLimit, targetY * evGain);\n> +\n> +       /*\n> +        * Do this calculation a few times as brightness increase can be\n> +        * non-linear when there are saturated regions.\n> +        */\n> +       gain = 1.0;\n> +       for (int i = 0; i < 8; i++) {\n> +               double initialY = computeInitialY(statistics, awb_, meteringMode_->weights, gain);\n> +               double extraGain = std::min(10.0, targetY / (initialY + .001));\n> +               gain *= extraGain;\n> +               LOG(RPiAgc, Debug) << \"Initial Y \" << initialY << \" target \" << targetY\n> +                                  << \" gives gain \" << gain;\n> +               if (extraGain < 1.01) /* close enough */\n> +                       break;\n> +       }\n> +\n> +       for (auto &c : *constraintMode_) {\n> +               double newTargetY;\n> +               double newGain = constraintComputeGain(c, h, lux.lux, evGain, newTargetY);\n> +               LOG(RPiAgc, Debug) << \"Constraint has target_Y \"\n> +                                  << newTargetY << \" giving gain \" << newGain;\n> +               if (c.bound == AgcConstraint::Bound::LOWER && newGain > gain) {\n> +                       LOG(RPiAgc, Debug) << \"Lower bound constraint adopted\";\n> +                       gain = newGain;\n> +                       targetY = newTargetY;\n> +               } else if (c.bound == AgcConstraint::Bound::UPPER && newGain < gain) {\n> +                       LOG(RPiAgc, Debug) << \"Upper bound constraint adopted\";\n> +                       gain = newGain;\n> +                       targetY = newTargetY;\n> +               }\n> +       }\n> +       LOG(RPiAgc, Debug) << \"Final gain \" << gain << \" (target_Y \" << targetY << \" ev \"\n> +                          << status_.ev << \" base_ev \" << config_.baseEv\n> +                          << \")\";\n> +}\n> +\n> +void AgcChannel::computeTargetExposure(double gain)\n> +{\n> +       if (status_.fixedShutter && status_.fixedAnalogueGain) {\n> +               /*\n> +                * When ag and shutter are both fixed, we need to drive the\n> +                * total exposure so that we end up with a digital gain of at least\n> +                * 1/minColourGain. Otherwise we'd desaturate channels causing\n> +                * white to go cyan or magenta.\n> +                */\n> +               double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> +               ASSERT(minColourGain != 0.0);\n> +               target_.totalExposure =\n> +                       status_.fixedShutter * status_.fixedAnalogueGain / minColourGain;\n> +       } else {\n> +               /*\n> +                * The statistics reflect the image without digital gain, so the final\n> +                * total exposure we're aiming for is:\n> +                */\n> +               target_.totalExposure = current_.totalExposureNoDG * gain;\n> +               /* The final target exposure is also limited to what the exposure mode allows. */\n> +               Duration maxShutter = status_.fixedShutter\n> +                                             ? status_.fixedShutter\n> +                                             : exposureMode_->shutter.back();\n> +               maxShutter = limitShutter(maxShutter);\n> +               Duration maxTotalExposure =\n> +                       maxShutter *\n> +                       (status_.fixedAnalogueGain != 0.0\n> +                                ? status_.fixedAnalogueGain\n> +                                : exposureMode_->gain.back());\n> +               target_.totalExposure = std::min(target_.totalExposure, maxTotalExposure);\n> +       }\n> +       LOG(RPiAgc, Debug) << \"Target totalExposure \" << target_.totalExposure;\n> +}\n> +\n> +bool AgcChannel::applyDigitalGain(double gain, double targetY)\n> +{\n> +       double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> +       ASSERT(minColourGain != 0.0);\n> +       double dg = 1.0 / minColourGain;\n> +       /*\n> +        * I think this pipeline subtracts black level and rescales before we\n> +        * get the stats, so no need to worry about it.\n> +        */\n> +       LOG(RPiAgc, Debug) << \"after AWB, target dg \" << dg << \" gain \" << gain\n> +                          << \" target_Y \" << targetY;\n> +       /*\n> +        * Finally, if we're trying to reduce exposure but the target_Y is\n> +        * \"close\" to 1.0, then the gain computed for that constraint will be\n> +        * only slightly less than one, because the measured Y can never be\n> +        * larger than 1.0. When this happens, demand a large digital gain so\n> +        * that the exposure can be reduced, de-saturating the image much more\n> +        * quickly (and we then approach the correct value more quickly from\n> +        * below).\n> +        */\n> +       bool desaturate = targetY > config_.fastReduceThreshold &&\n> +                         gain < sqrt(targetY);\n> +       if (desaturate)\n> +               dg /= config_.fastReduceThreshold;\n> +       LOG(RPiAgc, Debug) << \"Digital gain \" << dg << \" desaturate? \" << desaturate;\n> +       filtered_.totalExposureNoDG = filtered_.totalExposure / dg;\n> +       LOG(RPiAgc, Debug) << \"Target totalExposureNoDG \" << filtered_.totalExposureNoDG;\n> +       return desaturate;\n> +}\n> +\n> +void AgcChannel::filterExposure()\n> +{\n> +       double speed = config_.speed;\n> +       /*\n> +        * AGC adapts instantly if both shutter and gain are directly specified\n> +        * or we're in the startup phase.\n> +        */\n> +       if ((status_.fixedShutter && status_.fixedAnalogueGain) ||\n> +           frameCount_ <= config_.startupFrames)\n> +               speed = 1.0;\n> +       if (!filtered_.totalExposure) {\n> +               filtered_.totalExposure = target_.totalExposure;\n> +       } else {\n> +               /*\n> +                * If close to the result go faster, to save making so many\n> +                * micro-adjustments on the way. (Make this customisable?)\n> +                */\n> +               if (filtered_.totalExposure < 1.2 * target_.totalExposure &&\n> +                   filtered_.totalExposure > 0.8 * target_.totalExposure)\n> +                       speed = sqrt(speed);\n> +               filtered_.totalExposure = speed * target_.totalExposure +\n> +                                         filtered_.totalExposure * (1.0 - speed);\n> +       }\n> +       LOG(RPiAgc, Debug) << \"After filtering, totalExposure \" << filtered_.totalExposure\n> +                          << \" no dg \" << filtered_.totalExposureNoDG;\n> +}\n> +\n> +void AgcChannel::divideUpExposure()\n> +{\n> +       /*\n> +        * Sending the fixed shutter/gain cases through the same code may seem\n> +        * unnecessary, but it will make more sense when extend this to cover\n> +        * variable aperture.\n> +        */\n> +       Duration exposureValue = filtered_.totalExposureNoDG;\n> +       Duration shutterTime;\n> +       double analogueGain;\n> +       shutterTime = status_.fixedShutter ? status_.fixedShutter\n> +                                          : exposureMode_->shutter[0];\n> +       shutterTime = limitShutter(shutterTime);\n> +       analogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain\n> +                                                       : exposureMode_->gain[0];\n> +       analogueGain = limitGain(analogueGain);\n> +       if (shutterTime * analogueGain < exposureValue) {\n> +               for (unsigned int stage = 1;\n> +                    stage < exposureMode_->gain.size(); stage++) {\n> +                       if (!status_.fixedShutter) {\n> +                               Duration stageShutter =\n> +                                       limitShutter(exposureMode_->shutter[stage]);\n> +                               if (stageShutter * analogueGain >= exposureValue) {\n> +                                       shutterTime = exposureValue / analogueGain;\n> +                                       break;\n> +                               }\n> +                               shutterTime = stageShutter;\n> +                       }\n> +                       if (status_.fixedAnalogueGain == 0.0) {\n> +                               if (exposureMode_->gain[stage] * shutterTime >= exposureValue) {\n> +                                       analogueGain = exposureValue / shutterTime;\n> +                                       break;\n> +                               }\n> +                               analogueGain = exposureMode_->gain[stage];\n> +                               analogueGain = limitGain(analogueGain);\n> +                       }\n> +               }\n> +       }\n> +       LOG(RPiAgc, Debug) << \"Divided up shutter and gain are \" << shutterTime << \" and \"\n> +                          << analogueGain;\n> +       /*\n> +        * Finally adjust shutter time for flicker avoidance (require both\n> +        * shutter and gain not to be fixed).\n> +        */\n> +       if (!status_.fixedShutter && !status_.fixedAnalogueGain &&\n> +           status_.flickerPeriod) {\n> +               int flickerPeriods = shutterTime / status_.flickerPeriod;\n> +               if (flickerPeriods) {\n> +                       Duration newShutterTime = flickerPeriods * status_.flickerPeriod;\n> +                       analogueGain *= shutterTime / newShutterTime;\n> +                       /*\n> +                        * We should still not allow the ag to go over the\n> +                        * largest value in the exposure mode. Note that this\n> +                        * may force more of the total exposure into the digital\n> +                        * gain as a side-effect.\n> +                        */\n> +                       analogueGain = std::min(analogueGain, exposureMode_->gain.back());\n> +                       analogueGain = limitGain(analogueGain);\n> +                       shutterTime = newShutterTime;\n> +               }\n> +               LOG(RPiAgc, Debug) << \"After flicker avoidance, shutter \"\n> +                                  << shutterTime << \" gain \" << analogueGain;\n> +       }\n> +       filtered_.shutter = shutterTime;\n> +       filtered_.analogueGain = analogueGain;\n> +}\n> +\n> +void AgcChannel::writeAndFinish(Metadata *imageMetadata, bool desaturate)\n> +{\n> +       status_.totalExposureValue = filtered_.totalExposure;\n> +       status_.targetExposureValue = desaturate ? 0s : target_.totalExposureNoDG;\n> +       status_.shutterTime = filtered_.shutter;\n> +       status_.analogueGain = filtered_.analogueGain;\n> +       /*\n> +        * Write to metadata as well, in case anyone wants to update the camera\n> +        * immediately.\n> +        */\n> +       imageMetadata->set(\"agc.status\", status_);\n> +       LOG(RPiAgc, Debug) << \"Output written, total exposure requested is \"\n> +                          << filtered_.totalExposure;\n> +       LOG(RPiAgc, Debug) << \"Camera exposure update: shutter time \" << filtered_.shutter\n> +                          << \" analogue gain \" << filtered_.analogueGain;\n> +}\n> +\n> +Duration AgcChannel::limitShutter(Duration shutter)\n> +{\n> +       /*\n> +        * shutter == 0 is a special case for fixed shutter values, and must pass\n> +        * through unchanged\n> +        */\n> +       if (!shutter)\n> +               return shutter;\n> +\n> +       shutter = std::clamp(shutter, mode_.minShutter, maxShutter_);\n> +       return shutter;\n> +}\n> +\n> +double AgcChannel::limitGain(double gain) const\n> +{\n> +       /*\n> +        * Only limit the lower bounds of the gain value to what the sensor limits.\n> +        * The upper bound on analogue gain will be made up with additional digital\n> +        * gain applied by the ISP.\n> +        *\n> +        * gain == 0.0 is a special case for fixed shutter values, and must pass\n> +        * through unchanged\n> +        */\n> +       if (!gain)\n> +               return gain;\n> +\n> +       gain = std::max(gain, mode_.minAnalogueGain);\n> +       return gain;\n> +}\n> diff --git a/src/ipa/rpi/controller/rpi/agc_channel.h b/src/ipa/rpi/controller/rpi/agc_channel.h\n> new file mode 100644\n> index 00000000..dc4356f3\n> --- /dev/null\n> +++ b/src/ipa/rpi/controller/rpi/agc_channel.h\n> @@ -0,0 +1,135 @@\n> +/* SPDX-License-Identifier: BSD-2-Clause */\n> +/*\n> + * Copyright (C) 2019, Raspberry Pi Ltd\n> + *\n> + * agc.h - AGC/AEC control algorithm\n> + */\n> +#pragma once\n> +\n> +#include <mutex>\n> +#include <vector>\n> +\n> +#include <libcamera/base/utils.h>\n> +\n> +#include \"../agc_status.h\"\n> +#include \"../awb_status.h\"\n> +#include \"../pwl.h\"\n> +\n> +/* This is our implementation of AGC. */\n> +\n> +namespace RPiController {\n> +\n> +struct AgcMeteringMode {\n> +       std::vector<double> weights;\n> +       int read(const libcamera::YamlObject &params);\n> +};\n> +\n> +struct AgcExposureMode {\n> +       std::vector<libcamera::utils::Duration> shutter;\n> +       std::vector<double> gain;\n> +       int read(const libcamera::YamlObject &params);\n> +};\n> +\n> +struct AgcConstraint {\n> +       enum class Bound { LOWER = 0,\n> +                          UPPER = 1 };\n> +       Bound bound;\n> +       double qLo;\n> +       double qHi;\n> +       Pwl yTarget;\n> +       int read(const libcamera::YamlObject &params);\n> +};\n> +\n> +typedef std::vector<AgcConstraint> AgcConstraintMode;\n> +\n> +struct AgcConfig {\n> +       int read(const libcamera::YamlObject &params);\n> +       std::map<std::string, AgcMeteringMode> meteringModes;\n> +       std::map<std::string, AgcExposureMode> exposureModes;\n> +       std::map<std::string, AgcConstraintMode> constraintModes;\n> +       Pwl yTarget;\n> +       double speed;\n> +       uint16_t startupFrames;\n> +       unsigned int convergenceFrames;\n> +       double maxChange;\n> +       double minChange;\n> +       double fastReduceThreshold;\n> +       double speedUpThreshold;\n> +       std::string defaultMeteringMode;\n> +       std::string defaultExposureMode;\n> +       std::string defaultConstraintMode;\n> +       double baseEv;\n> +       libcamera::utils::Duration defaultExposureTime;\n> +       double defaultAnalogueGain;\n> +};\n> +\n> +class AgcChannel\n> +{\n> +public:\n> +       AgcChannel();\n> +       int read(const libcamera::YamlObject &params,\n> +                const Controller::HardwareConfig &hardwareConfig);\n> +       unsigned int getConvergenceFrames() const;\n> +       std::vector<double> const &getWeights() const;\n> +       void setEv(double ev);\n> +       void setFlickerPeriod(libcamera::utils::Duration flickerPeriod);\n> +       void setMaxShutter(libcamera::utils::Duration maxShutter);\n> +       void setFixedShutter(libcamera::utils::Duration fixedShutter);\n> +       void setFixedAnalogueGain(double fixedAnalogueGain);\n> +       void setMeteringMode(std::string const &meteringModeName);\n> +       void setExposureMode(std::string const &exposureModeName);\n> +       void setConstraintMode(std::string const &contraintModeName);\n> +       void enableAuto();\n> +       void disableAuto();\n> +       void switchMode(CameraMode const &cameraMode, Metadata *metadata);\n> +       void prepare(Metadata *imageMetadata);\n> +       void process(StatisticsPtr &stats, Metadata *imageMetadata);\n> +\n> +private:\n> +       bool updateLockStatus(DeviceStatus const &deviceStatus);\n> +       AgcConfig config_;\n> +       void housekeepConfig();\n> +       void fetchCurrentExposure(Metadata *imageMetadata);\n> +       void fetchAwbStatus(Metadata *imageMetadata);\n> +       void computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,\n> +                        double &gain, double &targetY);\n> +       void computeTargetExposure(double gain);\n> +       void filterExposure();\n> +       bool applyDigitalGain(double gain, double targetY);\n> +       void divideUpExposure();\n> +       void writeAndFinish(Metadata *imageMetadata, bool desaturate);\n> +       libcamera::utils::Duration limitShutter(libcamera::utils::Duration shutter);\n> +       double limitGain(double gain) const;\n> +       AgcMeteringMode *meteringMode_;\n> +       AgcExposureMode *exposureMode_;\n> +       AgcConstraintMode *constraintMode_;\n> +       CameraMode mode_;\n> +       uint64_t frameCount_;\n> +       AwbStatus awb_;\n> +       struct ExposureValues {\n> +               ExposureValues();\n> +\n> +               libcamera::utils::Duration shutter;\n> +               double analogueGain;\n> +               libcamera::utils::Duration totalExposure;\n> +               libcamera::utils::Duration totalExposureNoDG; /* without digital gain */\n> +       };\n> +       ExposureValues current_; /* values for the current frame */\n> +       ExposureValues target_; /* calculate the values we want here */\n> +       ExposureValues filtered_; /* these values are filtered towards target */\n> +       AgcStatus status_;\n> +       int lockCount_;\n> +       DeviceStatus lastDeviceStatus_;\n> +       libcamera::utils::Duration lastTargetExposure_;\n> +       /* Below here the \"settings\" that applications can change. */\n> +       std::string meteringModeName_;\n> +       std::string exposureModeName_;\n> +       std::string constraintModeName_;\n> +       double ev_;\n> +       libcamera::utils::Duration flickerPeriod_;\n> +       libcamera::utils::Duration maxShutter_;\n> +       libcamera::utils::Duration fixedShutter_;\n> +       double fixedAnalogueGain_;\n> +};\n> +\n> +} /* namespace RPiController */\n> --\n> 2.30.2\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 3357DBE080\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 22 Aug 2023 10:26:08 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 55631627E0;\n\tTue, 22 Aug 2023 12:26:07 +0200 (CEST)","from mail-yw1-x1132.google.com (mail-yw1-x1132.google.com\n\t[IPv6:2607:f8b0:4864:20::1132])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 314F86055E\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 22 Aug 2023 12:26:05 +0200 (CEST)","by mail-yw1-x1132.google.com with SMTP id\n\t00721157ae682-58d41109351so73477087b3.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 22 Aug 2023 03:26:05 -0700 (PDT)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1692699967;\n\tbh=3Pyiiq9ew7A7d2+s0z5kR0K1bqSlF2iIoMkirc3yRvE=;\n\th=References:In-Reply-To:Date:To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=tMCqjhDRMhe7kNJFMnFoqv4I/CVp90LTRStY8VdtndUJUqfVQus6Og1CAFZ898Z/f\n\t6LhXWF7tR3Tp2gOefRh0fifeUev/RzXLqgSVQBBAJa+5ylh38jxJPaBUL5TdbTZms+\n\tzThmDYb9a5+q0Y9dFhKe8rGoVRDHMdoLzobWPKb9HgdThGlcwuhjYDgX0dmqS6LANk\n\tHIyY078xLw4IUAXj4rjT1K3bDMEKZHWEO+KyLzad+57CmGRJdrdWohVBRRMKvOc1OY\n\t4nv1OOQ7VjOfk01u5s2DsJUMYSwOPtA+A7bTNAgL/ZVsNVzYxdbsidWMKZNYobHbve\n\tJy9g7vgLiI+Gw==","v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1692699964; x=1693304764;\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=swUuibnU9y+XJLyHxbStRd+x5cHwOT+zJepTl+zLTzE=;\n\tb=JvqD8iiePUwunmkCGfSUhaxkIRTu4HhJzESCway//qJjelNHm27g76RXKG+PtuDx/7\n\t06pTMf5kj5Z6yMhWcjyImHlXv/CtDDdHtsGOak7G4r45tgpbOS//AgexUfGPFrFLPxkj\n\tF6ebi4iSR/aLR2c/wIFIA9RlohKxCPMCyE/EeTjziHM2ruiWhe3bD6WaIrXlNUqfRtvi\n\t3R+DYV0altTVIt7ASq/rI0AtzjvE0TIFkru52gk1miBoat/aOYJITqN+KFtnFxqdG3HS\n\tYy/wdk3PlP/37nRidAK8bgIKx6/1pkfFY9ZujUc/CFtL9mzqHY3bsboSJhpJNb6lYrKZ\n\t695g=="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key; \n\tunprotected) header.d=raspberrypi.com\n\theader.i=@raspberrypi.com\n\theader.b=\"JvqD8iie\"; dkim-atps=neutral","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20221208; t=1692699964; x=1693304764;\n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:x-gm-message-state:from:to:cc:subject:date:message-id\n\t:reply-to;\n\tbh=swUuibnU9y+XJLyHxbStRd+x5cHwOT+zJepTl+zLTzE=;\n\tb=U1qd/gk4+uGLgHZVHqtI6ShiiRzuD7q2r7/FATweSKCMcTHc+Mm2W8411YDfPDIhZ0\n\t/DST8hyCF738M72y79JFFJwYDwAj9jYHb5Za9kt3cF9OVMLiCMLieKDzVVEc0eQesi+g\n\tkfeZEs+m26DTPp26ga2OyyYHYMUgQrac1DpL9v2lG5eYKT/i8D+uxlMsl92ytbjD5W2G\n\tfWW+9SnUZK4tX+kuJpdXLIU/9y89t6MMg4r8jnDE8jl3Zv3wcv2fI6+0StywZS5w2BwK\n\t1RPqFFGEBpl3Me+mMBld5L9DpqL9tFrBsT8UcdT8nUpjDewpKVdSVUeeRZK0V9FAiUkm\n\tQ9/Q==","X-Gm-Message-State":"AOJu0YzFb1Gcq9aXUCTiE9mzJ9kEDHhYtgKKHArFRPajZlZcbF9rDhsh\n\t6f53h6p5q6Bv+PCGYG+mHsUxPdsIZi15lXKr3xI0GA==","X-Google-Smtp-Source":"AGHT+IGcbkW5jeAadkgDduHQvK2wcP1Q0+gJXnh9XaaITtQujVrJEskznH5FbmHSy+KgGibCExSWVru0N1DRGlrRWHw=","X-Received":"by 2002:a0d:d7d2:0:b0:581:7958:5bda with SMTP id\n\tz201-20020a0dd7d2000000b0058179585bdamr7428501ywd.1.1692699962629;\n\tTue, 22 Aug 2023 03:26:02 -0700 (PDT)","MIME-Version":"1.0","References":"<20230731094641.73646-1-david.plowman@raspberrypi.com>\n\t<20230731094641.73646-3-david.plowman@raspberrypi.com>","In-Reply-To":"<20230731094641.73646-3-david.plowman@raspberrypi.com>","Date":"Tue, 22 Aug 2023 11:25:55 +0100","Message-ID":"<CAEmqJPoY91riFPHedVsh0D-x4-VGx599EB-jkb_yNbMmv6890g@mail.gmail.com>","To":"David Plowman <david.plowman@raspberrypi.com>","Content-Type":"text/plain; charset=\"UTF-8\"","Subject":"Re: [libcamera-devel] [PATCH 2/5] ipa: rpi: agc: Reorganise code\n\tfor multi-channel AGC","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>","From":"Naushir Patuck via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Naushir Patuck <naush@raspberrypi.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":27683,"web_url":"https://patchwork.libcamera.org/comment/27683/","msgid":"<CAEmqJPqPXijrHGZDwinEqvKS-T8fC7uV74b6Y+m0i4M_jxHpXQ@mail.gmail.com>","date":"2023-08-22T12:32:08","subject":"Re: [libcamera-devel] [PATCH 2/5] ipa: rpi: agc: Reorganise code\n\tfor multi-channel AGC","submitter":{"id":34,"url":"https://patchwork.libcamera.org/api/people/34/","name":"Naushir Patuck","email":"naush@raspberrypi.com"},"content":"Hi David,\n\nThank you for your patch.\n\nOn Mon, 31 Jul 2023 at 10:47, David Plowman via libcamera-devel\n<libcamera-devel@lists.libcamera.org> wrote:\n>\n> This commit does the basic reorganisation of the code in order to\n> implement multi-channel AGC. The main changes are:\n>\n> * The previous Agc class (in agc.cpp) has become the AgcChannel class\n>   in (agc_channel.cpp).\n>\n> * A new Agc class is introduced which is a wrapper round a number of\n>   AgcChannels.\n>\n> * The basic plumbing from ipa_base.cpp to Agc is updated to include a\n>   channel number. All the existing controls are hardwired to talk\n>   directly to channel 0.\n>\n> There are a couple of limitations which we expect to apply to\n> multi-channel AGC. We're not allowing different frame durations to be\n> applied to the channels, nor are we allowing separate metering\n> modes. To be fair, supporting these things is not impossible, but\n> there are reasons why it may be tricky so they remain \"TBD\" for now.\n>\n> This patch only includes the basic reorganisation and plumbing. It\n> does not yet update the important methods (switchMode, prepare and\n> process) to implement multi-channel AGC properly. This will appear in\n> a subsequent commit. For now, these functions are hard-coded just to\n> use channel 0, thereby preserving the existing behaviour.\n>\n> Signed-off-by: David Plowman <david.plowman@raspberrypi.com>\n\nDon't see anything wrong with this change\n\nReviewed-by: Naushir Patuck <naush@raspberrypi.com>\n\n\n> ---\n>  src/ipa/rpi/common/ipa_base.cpp            |  14 +-\n>  src/ipa/rpi/controller/agc_algorithm.h     |  19 +-\n>  src/ipa/rpi/controller/meson.build         |   1 +\n>  src/ipa/rpi/controller/rpi/agc.cpp         | 910 +++-----------------\n>  src/ipa/rpi/controller/rpi/agc.h           | 122 +--\n>  src/ipa/rpi/controller/rpi/agc_channel.cpp | 927 +++++++++++++++++++++\n>  src/ipa/rpi/controller/rpi/agc_channel.h   | 135 +++\n>  7 files changed, 1216 insertions(+), 912 deletions(-)\n>  create mode 100644 src/ipa/rpi/controller/rpi/agc_channel.cpp\n>  create mode 100644 src/ipa/rpi/controller/rpi/agc_channel.h\n>\n> diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp\n> index 6ae84cc6..f29c32fd 100644\n> --- a/src/ipa/rpi/common/ipa_base.cpp\n> +++ b/src/ipa/rpi/common/ipa_base.cpp\n> @@ -692,9 +692,9 @@ void IpaBase::applyControls(const ControlList &controls)\n>                         }\n>\n>                         if (ctrl.second.get<bool>() == false)\n> -                               agc->disableAuto();\n> +                               agc->disableAuto(0);\n>                         else\n> -                               agc->enableAuto();\n> +                               agc->enableAuto(0);\n>\n>                         libcameraMetadata_.set(controls::AeEnable, ctrl.second.get<bool>());\n>                         break;\n> @@ -710,7 +710,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>                         }\n>\n>                         /* The control provides units of microseconds. */\n> -                       agc->setFixedShutter(ctrl.second.get<int32_t>() * 1.0us);\n> +                       agc->setFixedShutter(0, ctrl.second.get<int32_t>() * 1.0us);\n>\n>                         libcameraMetadata_.set(controls::ExposureTime, ctrl.second.get<int32_t>());\n>                         break;\n> @@ -725,7 +725,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>                                 break;\n>                         }\n>\n> -                       agc->setFixedAnalogueGain(ctrl.second.get<float>());\n> +                       agc->setFixedAnalogueGain(0, ctrl.second.get<float>());\n>\n>                         libcameraMetadata_.set(controls::AnalogueGain,\n>                                                ctrl.second.get<float>());\n> @@ -763,7 +763,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>\n>                         int32_t idx = ctrl.second.get<int32_t>();\n>                         if (ConstraintModeTable.count(idx)) {\n> -                               agc->setConstraintMode(ConstraintModeTable.at(idx));\n> +                               agc->setConstraintMode(0, ConstraintModeTable.at(idx));\n>                                 libcameraMetadata_.set(controls::AeConstraintMode, idx);\n>                         } else {\n>                                 LOG(IPARPI, Error) << \"Constraint mode \" << idx\n> @@ -783,7 +783,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>\n>                         int32_t idx = ctrl.second.get<int32_t>();\n>                         if (ExposureModeTable.count(idx)) {\n> -                               agc->setExposureMode(ExposureModeTable.at(idx));\n> +                               agc->setExposureMode(0, ExposureModeTable.at(idx));\n>                                 libcameraMetadata_.set(controls::AeExposureMode, idx);\n>                         } else {\n>                                 LOG(IPARPI, Error) << \"Exposure mode \" << idx\n> @@ -806,7 +806,7 @@ void IpaBase::applyControls(const ControlList &controls)\n>                          * So convert to 2^EV\n>                          */\n>                         double ev = pow(2.0, ctrl.second.get<float>());\n> -                       agc->setEv(ev);\n> +                       agc->setEv(0, ev);\n>                         libcameraMetadata_.set(controls::ExposureValue,\n>                                                ctrl.second.get<float>());\n>                         break;\n> diff --git a/src/ipa/rpi/controller/agc_algorithm.h b/src/ipa/rpi/controller/agc_algorithm.h\n> index b6949daa..b8986560 100644\n> --- a/src/ipa/rpi/controller/agc_algorithm.h\n> +++ b/src/ipa/rpi/controller/agc_algorithm.h\n> @@ -21,16 +21,19 @@ public:\n>         /* An AGC algorithm must provide the following: */\n>         virtual unsigned int getConvergenceFrames() const = 0;\n>         virtual std::vector<double> const &getWeights() const = 0;\n> -       virtual void setEv(double ev) = 0;\n> -       virtual void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) = 0;\n> -       virtual void setFixedShutter(libcamera::utils::Duration fixedShutter) = 0;\n> +       virtual void setEv(unsigned int channel, double ev) = 0;\n> +       virtual void setFlickerPeriod(unsigned int channel,\n> +                                     libcamera::utils::Duration flickerPeriod) = 0;\n> +       virtual void setFixedShutter(unsigned int channel,\n> +                                    libcamera::utils::Duration fixedShutter) = 0;\n>         virtual void setMaxShutter(libcamera::utils::Duration maxShutter) = 0;\n> -       virtual void setFixedAnalogueGain(double fixedAnalogueGain) = 0;\n> +       virtual void setFixedAnalogueGain(unsigned int channel, double fixedAnalogueGain) = 0;\n>         virtual void setMeteringMode(std::string const &meteringModeName) = 0;\n> -       virtual void setExposureMode(std::string const &exposureModeName) = 0;\n> -       virtual void setConstraintMode(std::string const &contraintModeName) = 0;\n> -       virtual void enableAuto() = 0;\n> -       virtual void disableAuto() = 0;\n> +       virtual void setExposureMode(unsigned int channel, std::string const &exposureModeName) = 0;\n> +       virtual void setConstraintMode(unsigned int channel, std::string const &contraintModeName) = 0;\n> +       virtual void enableAuto(unsigned int channel) = 0;\n> +       virtual void disableAuto(unsigned int channel) = 0;\n> +       virtual void setActiveChannels(const std::vector<unsigned int> &activeChannels) = 0;\n>  };\n>\n>  } /* namespace RPiController */\n> diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build\n> index feb0334e..20b9cda9 100644\n> --- a/src/ipa/rpi/controller/meson.build\n> +++ b/src/ipa/rpi/controller/meson.build\n> @@ -8,6 +8,7 @@ rpi_ipa_controller_sources = files([\n>      'pwl.cpp',\n>      'rpi/af.cpp',\n>      'rpi/agc.cpp',\n> +    'rpi/agc_channel.cpp',\n>      'rpi/alsc.cpp',\n>      'rpi/awb.cpp',\n>      'rpi/black_level.cpp',\n> diff --git a/src/ipa/rpi/controller/rpi/agc.cpp b/src/ipa/rpi/controller/rpi/agc.cpp\n> index 7b02972a..c9c9c297 100644\n> --- a/src/ipa/rpi/controller/rpi/agc.cpp\n> +++ b/src/ipa/rpi/controller/rpi/agc.cpp\n> @@ -5,20 +5,12 @@\n>   * agc.cpp - AGC/AEC control algorithm\n>   */\n>\n> -#include <algorithm>\n> -#include <map>\n> -#include <tuple>\n> +#include \"agc.h\"\n>\n>  #include <libcamera/base/log.h>\n>\n> -#include \"../awb_status.h\"\n> -#include \"../device_status.h\"\n> -#include \"../histogram.h\"\n> -#include \"../lux_status.h\"\n>  #include \"../metadata.h\"\n>\n> -#include \"agc.h\"\n> -\n>  using namespace RPiController;\n>  using namespace libcamera;\n>  using libcamera::utils::Duration;\n> @@ -28,881 +20,203 @@ LOG_DEFINE_CATEGORY(RPiAgc)\n>\n>  #define NAME \"rpi.agc\"\n>\n> -int AgcMeteringMode::read(const libcamera::YamlObject &params)\n> +Agc::Agc(Controller *controller)\n> +       : AgcAlgorithm(controller),\n> +         activeChannels_({ 0 })\n>  {\n> -       const YamlObject &yamlWeights = params[\"weights\"];\n> -\n> -       for (const auto &p : yamlWeights.asList()) {\n> -               auto value = p.get<double>();\n> -               if (!value)\n> -                       return -EINVAL;\n> -               weights.push_back(*value);\n> -       }\n> -\n> -       return 0;\n>  }\n>\n> -static std::tuple<int, std::string>\n> -readMeteringModes(std::map<std::string, AgcMeteringMode> &metering_modes,\n> -                 const libcamera::YamlObject &params)\n> +char const *Agc::name() const\n>  {\n> -       std::string first;\n> -       int ret;\n> -\n> -       for (const auto &[key, value] : params.asDict()) {\n> -               AgcMeteringMode meteringMode;\n> -               ret = meteringMode.read(value);\n> -               if (ret)\n> -                       return { ret, {} };\n> -\n> -               metering_modes[key] = std::move(meteringMode);\n> -               if (first.empty())\n> -                       first = key;\n> -       }\n> -\n> -       return { 0, first };\n> +       return NAME;\n>  }\n>\n> -int AgcExposureMode::read(const libcamera::YamlObject &params)\n> +int Agc::read(const libcamera::YamlObject &params)\n>  {\n> -       auto value = params[\"shutter\"].getList<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       std::transform(value->begin(), value->end(), std::back_inserter(shutter),\n> -                      [](double v) { return v * 1us; });\n> -\n> -       value = params[\"gain\"].getList<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       gain = std::move(*value);\n> -\n> -       if (shutter.size() < 2 || gain.size() < 2) {\n> -               LOG(RPiAgc, Error)\n> -                       << \"AgcExposureMode: must have at least two entries in exposure profile\";\n> -               return -EINVAL;\n> -       }\n> -\n> -       if (shutter.size() != gain.size()) {\n> -               LOG(RPiAgc, Error)\n> -                       << \"AgcExposureMode: expect same number of exposure and gain entries in exposure profile\";\n> -               return -EINVAL;\n> +       /*\n> +        * When there is only a single channel we can read the old style syntax.\n> +        * Otherwise we expect a \"channels\" keyword followed by a list of configurations.\n> +        */\n> +       if (!params.contains(\"channels\")) {\n> +               LOG(RPiAgc, Debug) << \"Single channel only\";\n> +               channelData_.emplace_back();\n> +               return channelData_.back().channel.read(params, getHardwareConfig());\n>         }\n>\n> -       return 0;\n> -}\n> -\n> -static std::tuple<int, std::string>\n> -readExposureModes(std::map<std::string, AgcExposureMode> &exposureModes,\n> -                 const libcamera::YamlObject &params)\n> -{\n> -       std::string first;\n> -       int ret;\n> -\n> -       for (const auto &[key, value] : params.asDict()) {\n> -               AgcExposureMode exposureMode;\n> -               ret = exposureMode.read(value);\n> +       const auto &channels = params[\"channels\"].asList();\n> +       for (auto ch = channels.begin(); ch != channels.end(); ch++) {\n> +               LOG(RPiAgc, Debug) << \"Read AGC channel\";\n> +               channelData_.emplace_back();\n> +               int ret = channelData_.back().channel.read(*ch, getHardwareConfig());\n>                 if (ret)\n> -                       return { ret, {} };\n> -\n> -               exposureModes[key] = std::move(exposureMode);\n> -               if (first.empty())\n> -                       first = key;\n> +                       return ret;\n>         }\n>\n> -       return { 0, first };\n> -}\n> -\n> -int AgcConstraint::read(const libcamera::YamlObject &params)\n> -{\n> -       std::string boundString = params[\"bound\"].get<std::string>(\"\");\n> -       transform(boundString.begin(), boundString.end(),\n> -                 boundString.begin(), ::toupper);\n> -       if (boundString != \"UPPER\" && boundString != \"LOWER\") {\n> -               LOG(RPiAgc, Error) << \"AGC constraint type should be UPPER or LOWER\";\n> -               return -EINVAL;\n> +       LOG(RPiAgc, Debug) << \"Read \" << channelData_.size() << \" channel(s)\";\n> +       if (channelData_.empty()) {\n> +               LOG(RPiAgc, Error) << \"No AGC channels provided\";\n> +               return -1;\n>         }\n> -       bound = boundString == \"UPPER\" ? Bound::UPPER : Bound::LOWER;\n> -\n> -       auto value = params[\"q_lo\"].get<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       qLo = *value;\n> -\n> -       value = params[\"q_hi\"].get<double>();\n> -       if (!value)\n> -               return -EINVAL;\n> -       qHi = *value;\n> -\n> -       return yTarget.read(params[\"y_target\"]);\n> -}\n>\n> -static std::tuple<int, AgcConstraintMode>\n> -readConstraintMode(const libcamera::YamlObject &params)\n> -{\n> -       AgcConstraintMode mode;\n> -       int ret;\n> -\n> -       for (const auto &p : params.asList()) {\n> -               AgcConstraint constraint;\n> -               ret = constraint.read(p);\n> -               if (ret)\n> -                       return { ret, {} };\n> -\n> -               mode.push_back(std::move(constraint));\n> -       }\n> -\n> -       return { 0, mode };\n> +       return 0;\n>  }\n>\n> -static std::tuple<int, std::string>\n> -readConstraintModes(std::map<std::string, AgcConstraintMode> &constraintModes,\n> -                   const libcamera::YamlObject &params)\n> +int Agc::checkChannel(unsigned int channelIndex) const\n>  {\n> -       std::string first;\n> -       int ret;\n> -\n> -       for (const auto &[key, value] : params.asDict()) {\n> -               std::tie(ret, constraintModes[key]) = readConstraintMode(value);\n> -               if (ret)\n> -                       return { ret, {} };\n> -\n> -               if (first.empty())\n> -                       first = key;\n> +       if (channelIndex >= channelData_.size()) {\n> +               LOG(RPiAgc, Warning) << \"AGC channel \" << channelIndex << \" not available\";\n> +               return -1;\n>         }\n>\n> -       return { 0, first };\n> -}\n> -\n> -int AgcConfig::read(const libcamera::YamlObject &params)\n> -{\n> -       LOG(RPiAgc, Debug) << \"AgcConfig\";\n> -       int ret;\n> -\n> -       std::tie(ret, defaultMeteringMode) =\n> -               readMeteringModes(meteringModes, params[\"metering_modes\"]);\n> -       if (ret)\n> -               return ret;\n> -       std::tie(ret, defaultExposureMode) =\n> -               readExposureModes(exposureModes, params[\"exposure_modes\"]);\n> -       if (ret)\n> -               return ret;\n> -       std::tie(ret, defaultConstraintMode) =\n> -               readConstraintModes(constraintModes, params[\"constraint_modes\"]);\n> -       if (ret)\n> -               return ret;\n> -\n> -       ret = yTarget.read(params[\"y_target\"]);\n> -       if (ret)\n> -               return ret;\n> -\n> -       speed = params[\"speed\"].get<double>(0.2);\n> -       startupFrames = params[\"startup_frames\"].get<uint16_t>(10);\n> -       convergenceFrames = params[\"convergence_frames\"].get<unsigned int>(6);\n> -       fastReduceThreshold = params[\"fast_reduce_threshold\"].get<double>(0.4);\n> -       baseEv = params[\"base_ev\"].get<double>(1.0);\n> -\n> -       /* Start with quite a low value as ramping up is easier than ramping down. */\n> -       defaultExposureTime = params[\"default_exposure_time\"].get<double>(1000) * 1us;\n> -       defaultAnalogueGain = params[\"default_analogue_gain\"].get<double>(1.0);\n> -\n>         return 0;\n>  }\n>\n> -Agc::ExposureValues::ExposureValues()\n> -       : shutter(0s), analogueGain(0),\n> -         totalExposure(0s), totalExposureNoDG(0s)\n> +void Agc::disableAuto(unsigned int channelIndex)\n>  {\n> -}\n> -\n> -Agc::Agc(Controller *controller)\n> -       : AgcAlgorithm(controller), meteringMode_(nullptr),\n> -         exposureMode_(nullptr), constraintMode_(nullptr),\n> -         frameCount_(0), lockCount_(0),\n> -         lastTargetExposure_(0s), ev_(1.0), flickerPeriod_(0s),\n> -         maxShutter_(0s), fixedShutter_(0s), fixedAnalogueGain_(0.0)\n> -{\n> -       memset(&awb_, 0, sizeof(awb_));\n> -       /*\n> -        * Setting status_.totalExposureValue_ to zero initially tells us\n> -        * it's not been calculated yet (i.e. Process hasn't yet run).\n> -        */\n> -       status_ = {};\n> -       status_.ev = ev_;\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -char const *Agc::name() const\n> -{\n> -       return NAME;\n> +       LOG(RPiAgc, Debug) << \"disableAuto for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.disableAuto();\n>  }\n>\n> -int Agc::read(const libcamera::YamlObject &params)\n> +void Agc::enableAuto(unsigned int channelIndex)\n>  {\n> -       LOG(RPiAgc, Debug) << \"Agc\";\n> -\n> -       int ret = config_.read(params);\n> -       if (ret)\n> -               return ret;\n> -\n> -       const Size &size = getHardwareConfig().agcZoneWeights;\n> -       for (auto const &modes : config_.meteringModes) {\n> -               if (modes.second.weights.size() != size.width * size.height) {\n> -                       LOG(RPiAgc, Error) << \"AgcMeteringMode: Incorrect number of weights\";\n> -                       return -EINVAL;\n> -               }\n> -       }\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -       /*\n> -        * Set the config's defaults (which are the first ones it read) as our\n> -        * current modes, until someone changes them.  (they're all known to\n> -        * exist at this point)\n> -        */\n> -       meteringModeName_ = config_.defaultMeteringMode;\n> -       meteringMode_ = &config_.meteringModes[meteringModeName_];\n> -       exposureModeName_ = config_.defaultExposureMode;\n> -       exposureMode_ = &config_.exposureModes[exposureModeName_];\n> -       constraintModeName_ = config_.defaultConstraintMode;\n> -       constraintMode_ = &config_.constraintModes[constraintModeName_];\n> -       /* Set up the \"last shutter/gain\" values, in case AGC starts \"disabled\". */\n> -       status_.shutterTime = config_.defaultExposureTime;\n> -       status_.analogueGain = config_.defaultAnalogueGain;\n> -       return 0;\n> -}\n> -\n> -void Agc::disableAuto()\n> -{\n> -       fixedShutter_ = status_.shutterTime;\n> -       fixedAnalogueGain_ = status_.analogueGain;\n> -}\n> -\n> -void Agc::enableAuto()\n> -{\n> -       fixedShutter_ = 0s;\n> -       fixedAnalogueGain_ = 0;\n> +       LOG(RPiAgc, Debug) << \"enableAuto for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.enableAuto();\n>  }\n>\n>  unsigned int Agc::getConvergenceFrames() const\n>  {\n> -       /*\n> -        * If shutter and gain have been explicitly set, there is no\n> -        * convergence to happen, so no need to drop any frames - return zero.\n> -        */\n> -       if (fixedShutter_ && fixedAnalogueGain_)\n> -               return 0;\n> -       else\n> -               return config_.convergenceFrames;\n> +       /* If there are n channels, it presumably takes n times as long to converge. */\n> +       return channelData_[0].channel.getConvergenceFrames() * activeChannels_.size();\n>  }\n>\n>  std::vector<double> const &Agc::getWeights() const\n>  {\n>         /*\n> -        * In case someone calls setMeteringMode and then this before the\n> -        * algorithm has run and updated the meteringMode_ pointer.\n> +        * A limitation is that we're going to have to use the same weights across\n> +        * all channels.\n>          */\n> -       auto it = config_.meteringModes.find(meteringModeName_);\n> -       if (it == config_.meteringModes.end())\n> -               return meteringMode_->weights;\n> -       return it->second.weights;\n> +       return channelData_[0].channel.getWeights();\n>  }\n>\n> -void Agc::setEv(double ev)\n> +void Agc::setEv(unsigned int channelIndex, double ev)\n>  {\n> -       ev_ = ev;\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -void Agc::setFlickerPeriod(Duration flickerPeriod)\n> -{\n> -       flickerPeriod_ = flickerPeriod;\n> +       LOG(RPiAgc, Debug) << \"setEv \" << ev << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setEv(ev);\n>  }\n>\n> -void Agc::setMaxShutter(Duration maxShutter)\n> +void Agc::setFlickerPeriod(unsigned int channelIndex, Duration flickerPeriod)\n>  {\n> -       maxShutter_ = maxShutter;\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -void Agc::setFixedShutter(Duration fixedShutter)\n> -{\n> -       fixedShutter_ = fixedShutter;\n> -       /* Set this in case someone calls disableAuto() straight after. */\n> -       status_.shutterTime = limitShutter(fixedShutter_);\n> +       LOG(RPiAgc, Debug) << \"setFlickerPeriod \" << flickerPeriod\n> +                          << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setFlickerPeriod(flickerPeriod);\n>  }\n>\n> -void Agc::setFixedAnalogueGain(double fixedAnalogueGain)\n> -{\n> -       fixedAnalogueGain_ = fixedAnalogueGain;\n> -       /* Set this in case someone calls disableAuto() straight after. */\n> -       status_.analogueGain = limitGain(fixedAnalogueGain);\n> -}\n> -\n> -void Agc::setMeteringMode(std::string const &meteringModeName)\n> -{\n> -       meteringModeName_ = meteringModeName;\n> -}\n> -\n> -void Agc::setExposureMode(std::string const &exposureModeName)\n> -{\n> -       exposureModeName_ = exposureModeName;\n> -}\n> -\n> -void Agc::setConstraintMode(std::string const &constraintModeName)\n> -{\n> -       constraintModeName_ = constraintModeName;\n> -}\n> -\n> -void Agc::switchMode(CameraMode const &cameraMode,\n> -                    Metadata *metadata)\n> +void Agc::setMaxShutter(Duration maxShutter)\n>  {\n> -       /* AGC expects the mode sensitivity always to be non-zero. */\n> -       ASSERT(cameraMode.sensitivity);\n> -\n> -       housekeepConfig();\n> -\n> -       /*\n> -        * Store the mode in the local state. We must cache the sensitivity of\n> -        * of the previous mode for the calculations below.\n> -        */\n> -       double lastSensitivity = mode_.sensitivity;\n> -       mode_ = cameraMode;\n> -\n> -       Duration fixedShutter = limitShutter(fixedShutter_);\n> -       if (fixedShutter && fixedAnalogueGain_) {\n> -               /* We're going to reset the algorithm here with these fixed values. */\n> -\n> -               fetchAwbStatus(metadata);\n> -               double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> -               ASSERT(minColourGain != 0.0);\n> -\n> -               /* This is the equivalent of computeTargetExposure and applyDigitalGain. */\n> -               target_.totalExposureNoDG = fixedShutter_ * fixedAnalogueGain_;\n> -               target_.totalExposure = target_.totalExposureNoDG / minColourGain;\n> -\n> -               /* Equivalent of filterExposure. This resets any \"history\". */\n> -               filtered_ = target_;\n> -\n> -               /* Equivalent of divideUpExposure. */\n> -               filtered_.shutter = fixedShutter;\n> -               filtered_.analogueGain = fixedAnalogueGain_;\n> -       } else if (status_.totalExposureValue) {\n> -               /*\n> -                * On a mode switch, various things could happen:\n> -                * - the exposure profile might change\n> -                * - a fixed exposure or gain might be set\n> -                * - the new mode's sensitivity might be different\n> -                * We cope with the last of these by scaling the target values. After\n> -                * that we just need to re-divide the exposure/gain according to the\n> -                * current exposure profile, which takes care of everything else.\n> -                */\n> -\n> -               double ratio = lastSensitivity / cameraMode.sensitivity;\n> -               target_.totalExposureNoDG *= ratio;\n> -               target_.totalExposure *= ratio;\n> -               filtered_.totalExposureNoDG *= ratio;\n> -               filtered_.totalExposure *= ratio;\n> -\n> -               divideUpExposure();\n> -       } else {\n> -               /*\n> -                * We come through here on startup, when at least one of the shutter\n> -                * or gain has not been fixed. We must still write those values out so\n> -                * that they will be applied immediately. We supply some arbitrary defaults\n> -                * for any that weren't set.\n> -                */\n> -\n> -               /* Equivalent of divideUpExposure. */\n> -               filtered_.shutter = fixedShutter ? fixedShutter : config_.defaultExposureTime;\n> -               filtered_.analogueGain = fixedAnalogueGain_ ? fixedAnalogueGain_ : config_.defaultAnalogueGain;\n> -       }\n> -\n> -       writeAndFinish(metadata, false);\n> +       /* Frame durations will be the same across all channels too. */\n> +       for (auto &data : channelData_)\n> +               data.channel.setMaxShutter(maxShutter);\n>  }\n>\n> -void Agc::prepare(Metadata *imageMetadata)\n> +void Agc::setFixedShutter(unsigned int channelIndex, Duration fixedShutter)\n>  {\n> -       Duration totalExposureValue = status_.totalExposureValue;\n> -       AgcStatus delayedStatus;\n> -       AgcPrepareStatus prepareStatus;\n> -\n> -       if (!imageMetadata->get(\"agc.delayed_status\", delayedStatus))\n> -               totalExposureValue = delayedStatus.totalExposureValue;\n> -\n> -       prepareStatus.digitalGain = 1.0;\n> -       prepareStatus.locked = false;\n> -\n> -       if (status_.totalExposureValue) {\n> -               /* Process has run, so we have meaningful values. */\n> -               DeviceStatus deviceStatus;\n> -               if (imageMetadata->get(\"device.status\", deviceStatus) == 0) {\n> -                       Duration actualExposure = deviceStatus.shutterSpeed *\n> -                                                 deviceStatus.analogueGain;\n> -                       if (actualExposure) {\n> -                               double digitalGain = totalExposureValue / actualExposure;\n> -                               LOG(RPiAgc, Debug) << \"Want total exposure \" << totalExposureValue;\n> -                               /*\n> -                                * Never ask for a gain < 1.0, and also impose\n> -                                * some upper limit. Make it customisable?\n> -                                */\n> -                               prepareStatus.digitalGain = std::max(1.0, std::min(digitalGain, 4.0));\n> -                               LOG(RPiAgc, Debug) << \"Actual exposure \" << actualExposure;\n> -                               LOG(RPiAgc, Debug) << \"Use digitalGain \" << prepareStatus.digitalGain;\n> -                               LOG(RPiAgc, Debug) << \"Effective exposure \"\n> -                                                  << actualExposure * prepareStatus.digitalGain;\n> -                               /* Decide whether AEC/AGC has converged. */\n> -                               prepareStatus.locked = updateLockStatus(deviceStatus);\n> -                       }\n> -               } else\n> -                       LOG(RPiAgc, Warning) << name() << \": no device metadata\";\n> -               imageMetadata->set(\"agc.prepare_status\", prepareStatus);\n> -       }\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -void Agc::process(StatisticsPtr &stats, Metadata *imageMetadata)\n> -{\n> -       frameCount_++;\n> -       /*\n> -        * First a little bit of housekeeping, fetching up-to-date settings and\n> -        * configuration, that kind of thing.\n> -        */\n> -       housekeepConfig();\n> -       /* Fetch the AWB status immediately, so that we can assume it's there. */\n> -       fetchAwbStatus(imageMetadata);\n> -       /* Get the current exposure values for the frame that's just arrived. */\n> -       fetchCurrentExposure(imageMetadata);\n> -       /* Compute the total gain we require relative to the current exposure. */\n> -       double gain, targetY;\n> -       computeGain(stats, imageMetadata, gain, targetY);\n> -       /* Now compute the target (final) exposure which we think we want. */\n> -       computeTargetExposure(gain);\n> -       /* The results have to be filtered so as not to change too rapidly. */\n> -       filterExposure();\n> -       /*\n> -        * Some of the exposure has to be applied as digital gain, so work out\n> -        * what that is. This function also tells us whether it's decided to\n> -        * \"desaturate\" the image more quickly.\n> -        */\n> -       bool desaturate = applyDigitalGain(gain, targetY);\n> -       /*\n> -        * The last thing is to divide up the exposure value into a shutter time\n> -        * and analogue gain, according to the current exposure mode.\n> -        */\n> -       divideUpExposure();\n> -       /* Finally advertise what we've done. */\n> -       writeAndFinish(imageMetadata, desaturate);\n> +       LOG(RPiAgc, Debug) << \"setFixedShutter \" << fixedShutter\n> +                          << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setFixedShutter(fixedShutter);\n>  }\n>\n> -bool Agc::updateLockStatus(DeviceStatus const &deviceStatus)\n> +void Agc::setFixedAnalogueGain(unsigned int channelIndex, double fixedAnalogueGain)\n>  {\n> -       const double errorFactor = 0.10; /* make these customisable? */\n> -       const int maxLockCount = 5;\n> -       /* Reset \"lock count\" when we exceed this multiple of errorFactor */\n> -       const double resetMargin = 1.5;\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -       /* Add 200us to the exposure time error to allow for line quantisation. */\n> -       Duration exposureError = lastDeviceStatus_.shutterSpeed * errorFactor + 200us;\n> -       double gainError = lastDeviceStatus_.analogueGain * errorFactor;\n> -       Duration targetError = lastTargetExposure_ * errorFactor;\n> -\n> -       /*\n> -        * Note that we don't know the exposure/gain limits of the sensor, so\n> -        * the values we keep requesting may be unachievable. For this reason\n> -        * we only insist that we're close to values in the past few frames.\n> -        */\n> -       if (deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed - exposureError &&\n> -           deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed + exposureError &&\n> -           deviceStatus.analogueGain > lastDeviceStatus_.analogueGain - gainError &&\n> -           deviceStatus.analogueGain < lastDeviceStatus_.analogueGain + gainError &&\n> -           status_.targetExposureValue > lastTargetExposure_ - targetError &&\n> -           status_.targetExposureValue < lastTargetExposure_ + targetError)\n> -               lockCount_ = std::min(lockCount_ + 1, maxLockCount);\n> -       else if (deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed - resetMargin * exposureError ||\n> -                deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed + resetMargin * exposureError ||\n> -                deviceStatus.analogueGain < lastDeviceStatus_.analogueGain - resetMargin * gainError ||\n> -                deviceStatus.analogueGain > lastDeviceStatus_.analogueGain + resetMargin * gainError ||\n> -                status_.targetExposureValue < lastTargetExposure_ - resetMargin * targetError ||\n> -                status_.targetExposureValue > lastTargetExposure_ + resetMargin * targetError)\n> -               lockCount_ = 0;\n> -\n> -       lastDeviceStatus_ = deviceStatus;\n> -       lastTargetExposure_ = status_.targetExposureValue;\n> -\n> -       LOG(RPiAgc, Debug) << \"Lock count updated to \" << lockCount_;\n> -       return lockCount_ == maxLockCount;\n> +       LOG(RPiAgc, Debug) << \"setFixedAnalogueGain \" << fixedAnalogueGain\n> +                          << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setFixedAnalogueGain(fixedAnalogueGain);\n>  }\n>\n> -void Agc::housekeepConfig()\n> +void Agc::setMeteringMode(std::string const &meteringModeName)\n>  {\n> -       /* First fetch all the up-to-date settings, so no one else has to do it. */\n> -       status_.ev = ev_;\n> -       status_.fixedShutter = limitShutter(fixedShutter_);\n> -       status_.fixedAnalogueGain = fixedAnalogueGain_;\n> -       status_.flickerPeriod = flickerPeriod_;\n> -       LOG(RPiAgc, Debug) << \"ev \" << status_.ev << \" fixedShutter \"\n> -                          << status_.fixedShutter << \" fixedAnalogueGain \"\n> -                          << status_.fixedAnalogueGain;\n> -       /*\n> -        * Make sure the \"mode\" pointers point to the up-to-date things, if\n> -        * they've changed.\n> -        */\n> -       if (meteringModeName_ != status_.meteringMode) {\n> -               auto it = config_.meteringModes.find(meteringModeName_);\n> -               if (it == config_.meteringModes.end()) {\n> -                       LOG(RPiAgc, Warning) << \"No metering mode \" << meteringModeName_;\n> -                       meteringModeName_ = status_.meteringMode;\n> -               } else {\n> -                       meteringMode_ = &it->second;\n> -                       status_.meteringMode = meteringModeName_;\n> -               }\n> -       }\n> -       if (exposureModeName_ != status_.exposureMode) {\n> -               auto it = config_.exposureModes.find(exposureModeName_);\n> -               if (it == config_.exposureModes.end()) {\n> -                       LOG(RPiAgc, Warning) << \"No exposure profile \" << exposureModeName_;\n> -                       exposureModeName_ = status_.exposureMode;\n> -               } else {\n> -                       exposureMode_ = &it->second;\n> -                       status_.exposureMode = exposureModeName_;\n> -               }\n> -       }\n> -       if (constraintModeName_ != status_.constraintMode) {\n> -               auto it = config_.constraintModes.find(constraintModeName_);\n> -               if (it == config_.constraintModes.end()) {\n> -                       LOG(RPiAgc, Warning) << \"No constraint list \" << constraintModeName_;\n> -                       constraintModeName_ = status_.constraintMode;\n> -               } else {\n> -                       constraintMode_ = &it->second;\n> -                       status_.constraintMode = constraintModeName_;\n> -               }\n> -       }\n> -       LOG(RPiAgc, Debug) << \"exposureMode \"\n> -                          << exposureModeName_ << \" constraintMode \"\n> -                          << constraintModeName_ << \" meteringMode \"\n> -                          << meteringModeName_;\n> +       /* Metering modes will be the same across all channels too. */\n> +       for (auto &data : channelData_)\n> +               data.channel.setMeteringMode(meteringModeName);\n>  }\n>\n> -void Agc::fetchCurrentExposure(Metadata *imageMetadata)\n> +void Agc::setExposureMode(unsigned int channelIndex, std::string const &exposureModeName)\n>  {\n> -       std::unique_lock<Metadata> lock(*imageMetadata);\n> -       DeviceStatus *deviceStatus =\n> -               imageMetadata->getLocked<DeviceStatus>(\"device.status\");\n> -       if (!deviceStatus)\n> -               LOG(RPiAgc, Fatal) << \"No device metadata\";\n> -       current_.shutter = deviceStatus->shutterSpeed;\n> -       current_.analogueGain = deviceStatus->analogueGain;\n> -       AgcStatus *agcStatus =\n> -               imageMetadata->getLocked<AgcStatus>(\"agc.status\");\n> -       current_.totalExposure = agcStatus ? agcStatus->totalExposureValue : 0s;\n> -       current_.totalExposureNoDG = current_.shutter * current_.analogueGain;\n> -}\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -void Agc::fetchAwbStatus(Metadata *imageMetadata)\n> -{\n> -       awb_.gainR = 1.0; /* in case not found in metadata */\n> -       awb_.gainG = 1.0;\n> -       awb_.gainB = 1.0;\n> -       if (imageMetadata->get(\"awb.status\", awb_) != 0)\n> -               LOG(RPiAgc, Debug) << \"No AWB status found\";\n> +       LOG(RPiAgc, Debug) << \"setExposureMode \" << exposureModeName\n> +                          << \" for channel \" << channelIndex;\n> +       channelData_[channelIndex].channel.setExposureMode(exposureModeName);\n>  }\n>\n> -static double computeInitialY(StatisticsPtr &stats, AwbStatus const &awb,\n> -                             std::vector<double> &weights, double gain)\n> +void Agc::setConstraintMode(unsigned int channelIndex, std::string const &constraintModeName)\n>  {\n> -       constexpr uint64_t maxVal = 1 << Statistics::NormalisationFactorPow2;\n> +       if (checkChannel(channelIndex))\n> +               return;\n>\n> -       ASSERT(weights.size() == stats->agcRegions.numRegions());\n> -\n> -       /*\n> -        * Note that the weights are applied by the IPA to the statistics directly,\n> -        * before they are given to us here.\n> -        */\n> -       double rSum = 0, gSum = 0, bSum = 0, pixelSum = 0;\n> -       for (unsigned int i = 0; i < stats->agcRegions.numRegions(); i++) {\n> -               auto &region = stats->agcRegions.get(i);\n> -               rSum += std::min<double>(region.val.rSum * gain, (maxVal - 1) * region.counted);\n> -               gSum += std::min<double>(region.val.gSum * gain, (maxVal - 1) * region.counted);\n> -               bSum += std::min<double>(region.val.bSum * gain, (maxVal - 1) * region.counted);\n> -               pixelSum += region.counted;\n> -       }\n> -       if (pixelSum == 0.0) {\n> -               LOG(RPiAgc, Warning) << \"computeInitialY: pixelSum is zero\";\n> -               return 0;\n> -       }\n> -       double ySum = rSum * awb.gainR * .299 +\n> -                     gSum * awb.gainG * .587 +\n> -                     bSum * awb.gainB * .114;\n> -       return ySum / pixelSum / maxVal;\n> +       channelData_[channelIndex].channel.setConstraintMode(constraintModeName);\n>  }\n>\n> -/*\n> - * We handle extra gain through EV by adjusting our Y targets. However, you\n> - * simply can't monitor histograms once they get very close to (or beyond!)\n> - * saturation, so we clamp the Y targets to this value. It does mean that EV\n> - * increases don't necessarily do quite what you might expect in certain\n> - * (contrived) cases.\n> - */\n> -\n> -static constexpr double EvGainYTargetLimit = 0.9;\n> -\n> -static double constraintComputeGain(AgcConstraint &c, const Histogram &h, double lux,\n> -                                   double evGain, double &targetY)\n> +template<typename T>\n> +std::ostream &operator<<(std::ostream &os, const std::vector<T> &v)\n>  {\n> -       targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));\n> -       targetY = std::min(EvGainYTargetLimit, targetY * evGain);\n> -       double iqm = h.interQuantileMean(c.qLo, c.qHi);\n> -       return (targetY * h.bins()) / iqm;\n> +       os << \"{\";\n> +       for (const auto &e : v)\n> +               os << \" \" << e;\n> +       os << \" }\";\n> +       return os;\n>  }\n>\n> -void Agc::computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,\n> -                     double &gain, double &targetY)\n> +void Agc::setActiveChannels(const std::vector<unsigned int> &activeChannels)\n>  {\n> -       struct LuxStatus lux = {};\n> -       lux.lux = 400; /* default lux level to 400 in case no metadata found */\n> -       if (imageMetadata->get(\"lux.status\", lux) != 0)\n> -               LOG(RPiAgc, Warning) << \"No lux level found\";\n> -       const Histogram &h = statistics->yHist;\n> -       double evGain = status_.ev * config_.baseEv;\n> -       /*\n> -        * The initial gain and target_Y come from some of the regions. After\n> -        * that we consider the histogram constraints.\n> -        */\n> -       targetY = config_.yTarget.eval(config_.yTarget.domain().clip(lux.lux));\n> -       targetY = std::min(EvGainYTargetLimit, targetY * evGain);\n> -\n> -       /*\n> -        * Do this calculation a few times as brightness increase can be\n> -        * non-linear when there are saturated regions.\n> -        */\n> -       gain = 1.0;\n> -       for (int i = 0; i < 8; i++) {\n> -               double initialY = computeInitialY(statistics, awb_, meteringMode_->weights, gain);\n> -               double extraGain = std::min(10.0, targetY / (initialY + .001));\n> -               gain *= extraGain;\n> -               LOG(RPiAgc, Debug) << \"Initial Y \" << initialY << \" target \" << targetY\n> -                                  << \" gives gain \" << gain;\n> -               if (extraGain < 1.01) /* close enough */\n> -                       break;\n> -       }\n> -\n> -       for (auto &c : *constraintMode_) {\n> -               double newTargetY;\n> -               double newGain = constraintComputeGain(c, h, lux.lux, evGain, newTargetY);\n> -               LOG(RPiAgc, Debug) << \"Constraint has target_Y \"\n> -                                  << newTargetY << \" giving gain \" << newGain;\n> -               if (c.bound == AgcConstraint::Bound::LOWER && newGain > gain) {\n> -                       LOG(RPiAgc, Debug) << \"Lower bound constraint adopted\";\n> -                       gain = newGain;\n> -                       targetY = newTargetY;\n> -               } else if (c.bound == AgcConstraint::Bound::UPPER && newGain < gain) {\n> -                       LOG(RPiAgc, Debug) << \"Upper bound constraint adopted\";\n> -                       gain = newGain;\n> -                       targetY = newTargetY;\n> -               }\n> +       if (activeChannels.empty()) {\n> +               LOG(RPiAgc, Warning) << \"No active AGC channels supplied\";\n> +               return;\n>         }\n> -       LOG(RPiAgc, Debug) << \"Final gain \" << gain << \" (target_Y \" << targetY << \" ev \"\n> -                          << status_.ev << \" base_ev \" << config_.baseEv\n> -                          << \")\";\n> -}\n> -\n> -void Agc::computeTargetExposure(double gain)\n> -{\n> -       if (status_.fixedShutter && status_.fixedAnalogueGain) {\n> -               /*\n> -                * When ag and shutter are both fixed, we need to drive the\n> -                * total exposure so that we end up with a digital gain of at least\n> -                * 1/minColourGain. Otherwise we'd desaturate channels causing\n> -                * white to go cyan or magenta.\n> -                */\n> -               double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> -               ASSERT(minColourGain != 0.0);\n> -               target_.totalExposure =\n> -                       status_.fixedShutter * status_.fixedAnalogueGain / minColourGain;\n> -       } else {\n> -               /*\n> -                * The statistics reflect the image without digital gain, so the final\n> -                * total exposure we're aiming for is:\n> -                */\n> -               target_.totalExposure = current_.totalExposureNoDG * gain;\n> -               /* The final target exposure is also limited to what the exposure mode allows. */\n> -               Duration maxShutter = status_.fixedShutter\n> -                                             ? status_.fixedShutter\n> -                                             : exposureMode_->shutter.back();\n> -               maxShutter = limitShutter(maxShutter);\n> -               Duration maxTotalExposure =\n> -                       maxShutter *\n> -                       (status_.fixedAnalogueGain != 0.0\n> -                                ? status_.fixedAnalogueGain\n> -                                : exposureMode_->gain.back());\n> -               target_.totalExposure = std::min(target_.totalExposure, maxTotalExposure);\n> -       }\n> -       LOG(RPiAgc, Debug) << \"Target totalExposure \" << target_.totalExposure;\n> -}\n>\n> -bool Agc::applyDigitalGain(double gain, double targetY)\n> -{\n> -       double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> -       ASSERT(minColourGain != 0.0);\n> -       double dg = 1.0 / minColourGain;\n> -       /*\n> -        * I think this pipeline subtracts black level and rescales before we\n> -        * get the stats, so no need to worry about it.\n> -        */\n> -       LOG(RPiAgc, Debug) << \"after AWB, target dg \" << dg << \" gain \" << gain\n> -                          << \" target_Y \" << targetY;\n> -       /*\n> -        * Finally, if we're trying to reduce exposure but the target_Y is\n> -        * \"close\" to 1.0, then the gain computed for that constraint will be\n> -        * only slightly less than one, because the measured Y can never be\n> -        * larger than 1.0. When this happens, demand a large digital gain so\n> -        * that the exposure can be reduced, de-saturating the image much more\n> -        * quickly (and we then approach the correct value more quickly from\n> -        * below).\n> -        */\n> -       bool desaturate = targetY > config_.fastReduceThreshold &&\n> -                         gain < sqrt(targetY);\n> -       if (desaturate)\n> -               dg /= config_.fastReduceThreshold;\n> -       LOG(RPiAgc, Debug) << \"Digital gain \" << dg << \" desaturate? \" << desaturate;\n> -       filtered_.totalExposureNoDG = filtered_.totalExposure / dg;\n> -       LOG(RPiAgc, Debug) << \"Target totalExposureNoDG \" << filtered_.totalExposureNoDG;\n> -       return desaturate;\n> -}\n> -\n> -void Agc::filterExposure()\n> -{\n> -       double speed = config_.speed;\n> -       /*\n> -        * AGC adapts instantly if both shutter and gain are directly specified\n> -        * or we're in the startup phase.\n> -        */\n> -       if ((status_.fixedShutter && status_.fixedAnalogueGain) ||\n> -           frameCount_ <= config_.startupFrames)\n> -               speed = 1.0;\n> -       if (!filtered_.totalExposure) {\n> -               filtered_.totalExposure = target_.totalExposure;\n> -       } else {\n> -               /*\n> -                * If close to the result go faster, to save making so many\n> -                * micro-adjustments on the way. (Make this customisable?)\n> -                */\n> -               if (filtered_.totalExposure < 1.2 * target_.totalExposure &&\n> -                   filtered_.totalExposure > 0.8 * target_.totalExposure)\n> -                       speed = sqrt(speed);\n> -               filtered_.totalExposure = speed * target_.totalExposure +\n> -                                         filtered_.totalExposure * (1.0 - speed);\n> -       }\n> -       LOG(RPiAgc, Debug) << \"After filtering, totalExposure \" << filtered_.totalExposure\n> -                          << \" no dg \" << filtered_.totalExposureNoDG;\n> -}\n> +       for (auto index : activeChannels)\n> +               if (checkChannel(index))\n> +                       return;\n>\n> -void Agc::divideUpExposure()\n> -{\n> -       /*\n> -        * Sending the fixed shutter/gain cases through the same code may seem\n> -        * unnecessary, but it will make more sense when extend this to cover\n> -        * variable aperture.\n> -        */\n> -       Duration exposureValue = filtered_.totalExposureNoDG;\n> -       Duration shutterTime;\n> -       double analogueGain;\n> -       shutterTime = status_.fixedShutter ? status_.fixedShutter\n> -                                          : exposureMode_->shutter[0];\n> -       shutterTime = limitShutter(shutterTime);\n> -       analogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain\n> -                                                       : exposureMode_->gain[0];\n> -       analogueGain = limitGain(analogueGain);\n> -       if (shutterTime * analogueGain < exposureValue) {\n> -               for (unsigned int stage = 1;\n> -                    stage < exposureMode_->gain.size(); stage++) {\n> -                       if (!status_.fixedShutter) {\n> -                               Duration stageShutter =\n> -                                       limitShutter(exposureMode_->shutter[stage]);\n> -                               if (stageShutter * analogueGain >= exposureValue) {\n> -                                       shutterTime = exposureValue / analogueGain;\n> -                                       break;\n> -                               }\n> -                               shutterTime = stageShutter;\n> -                       }\n> -                       if (status_.fixedAnalogueGain == 0.0) {\n> -                               if (exposureMode_->gain[stage] * shutterTime >= exposureValue) {\n> -                                       analogueGain = exposureValue / shutterTime;\n> -                                       break;\n> -                               }\n> -                               analogueGain = exposureMode_->gain[stage];\n> -                               analogueGain = limitGain(analogueGain);\n> -                       }\n> -               }\n> -       }\n> -       LOG(RPiAgc, Debug) << \"Divided up shutter and gain are \" << shutterTime << \" and \"\n> -                          << analogueGain;\n> -       /*\n> -        * Finally adjust shutter time for flicker avoidance (require both\n> -        * shutter and gain not to be fixed).\n> -        */\n> -       if (!status_.fixedShutter && !status_.fixedAnalogueGain &&\n> -           status_.flickerPeriod) {\n> -               int flickerPeriods = shutterTime / status_.flickerPeriod;\n> -               if (flickerPeriods) {\n> -                       Duration newShutterTime = flickerPeriods * status_.flickerPeriod;\n> -                       analogueGain *= shutterTime / newShutterTime;\n> -                       /*\n> -                        * We should still not allow the ag to go over the\n> -                        * largest value in the exposure mode. Note that this\n> -                        * may force more of the total exposure into the digital\n> -                        * gain as a side-effect.\n> -                        */\n> -                       analogueGain = std::min(analogueGain, exposureMode_->gain.back());\n> -                       analogueGain = limitGain(analogueGain);\n> -                       shutterTime = newShutterTime;\n> -               }\n> -               LOG(RPiAgc, Debug) << \"After flicker avoidance, shutter \"\n> -                                  << shutterTime << \" gain \" << analogueGain;\n> -       }\n> -       filtered_.shutter = shutterTime;\n> -       filtered_.analogueGain = analogueGain;\n> +       LOG(RPiAgc, Debug) << \"setActiveChannels \" << activeChannels;\n> +       activeChannels_ = activeChannels;\n>  }\n>\n> -void Agc::writeAndFinish(Metadata *imageMetadata, bool desaturate)\n> +void Agc::switchMode(CameraMode const &cameraMode,\n> +                    Metadata *metadata)\n>  {\n> -       status_.totalExposureValue = filtered_.totalExposure;\n> -       status_.targetExposureValue = desaturate ? 0s : target_.totalExposureNoDG;\n> -       status_.shutterTime = filtered_.shutter;\n> -       status_.analogueGain = filtered_.analogueGain;\n> -       /*\n> -        * Write to metadata as well, in case anyone wants to update the camera\n> -        * immediately.\n> -        */\n> -       imageMetadata->set(\"agc.status\", status_);\n> -       LOG(RPiAgc, Debug) << \"Output written, total exposure requested is \"\n> -                          << filtered_.totalExposure;\n> -       LOG(RPiAgc, Debug) << \"Camera exposure update: shutter time \" << filtered_.shutter\n> -                          << \" analogue gain \" << filtered_.analogueGain;\n> +       LOG(RPiAgc, Debug) << \"switchMode for channel 0\";\n> +       channelData_[0].channel.switchMode(cameraMode, metadata);\n>  }\n>\n> -Duration Agc::limitShutter(Duration shutter)\n> +void Agc::prepare(Metadata *imageMetadata)\n>  {\n> -       /*\n> -        * shutter == 0 is a special case for fixed shutter values, and must pass\n> -        * through unchanged\n> -        */\n> -       if (!shutter)\n> -               return shutter;\n> -\n> -       shutter = std::clamp(shutter, mode_.minShutter, maxShutter_);\n> -       return shutter;\n> +       LOG(RPiAgc, Debug) << \"prepare for channel 0\";\n> +       channelData_[0].channel.prepare(imageMetadata);\n>  }\n>\n> -double Agc::limitGain(double gain) const\n> +void Agc::process(StatisticsPtr &stats, Metadata *imageMetadata)\n>  {\n> -       /*\n> -        * Only limit the lower bounds of the gain value to what the sensor limits.\n> -        * The upper bound on analogue gain will be made up with additional digital\n> -        * gain applied by the ISP.\n> -        *\n> -        * gain == 0.0 is a special case for fixed shutter values, and must pass\n> -        * through unchanged\n> -        */\n> -       if (!gain)\n> -               return gain;\n> -\n> -       gain = std::max(gain, mode_.minAnalogueGain);\n> -       return gain;\n> +       LOG(RPiAgc, Debug) << \"process for channel 0\";\n> +       channelData_[0].channel.process(stats, imageMetadata);\n>  }\n>\n>  /* Register algorithm with the system. */\n> diff --git a/src/ipa/rpi/controller/rpi/agc.h b/src/ipa/rpi/controller/rpi/agc.h\n> index aaf77c8f..a9158910 100644\n> --- a/src/ipa/rpi/controller/rpi/agc.h\n> +++ b/src/ipa/rpi/controller/rpi/agc.h\n> @@ -6,60 +6,19 @@\n>   */\n>  #pragma once\n>\n> +#include <optional>\n>  #include <vector>\n> -#include <mutex>\n> -\n> -#include <libcamera/base/utils.h>\n>\n>  #include \"../agc_algorithm.h\"\n> -#include \"../agc_status.h\"\n> -#include \"../pwl.h\"\n>\n> -/* This is our implementation of AGC. */\n> +#include \"agc_channel.h\"\n>\n>  namespace RPiController {\n>\n> -struct AgcMeteringMode {\n> -       std::vector<double> weights;\n> -       int read(const libcamera::YamlObject &params);\n> -};\n> -\n> -struct AgcExposureMode {\n> -       std::vector<libcamera::utils::Duration> shutter;\n> -       std::vector<double> gain;\n> -       int read(const libcamera::YamlObject &params);\n> -};\n> -\n> -struct AgcConstraint {\n> -       enum class Bound { LOWER = 0, UPPER = 1 };\n> -       Bound bound;\n> -       double qLo;\n> -       double qHi;\n> -       Pwl yTarget;\n> -       int read(const libcamera::YamlObject &params);\n> -};\n> -\n> -typedef std::vector<AgcConstraint> AgcConstraintMode;\n> -\n> -struct AgcConfig {\n> -       int read(const libcamera::YamlObject &params);\n> -       std::map<std::string, AgcMeteringMode> meteringModes;\n> -       std::map<std::string, AgcExposureMode> exposureModes;\n> -       std::map<std::string, AgcConstraintMode> constraintModes;\n> -       Pwl yTarget;\n> -       double speed;\n> -       uint16_t startupFrames;\n> -       unsigned int convergenceFrames;\n> -       double maxChange;\n> -       double minChange;\n> -       double fastReduceThreshold;\n> -       double speedUpThreshold;\n> -       std::string defaultMeteringMode;\n> -       std::string defaultExposureMode;\n> -       std::string defaultConstraintMode;\n> -       double baseEv;\n> -       libcamera::utils::Duration defaultExposureTime;\n> -       double defaultAnalogueGain;\n> +struct AgcChannelData {\n> +       AgcChannel channel;\n> +       std::optional<DeviceStatus> deviceStatus;\n> +       StatisticsPtr statistics;\n>  };\n>\n>  class Agc : public AgcAlgorithm\n> @@ -70,65 +29,30 @@ public:\n>         int read(const libcamera::YamlObject &params) override;\n>         unsigned int getConvergenceFrames() const override;\n>         std::vector<double> const &getWeights() const override;\n> -       void setEv(double ev) override;\n> -       void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) override;\n> +       void setEv(unsigned int channel, double ev) override;\n> +       void setFlickerPeriod(unsigned int channelIndex,\n> +                             libcamera::utils::Duration flickerPeriod) override;\n>         void setMaxShutter(libcamera::utils::Duration maxShutter) override;\n> -       void setFixedShutter(libcamera::utils::Duration fixedShutter) override;\n> -       void setFixedAnalogueGain(double fixedAnalogueGain) override;\n> +       void setFixedShutter(unsigned int channelIndex,\n> +                            libcamera::utils::Duration fixedShutter) override;\n> +       void setFixedAnalogueGain(unsigned int channelIndex,\n> +                                 double fixedAnalogueGain) override;\n>         void setMeteringMode(std::string const &meteringModeName) override;\n> -       void setExposureMode(std::string const &exposureModeName) override;\n> -       void setConstraintMode(std::string const &contraintModeName) override;\n> -       void enableAuto() override;\n> -       void disableAuto() override;\n> +       void setExposureMode(unsigned int channelIndex,\n> +                            std::string const &exposureModeName) override;\n> +       void setConstraintMode(unsigned int channelIndex,\n> +                              std::string const &contraintModeName) override;\n> +       void enableAuto(unsigned int channelIndex) override;\n> +       void disableAuto(unsigned int channelIndex) override;\n>         void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;\n>         void prepare(Metadata *imageMetadata) override;\n>         void process(StatisticsPtr &stats, Metadata *imageMetadata) override;\n> +       void setActiveChannels(const std::vector<unsigned int> &activeChannels) override;\n>\n>  private:\n> -       bool updateLockStatus(DeviceStatus const &deviceStatus);\n> -       AgcConfig config_;\n> -       void housekeepConfig();\n> -       void fetchCurrentExposure(Metadata *imageMetadata);\n> -       void fetchAwbStatus(Metadata *imageMetadata);\n> -       void computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,\n> -                        double &gain, double &targetY);\n> -       void computeTargetExposure(double gain);\n> -       void filterExposure();\n> -       bool applyDigitalGain(double gain, double targetY);\n> -       void divideUpExposure();\n> -       void writeAndFinish(Metadata *imageMetadata, bool desaturate);\n> -       libcamera::utils::Duration limitShutter(libcamera::utils::Duration shutter);\n> -       double limitGain(double gain) const;\n> -       AgcMeteringMode *meteringMode_;\n> -       AgcExposureMode *exposureMode_;\n> -       AgcConstraintMode *constraintMode_;\n> -       CameraMode mode_;\n> -       uint64_t frameCount_;\n> -       AwbStatus awb_;\n> -       struct ExposureValues {\n> -               ExposureValues();\n> -\n> -               libcamera::utils::Duration shutter;\n> -               double analogueGain;\n> -               libcamera::utils::Duration totalExposure;\n> -               libcamera::utils::Duration totalExposureNoDG; /* without digital gain */\n> -       };\n> -       ExposureValues current_;  /* values for the current frame */\n> -       ExposureValues target_;   /* calculate the values we want here */\n> -       ExposureValues filtered_; /* these values are filtered towards target */\n> -       AgcStatus status_;\n> -       int lockCount_;\n> -       DeviceStatus lastDeviceStatus_;\n> -       libcamera::utils::Duration lastTargetExposure_;\n> -       /* Below here the \"settings\" that applications can change. */\n> -       std::string meteringModeName_;\n> -       std::string exposureModeName_;\n> -       std::string constraintModeName_;\n> -       double ev_;\n> -       libcamera::utils::Duration flickerPeriod_;\n> -       libcamera::utils::Duration maxShutter_;\n> -       libcamera::utils::Duration fixedShutter_;\n> -       double fixedAnalogueGain_;\n> +       int checkChannel(unsigned int channel) const;\n> +       std::vector<AgcChannelData> channelData_;\n> +       std::vector<unsigned int> activeChannels_;\n>  };\n>\n>  } /* namespace RPiController */\n> diff --git a/src/ipa/rpi/controller/rpi/agc_channel.cpp b/src/ipa/rpi/controller/rpi/agc_channel.cpp\n> new file mode 100644\n> index 00000000..d6e30ef2\n> --- /dev/null\n> +++ b/src/ipa/rpi/controller/rpi/agc_channel.cpp\n> @@ -0,0 +1,927 @@\n> +/* SPDX-License-Identifier: BSD-2-Clause */\n> +/*\n> + * Copyright (C) 2019, Raspberry Pi Ltd\n> + *\n> + * agc.cpp - AGC/AEC control algorithm\n> + */\n> +\n> +#include <algorithm>\n> +#include <map>\n> +#include <tuple>\n> +\n> +#include <libcamera/base/log.h>\n> +\n> +#include \"../awb_status.h\"\n> +#include \"../device_status.h\"\n> +#include \"../histogram.h\"\n> +#include \"../lux_status.h\"\n> +#include \"../metadata.h\"\n> +\n> +#include \"agc.h\"\n> +\n> +using namespace RPiController;\n> +using namespace libcamera;\n> +using libcamera::utils::Duration;\n> +using namespace std::literals::chrono_literals;\n> +\n> +LOG_DECLARE_CATEGORY(RPiAgc)\n> +\n> +#define NAME \"rpi.agc\"\n> +\n> +int AgcMeteringMode::read(const libcamera::YamlObject &params)\n> +{\n> +       const YamlObject &yamlWeights = params[\"weights\"];\n> +\n> +       for (const auto &p : yamlWeights.asList()) {\n> +               auto value = p.get<double>();\n> +               if (!value)\n> +                       return -EINVAL;\n> +               weights.push_back(*value);\n> +       }\n> +\n> +       return 0;\n> +}\n> +\n> +static std::tuple<int, std::string>\n> +readMeteringModes(std::map<std::string, AgcMeteringMode> &metering_modes,\n> +                 const libcamera::YamlObject &params)\n> +{\n> +       std::string first;\n> +       int ret;\n> +\n> +       for (const auto &[key, value] : params.asDict()) {\n> +               AgcMeteringMode meteringMode;\n> +               ret = meteringMode.read(value);\n> +               if (ret)\n> +                       return { ret, {} };\n> +\n> +               metering_modes[key] = std::move(meteringMode);\n> +               if (first.empty())\n> +                       first = key;\n> +       }\n> +\n> +       return { 0, first };\n> +}\n> +\n> +int AgcExposureMode::read(const libcamera::YamlObject &params)\n> +{\n> +       auto value = params[\"shutter\"].getList<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       std::transform(value->begin(), value->end(), std::back_inserter(shutter),\n> +                      [](double v) { return v * 1us; });\n> +\n> +       value = params[\"gain\"].getList<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       gain = std::move(*value);\n> +\n> +       if (shutter.size() < 2 || gain.size() < 2) {\n> +               LOG(RPiAgc, Error)\n> +                       << \"AgcExposureMode: must have at least two entries in exposure profile\";\n> +               return -EINVAL;\n> +       }\n> +\n> +       if (shutter.size() != gain.size()) {\n> +               LOG(RPiAgc, Error)\n> +                       << \"AgcExposureMode: expect same number of exposure and gain entries in exposure profile\";\n> +               return -EINVAL;\n> +       }\n> +\n> +       return 0;\n> +}\n> +\n> +static std::tuple<int, std::string>\n> +readExposureModes(std::map<std::string, AgcExposureMode> &exposureModes,\n> +                 const libcamera::YamlObject &params)\n> +{\n> +       std::string first;\n> +       int ret;\n> +\n> +       for (const auto &[key, value] : params.asDict()) {\n> +               AgcExposureMode exposureMode;\n> +               ret = exposureMode.read(value);\n> +               if (ret)\n> +                       return { ret, {} };\n> +\n> +               exposureModes[key] = std::move(exposureMode);\n> +               if (first.empty())\n> +                       first = key;\n> +       }\n> +\n> +       return { 0, first };\n> +}\n> +\n> +int AgcConstraint::read(const libcamera::YamlObject &params)\n> +{\n> +       std::string boundString = params[\"bound\"].get<std::string>(\"\");\n> +       transform(boundString.begin(), boundString.end(),\n> +                 boundString.begin(), ::toupper);\n> +       if (boundString != \"UPPER\" && boundString != \"LOWER\") {\n> +               LOG(RPiAgc, Error) << \"AGC constraint type should be UPPER or LOWER\";\n> +               return -EINVAL;\n> +       }\n> +       bound = boundString == \"UPPER\" ? Bound::UPPER : Bound::LOWER;\n> +\n> +       auto value = params[\"q_lo\"].get<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       qLo = *value;\n> +\n> +       value = params[\"q_hi\"].get<double>();\n> +       if (!value)\n> +               return -EINVAL;\n> +       qHi = *value;\n> +\n> +       return yTarget.read(params[\"y_target\"]);\n> +}\n> +\n> +static std::tuple<int, AgcConstraintMode>\n> +readConstraintMode(const libcamera::YamlObject &params)\n> +{\n> +       AgcConstraintMode mode;\n> +       int ret;\n> +\n> +       for (const auto &p : params.asList()) {\n> +               AgcConstraint constraint;\n> +               ret = constraint.read(p);\n> +               if (ret)\n> +                       return { ret, {} };\n> +\n> +               mode.push_back(std::move(constraint));\n> +       }\n> +\n> +       return { 0, mode };\n> +}\n> +\n> +static std::tuple<int, std::string>\n> +readConstraintModes(std::map<std::string, AgcConstraintMode> &constraintModes,\n> +                   const libcamera::YamlObject &params)\n> +{\n> +       std::string first;\n> +       int ret;\n> +\n> +       for (const auto &[key, value] : params.asDict()) {\n> +               std::tie(ret, constraintModes[key]) = readConstraintMode(value);\n> +               if (ret)\n> +                       return { ret, {} };\n> +\n> +               if (first.empty())\n> +                       first = key;\n> +       }\n> +\n> +       return { 0, first };\n> +}\n> +\n> +int AgcConfig::read(const libcamera::YamlObject &params)\n> +{\n> +       LOG(RPiAgc, Debug) << \"AgcConfig\";\n> +       int ret;\n> +\n> +       std::tie(ret, defaultMeteringMode) =\n> +               readMeteringModes(meteringModes, params[\"metering_modes\"]);\n> +       if (ret)\n> +               return ret;\n> +       std::tie(ret, defaultExposureMode) =\n> +               readExposureModes(exposureModes, params[\"exposure_modes\"]);\n> +       if (ret)\n> +               return ret;\n> +       std::tie(ret, defaultConstraintMode) =\n> +               readConstraintModes(constraintModes, params[\"constraint_modes\"]);\n> +       if (ret)\n> +               return ret;\n> +\n> +       ret = yTarget.read(params[\"y_target\"]);\n> +       if (ret)\n> +               return ret;\n> +\n> +       speed = params[\"speed\"].get<double>(0.2);\n> +       startupFrames = params[\"startup_frames\"].get<uint16_t>(10);\n> +       convergenceFrames = params[\"convergence_frames\"].get<unsigned int>(6);\n> +       fastReduceThreshold = params[\"fast_reduce_threshold\"].get<double>(0.4);\n> +       baseEv = params[\"base_ev\"].get<double>(1.0);\n> +\n> +       /* Start with quite a low value as ramping up is easier than ramping down. */\n> +       defaultExposureTime = params[\"default_exposure_time\"].get<double>(1000) * 1us;\n> +       defaultAnalogueGain = params[\"default_analogue_gain\"].get<double>(1.0);\n> +\n> +       return 0;\n> +}\n> +\n> +AgcChannel::ExposureValues::ExposureValues()\n> +       : shutter(0s), analogueGain(0),\n> +         totalExposure(0s), totalExposureNoDG(0s)\n> +{\n> +}\n> +\n> +AgcChannel::AgcChannel()\n> +       : meteringMode_(nullptr), exposureMode_(nullptr), constraintMode_(nullptr),\n> +         frameCount_(0), lockCount_(0),\n> +         lastTargetExposure_(0s), ev_(1.0), flickerPeriod_(0s),\n> +         maxShutter_(0s), fixedShutter_(0s), fixedAnalogueGain_(0.0)\n> +{\n> +       memset(&awb_, 0, sizeof(awb_));\n> +       /*\n> +        * Setting status_.totalExposureValue_ to zero initially tells us\n> +        * it's not been calculated yet (i.e. Process hasn't yet run).\n> +        */\n> +       status_ = {};\n> +       status_.ev = ev_;\n> +}\n> +\n> +int AgcChannel::read(const libcamera::YamlObject &params,\n> +                    const Controller::HardwareConfig &hardwareConfig)\n> +{\n> +       int ret = config_.read(params);\n> +       if (ret)\n> +               return ret;\n> +\n> +       const Size &size = hardwareConfig.agcZoneWeights;\n> +       for (auto const &modes : config_.meteringModes) {\n> +               if (modes.second.weights.size() != size.width * size.height) {\n> +                       LOG(RPiAgc, Error) << \"AgcMeteringMode: Incorrect number of weights\";\n> +                       return -EINVAL;\n> +               }\n> +       }\n> +\n> +       /*\n> +        * Set the config's defaults (which are the first ones it read) as our\n> +        * current modes, until someone changes them.  (they're all known to\n> +        * exist at this point)\n> +        */\n> +       meteringModeName_ = config_.defaultMeteringMode;\n> +       meteringMode_ = &config_.meteringModes[meteringModeName_];\n> +       exposureModeName_ = config_.defaultExposureMode;\n> +       exposureMode_ = &config_.exposureModes[exposureModeName_];\n> +       constraintModeName_ = config_.defaultConstraintMode;\n> +       constraintMode_ = &config_.constraintModes[constraintModeName_];\n> +       /* Set up the \"last shutter/gain\" values, in case AGC starts \"disabled\". */\n> +       status_.shutterTime = config_.defaultExposureTime;\n> +       status_.analogueGain = config_.defaultAnalogueGain;\n> +       return 0;\n> +}\n> +\n> +void AgcChannel::disableAuto()\n> +{\n> +       fixedShutter_ = status_.shutterTime;\n> +       fixedAnalogueGain_ = status_.analogueGain;\n> +}\n> +\n> +void AgcChannel::enableAuto()\n> +{\n> +       fixedShutter_ = 0s;\n> +       fixedAnalogueGain_ = 0;\n> +}\n> +\n> +unsigned int AgcChannel::getConvergenceFrames() const\n> +{\n> +       /*\n> +        * If shutter and gain have been explicitly set, there is no\n> +        * convergence to happen, so no need to drop any frames - return zero.\n> +        */\n> +       if (fixedShutter_ && fixedAnalogueGain_)\n> +               return 0;\n> +       else\n> +               return config_.convergenceFrames;\n> +}\n> +\n> +std::vector<double> const &AgcChannel::getWeights() const\n> +{\n> +       /*\n> +        * In case someone calls setMeteringMode and then this before the\n> +        * algorithm has run and updated the meteringMode_ pointer.\n> +        */\n> +       auto it = config_.meteringModes.find(meteringModeName_);\n> +       if (it == config_.meteringModes.end())\n> +               return meteringMode_->weights;\n> +       return it->second.weights;\n> +}\n> +\n> +void AgcChannel::setEv(double ev)\n> +{\n> +       ev_ = ev;\n> +}\n> +\n> +void AgcChannel::setFlickerPeriod(Duration flickerPeriod)\n> +{\n> +       flickerPeriod_ = flickerPeriod;\n> +}\n> +\n> +void AgcChannel::setMaxShutter(Duration maxShutter)\n> +{\n> +       maxShutter_ = maxShutter;\n> +}\n> +\n> +void AgcChannel::setFixedShutter(Duration fixedShutter)\n> +{\n> +       fixedShutter_ = fixedShutter;\n> +       /* Set this in case someone calls disableAuto() straight after. */\n> +       status_.shutterTime = limitShutter(fixedShutter_);\n> +}\n> +\n> +void AgcChannel::setFixedAnalogueGain(double fixedAnalogueGain)\n> +{\n> +       fixedAnalogueGain_ = fixedAnalogueGain;\n> +       /* Set this in case someone calls disableAuto() straight after. */\n> +       status_.analogueGain = limitGain(fixedAnalogueGain);\n> +}\n> +\n> +void AgcChannel::setMeteringMode(std::string const &meteringModeName)\n> +{\n> +       meteringModeName_ = meteringModeName;\n> +}\n> +\n> +void AgcChannel::setExposureMode(std::string const &exposureModeName)\n> +{\n> +       exposureModeName_ = exposureModeName;\n> +}\n> +\n> +void AgcChannel::setConstraintMode(std::string const &constraintModeName)\n> +{\n> +       constraintModeName_ = constraintModeName;\n> +}\n> +\n> +void AgcChannel::switchMode(CameraMode const &cameraMode,\n> +                           Metadata *metadata)\n> +{\n> +       /* AGC expects the mode sensitivity always to be non-zero. */\n> +       ASSERT(cameraMode.sensitivity);\n> +\n> +       housekeepConfig();\n> +\n> +       /*\n> +        * Store the mode in the local state. We must cache the sensitivity of\n> +        * of the previous mode for the calculations below.\n> +        */\n> +       double lastSensitivity = mode_.sensitivity;\n> +       mode_ = cameraMode;\n> +\n> +       Duration fixedShutter = limitShutter(fixedShutter_);\n> +       if (fixedShutter && fixedAnalogueGain_) {\n> +               /* We're going to reset the algorithm here with these fixed values. */\n> +\n> +               fetchAwbStatus(metadata);\n> +               double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> +               ASSERT(minColourGain != 0.0);\n> +\n> +               /* This is the equivalent of computeTargetExposure and applyDigitalGain. */\n> +               target_.totalExposureNoDG = fixedShutter_ * fixedAnalogueGain_;\n> +               target_.totalExposure = target_.totalExposureNoDG / minColourGain;\n> +\n> +               /* Equivalent of filterExposure. This resets any \"history\". */\n> +               filtered_ = target_;\n> +\n> +               /* Equivalent of divideUpExposure. */\n> +               filtered_.shutter = fixedShutter;\n> +               filtered_.analogueGain = fixedAnalogueGain_;\n> +       } else if (status_.totalExposureValue) {\n> +               /*\n> +                * On a mode switch, various things could happen:\n> +                * - the exposure profile might change\n> +                * - a fixed exposure or gain might be set\n> +                * - the new mode's sensitivity might be different\n> +                * We cope with the last of these by scaling the target values. After\n> +                * that we just need to re-divide the exposure/gain according to the\n> +                * current exposure profile, which takes care of everything else.\n> +                */\n> +\n> +               double ratio = lastSensitivity / cameraMode.sensitivity;\n> +               target_.totalExposureNoDG *= ratio;\n> +               target_.totalExposure *= ratio;\n> +               filtered_.totalExposureNoDG *= ratio;\n> +               filtered_.totalExposure *= ratio;\n> +\n> +               divideUpExposure();\n> +       } else {\n> +               /*\n> +                * We come through here on startup, when at least one of the shutter\n> +                * or gain has not been fixed. We must still write those values out so\n> +                * that they will be applied immediately. We supply some arbitrary defaults\n> +                * for any that weren't set.\n> +                */\n> +\n> +               /* Equivalent of divideUpExposure. */\n> +               filtered_.shutter = fixedShutter ? fixedShutter : config_.defaultExposureTime;\n> +               filtered_.analogueGain = fixedAnalogueGain_ ? fixedAnalogueGain_ : config_.defaultAnalogueGain;\n> +       }\n> +\n> +       writeAndFinish(metadata, false);\n> +}\n> +\n> +void AgcChannel::prepare(Metadata *imageMetadata)\n> +{\n> +       Duration totalExposureValue = status_.totalExposureValue;\n> +       AgcStatus delayedStatus;\n> +       AgcPrepareStatus prepareStatus;\n> +\n> +       if (!imageMetadata->get(\"agc.delayed_status\", delayedStatus))\n> +               totalExposureValue = delayedStatus.totalExposureValue;\n> +\n> +       prepareStatus.digitalGain = 1.0;\n> +       prepareStatus.locked = false;\n> +\n> +       if (status_.totalExposureValue) {\n> +               /* Process has run, so we have meaningful values. */\n> +               DeviceStatus deviceStatus;\n> +               if (imageMetadata->get(\"device.status\", deviceStatus) == 0) {\n> +                       Duration actualExposure = deviceStatus.shutterSpeed *\n> +                                                 deviceStatus.analogueGain;\n> +                       if (actualExposure) {\n> +                               double digitalGain = totalExposureValue / actualExposure;\n> +                               LOG(RPiAgc, Debug) << \"Want total exposure \" << totalExposureValue;\n> +                               /*\n> +                                * Never ask for a gain < 1.0, and also impose\n> +                                * some upper limit. Make it customisable?\n> +                                */\n> +                               prepareStatus.digitalGain = std::max(1.0, std::min(digitalGain, 4.0));\n> +                               LOG(RPiAgc, Debug) << \"Actual exposure \" << actualExposure;\n> +                               LOG(RPiAgc, Debug) << \"Use digitalGain \" << prepareStatus.digitalGain;\n> +                               LOG(RPiAgc, Debug) << \"Effective exposure \"\n> +                                                  << actualExposure * prepareStatus.digitalGain;\n> +                               /* Decide whether AEC/AGC has converged. */\n> +                               prepareStatus.locked = updateLockStatus(deviceStatus);\n> +                       }\n> +               } else\n> +                       LOG(RPiAgc, Warning) << \"AgcChannel: no device metadata\";\n> +               imageMetadata->set(\"agc.prepare_status\", prepareStatus);\n> +       }\n> +}\n> +\n> +void AgcChannel::process(StatisticsPtr &stats, Metadata *imageMetadata)\n> +{\n> +       frameCount_++;\n> +       /*\n> +        * First a little bit of housekeeping, fetching up-to-date settings and\n> +        * configuration, that kind of thing.\n> +        */\n> +       housekeepConfig();\n> +       /* Fetch the AWB status immediately, so that we can assume it's there. */\n> +       fetchAwbStatus(imageMetadata);\n> +       /* Get the current exposure values for the frame that's just arrived. */\n> +       fetchCurrentExposure(imageMetadata);\n> +       /* Compute the total gain we require relative to the current exposure. */\n> +       double gain, targetY;\n> +       computeGain(stats, imageMetadata, gain, targetY);\n> +       /* Now compute the target (final) exposure which we think we want. */\n> +       computeTargetExposure(gain);\n> +       /* The results have to be filtered so as not to change too rapidly. */\n> +       filterExposure();\n> +       /*\n> +        * Some of the exposure has to be applied as digital gain, so work out\n> +        * what that is. This function also tells us whether it's decided to\n> +        * \"desaturate\" the image more quickly.\n> +        */\n> +       bool desaturate = applyDigitalGain(gain, targetY);\n> +       /*\n> +        * The last thing is to divide up the exposure value into a shutter time\n> +        * and analogue gain, according to the current exposure mode.\n> +        */\n> +       divideUpExposure();\n> +       /* Finally advertise what we've done. */\n> +       writeAndFinish(imageMetadata, desaturate);\n> +}\n> +\n> +bool AgcChannel::updateLockStatus(DeviceStatus const &deviceStatus)\n> +{\n> +       const double errorFactor = 0.10; /* make these customisable? */\n> +       const int maxLockCount = 5;\n> +       /* Reset \"lock count\" when we exceed this multiple of errorFactor */\n> +       const double resetMargin = 1.5;\n> +\n> +       /* Add 200us to the exposure time error to allow for line quantisation. */\n> +       Duration exposureError = lastDeviceStatus_.shutterSpeed * errorFactor + 200us;\n> +       double gainError = lastDeviceStatus_.analogueGain * errorFactor;\n> +       Duration targetError = lastTargetExposure_ * errorFactor;\n> +\n> +       /*\n> +        * Note that we don't know the exposure/gain limits of the sensor, so\n> +        * the values we keep requesting may be unachievable. For this reason\n> +        * we only insist that we're close to values in the past few frames.\n> +        */\n> +       if (deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed - exposureError &&\n> +           deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed + exposureError &&\n> +           deviceStatus.analogueGain > lastDeviceStatus_.analogueGain - gainError &&\n> +           deviceStatus.analogueGain < lastDeviceStatus_.analogueGain + gainError &&\n> +           status_.targetExposureValue > lastTargetExposure_ - targetError &&\n> +           status_.targetExposureValue < lastTargetExposure_ + targetError)\n> +               lockCount_ = std::min(lockCount_ + 1, maxLockCount);\n> +       else if (deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed - resetMargin * exposureError ||\n> +                deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed + resetMargin * exposureError ||\n> +                deviceStatus.analogueGain < lastDeviceStatus_.analogueGain - resetMargin * gainError ||\n> +                deviceStatus.analogueGain > lastDeviceStatus_.analogueGain + resetMargin * gainError ||\n> +                status_.targetExposureValue < lastTargetExposure_ - resetMargin * targetError ||\n> +                status_.targetExposureValue > lastTargetExposure_ + resetMargin * targetError)\n> +               lockCount_ = 0;\n> +\n> +       lastDeviceStatus_ = deviceStatus;\n> +       lastTargetExposure_ = status_.targetExposureValue;\n> +\n> +       LOG(RPiAgc, Debug) << \"Lock count updated to \" << lockCount_;\n> +       return lockCount_ == maxLockCount;\n> +}\n> +\n> +void AgcChannel::housekeepConfig()\n> +{\n> +       /* First fetch all the up-to-date settings, so no one else has to do it. */\n> +       status_.ev = ev_;\n> +       status_.fixedShutter = limitShutter(fixedShutter_);\n> +       status_.fixedAnalogueGain = fixedAnalogueGain_;\n> +       status_.flickerPeriod = flickerPeriod_;\n> +       LOG(RPiAgc, Debug) << \"ev \" << status_.ev << \" fixedShutter \"\n> +                          << status_.fixedShutter << \" fixedAnalogueGain \"\n> +                          << status_.fixedAnalogueGain;\n> +       /*\n> +        * Make sure the \"mode\" pointers point to the up-to-date things, if\n> +        * they've changed.\n> +        */\n> +       if (meteringModeName_ != status_.meteringMode) {\n> +               auto it = config_.meteringModes.find(meteringModeName_);\n> +               if (it == config_.meteringModes.end()) {\n> +                       LOG(RPiAgc, Warning) << \"No metering mode \" << meteringModeName_;\n> +                       meteringModeName_ = status_.meteringMode;\n> +               } else {\n> +                       meteringMode_ = &it->second;\n> +                       status_.meteringMode = meteringModeName_;\n> +               }\n> +       }\n> +       if (exposureModeName_ != status_.exposureMode) {\n> +               auto it = config_.exposureModes.find(exposureModeName_);\n> +               if (it == config_.exposureModes.end()) {\n> +                       LOG(RPiAgc, Warning) << \"No exposure profile \" << exposureModeName_;\n> +                       exposureModeName_ = status_.exposureMode;\n> +               } else {\n> +                       exposureMode_ = &it->second;\n> +                       status_.exposureMode = exposureModeName_;\n> +               }\n> +       }\n> +       if (constraintModeName_ != status_.constraintMode) {\n> +               auto it = config_.constraintModes.find(constraintModeName_);\n> +               if (it == config_.constraintModes.end()) {\n> +                       LOG(RPiAgc, Warning) << \"No constraint list \" << constraintModeName_;\n> +                       constraintModeName_ = status_.constraintMode;\n> +               } else {\n> +                       constraintMode_ = &it->second;\n> +                       status_.constraintMode = constraintModeName_;\n> +               }\n> +       }\n> +       LOG(RPiAgc, Debug) << \"exposureMode \"\n> +                          << exposureModeName_ << \" constraintMode \"\n> +                          << constraintModeName_ << \" meteringMode \"\n> +                          << meteringModeName_;\n> +}\n> +\n> +void AgcChannel::fetchCurrentExposure(Metadata *imageMetadata)\n> +{\n> +       std::unique_lock<Metadata> lock(*imageMetadata);\n> +       DeviceStatus *deviceStatus =\n> +               imageMetadata->getLocked<DeviceStatus>(\"device.status\");\n> +       if (!deviceStatus)\n> +               LOG(RPiAgc, Fatal) << \"No device metadata\";\n> +       current_.shutter = deviceStatus->shutterSpeed;\n> +       current_.analogueGain = deviceStatus->analogueGain;\n> +       AgcStatus *agcStatus =\n> +               imageMetadata->getLocked<AgcStatus>(\"agc.status\");\n> +       current_.totalExposure = agcStatus ? agcStatus->totalExposureValue : 0s;\n> +       current_.totalExposureNoDG = current_.shutter * current_.analogueGain;\n> +}\n> +\n> +void AgcChannel::fetchAwbStatus(Metadata *imageMetadata)\n> +{\n> +       awb_.gainR = 1.0; /* in case not found in metadata */\n> +       awb_.gainG = 1.0;\n> +       awb_.gainB = 1.0;\n> +       if (imageMetadata->get(\"awb.status\", awb_) != 0)\n> +               LOG(RPiAgc, Debug) << \"No AWB status found\";\n> +}\n> +\n> +static double computeInitialY(StatisticsPtr &stats, AwbStatus const &awb,\n> +                             std::vector<double> &weights, double gain)\n> +{\n> +       constexpr uint64_t maxVal = 1 << Statistics::NormalisationFactorPow2;\n> +\n> +       /*\n> +        * If we have no AGC region stats, but do have a a Y histogram, use that\n> +        * directly to caluclate the mean Y value of the image.\n> +        */\n> +       if (!stats->agcRegions.numRegions() && stats->yHist.bins()) {\n> +               /*\n> +                * When the gain is applied to the histogram, anything below minBin\n> +                * will scale up directly with the gain, but anything above that\n> +                * will saturate into the top bin.\n> +                */\n> +               auto &hist = stats->yHist;\n> +               double minBin = std::min(1.0, 1.0 / gain) * hist.bins();\n> +               double binMean = hist.interBinMean(0.0, minBin);\n> +               double numUnsaturated = hist.cumulativeFreq(minBin);\n> +               /* This term is from all the pixels that won't saturate. */\n> +               double ySum = binMean * gain * numUnsaturated;\n> +               /* And add the ones that will saturate. */\n> +               ySum += (hist.total() - numUnsaturated) * hist.bins();\n> +               return ySum / hist.total() / hist.bins();\n> +       }\n> +\n> +       ASSERT(weights.size() == stats->agcRegions.numRegions());\n> +\n> +       /*\n> +        * Note that the weights are applied by the IPA to the statistics directly,\n> +        * before they are given to us here.\n> +        */\n> +       double rSum = 0, gSum = 0, bSum = 0, pixelSum = 0;\n> +       for (unsigned int i = 0; i < stats->agcRegions.numRegions(); i++) {\n> +               auto &region = stats->agcRegions.get(i);\n> +               rSum += std::min<double>(region.val.rSum * gain, (maxVal - 1) * region.counted);\n> +               gSum += std::min<double>(region.val.gSum * gain, (maxVal - 1) * region.counted);\n> +               bSum += std::min<double>(region.val.bSum * gain, (maxVal - 1) * region.counted);\n> +               pixelSum += region.counted;\n> +       }\n> +       if (pixelSum == 0.0) {\n> +               LOG(RPiAgc, Warning) << \"computeInitialY: pixelSum is zero\";\n> +               return 0;\n> +       }\n> +\n> +       double ySum;\n> +       /* Factor in the AWB correction if needed. */\n> +       if (stats->agcStatsPos == Statistics::AgcStatsPos::PreWb) {\n> +               ySum = rSum * awb.gainR * .299 +\n> +                      gSum * awb.gainG * .587 +\n> +                      gSum * awb.gainB * .114;\n> +       } else\n> +               ySum = rSum * .299 + gSum * .587 + gSum * .114;\n> +\n> +       return ySum / pixelSum / (1 << 16);\n> +}\n> +\n> +/*\n> + * We handle extra gain through EV by adjusting our Y targets. However, you\n> + * simply can't monitor histograms once they get very close to (or beyond!)\n> + * saturation, so we clamp the Y targets to this value. It does mean that EV\n> + * increases don't necessarily do quite what you might expect in certain\n> + * (contrived) cases.\n> + */\n> +\n> +static constexpr double EvGainYTargetLimit = 0.9;\n> +\n> +static double constraintComputeGain(AgcConstraint &c, const Histogram &h, double lux,\n> +                                   double evGain, double &targetY)\n> +{\n> +       targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));\n> +       targetY = std::min(EvGainYTargetLimit, targetY * evGain);\n> +       double iqm = h.interQuantileMean(c.qLo, c.qHi);\n> +       return (targetY * h.bins()) / iqm;\n> +}\n> +\n> +void AgcChannel::computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,\n> +                            double &gain, double &targetY)\n> +{\n> +       struct LuxStatus lux = {};\n> +       lux.lux = 400; /* default lux level to 400 in case no metadata found */\n> +       if (imageMetadata->get(\"lux.status\", lux) != 0)\n> +               LOG(RPiAgc, Warning) << \"No lux level found\";\n> +       const Histogram &h = statistics->yHist;\n> +       double evGain = status_.ev * config_.baseEv;\n> +       /*\n> +        * The initial gain and target_Y come from some of the regions. After\n> +        * that we consider the histogram constraints.\n> +        */\n> +       targetY = config_.yTarget.eval(config_.yTarget.domain().clip(lux.lux));\n> +       targetY = std::min(EvGainYTargetLimit, targetY * evGain);\n> +\n> +       /*\n> +        * Do this calculation a few times as brightness increase can be\n> +        * non-linear when there are saturated regions.\n> +        */\n> +       gain = 1.0;\n> +       for (int i = 0; i < 8; i++) {\n> +               double initialY = computeInitialY(statistics, awb_, meteringMode_->weights, gain);\n> +               double extraGain = std::min(10.0, targetY / (initialY + .001));\n> +               gain *= extraGain;\n> +               LOG(RPiAgc, Debug) << \"Initial Y \" << initialY << \" target \" << targetY\n> +                                  << \" gives gain \" << gain;\n> +               if (extraGain < 1.01) /* close enough */\n> +                       break;\n> +       }\n> +\n> +       for (auto &c : *constraintMode_) {\n> +               double newTargetY;\n> +               double newGain = constraintComputeGain(c, h, lux.lux, evGain, newTargetY);\n> +               LOG(RPiAgc, Debug) << \"Constraint has target_Y \"\n> +                                  << newTargetY << \" giving gain \" << newGain;\n> +               if (c.bound == AgcConstraint::Bound::LOWER && newGain > gain) {\n> +                       LOG(RPiAgc, Debug) << \"Lower bound constraint adopted\";\n> +                       gain = newGain;\n> +                       targetY = newTargetY;\n> +               } else if (c.bound == AgcConstraint::Bound::UPPER && newGain < gain) {\n> +                       LOG(RPiAgc, Debug) << \"Upper bound constraint adopted\";\n> +                       gain = newGain;\n> +                       targetY = newTargetY;\n> +               }\n> +       }\n> +       LOG(RPiAgc, Debug) << \"Final gain \" << gain << \" (target_Y \" << targetY << \" ev \"\n> +                          << status_.ev << \" base_ev \" << config_.baseEv\n> +                          << \")\";\n> +}\n> +\n> +void AgcChannel::computeTargetExposure(double gain)\n> +{\n> +       if (status_.fixedShutter && status_.fixedAnalogueGain) {\n> +               /*\n> +                * When ag and shutter are both fixed, we need to drive the\n> +                * total exposure so that we end up with a digital gain of at least\n> +                * 1/minColourGain. Otherwise we'd desaturate channels causing\n> +                * white to go cyan or magenta.\n> +                */\n> +               double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> +               ASSERT(minColourGain != 0.0);\n> +               target_.totalExposure =\n> +                       status_.fixedShutter * status_.fixedAnalogueGain / minColourGain;\n> +       } else {\n> +               /*\n> +                * The statistics reflect the image without digital gain, so the final\n> +                * total exposure we're aiming for is:\n> +                */\n> +               target_.totalExposure = current_.totalExposureNoDG * gain;\n> +               /* The final target exposure is also limited to what the exposure mode allows. */\n> +               Duration maxShutter = status_.fixedShutter\n> +                                             ? status_.fixedShutter\n> +                                             : exposureMode_->shutter.back();\n> +               maxShutter = limitShutter(maxShutter);\n> +               Duration maxTotalExposure =\n> +                       maxShutter *\n> +                       (status_.fixedAnalogueGain != 0.0\n> +                                ? status_.fixedAnalogueGain\n> +                                : exposureMode_->gain.back());\n> +               target_.totalExposure = std::min(target_.totalExposure, maxTotalExposure);\n> +       }\n> +       LOG(RPiAgc, Debug) << \"Target totalExposure \" << target_.totalExposure;\n> +}\n> +\n> +bool AgcChannel::applyDigitalGain(double gain, double targetY)\n> +{\n> +       double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });\n> +       ASSERT(minColourGain != 0.0);\n> +       double dg = 1.0 / minColourGain;\n> +       /*\n> +        * I think this pipeline subtracts black level and rescales before we\n> +        * get the stats, so no need to worry about it.\n> +        */\n> +       LOG(RPiAgc, Debug) << \"after AWB, target dg \" << dg << \" gain \" << gain\n> +                          << \" target_Y \" << targetY;\n> +       /*\n> +        * Finally, if we're trying to reduce exposure but the target_Y is\n> +        * \"close\" to 1.0, then the gain computed for that constraint will be\n> +        * only slightly less than one, because the measured Y can never be\n> +        * larger than 1.0. When this happens, demand a large digital gain so\n> +        * that the exposure can be reduced, de-saturating the image much more\n> +        * quickly (and we then approach the correct value more quickly from\n> +        * below).\n> +        */\n> +       bool desaturate = targetY > config_.fastReduceThreshold &&\n> +                         gain < sqrt(targetY);\n> +       if (desaturate)\n> +               dg /= config_.fastReduceThreshold;\n> +       LOG(RPiAgc, Debug) << \"Digital gain \" << dg << \" desaturate? \" << desaturate;\n> +       filtered_.totalExposureNoDG = filtered_.totalExposure / dg;\n> +       LOG(RPiAgc, Debug) << \"Target totalExposureNoDG \" << filtered_.totalExposureNoDG;\n> +       return desaturate;\n> +}\n> +\n> +void AgcChannel::filterExposure()\n> +{\n> +       double speed = config_.speed;\n> +       /*\n> +        * AGC adapts instantly if both shutter and gain are directly specified\n> +        * or we're in the startup phase.\n> +        */\n> +       if ((status_.fixedShutter && status_.fixedAnalogueGain) ||\n> +           frameCount_ <= config_.startupFrames)\n> +               speed = 1.0;\n> +       if (!filtered_.totalExposure) {\n> +               filtered_.totalExposure = target_.totalExposure;\n> +       } else {\n> +               /*\n> +                * If close to the result go faster, to save making so many\n> +                * micro-adjustments on the way. (Make this customisable?)\n> +                */\n> +               if (filtered_.totalExposure < 1.2 * target_.totalExposure &&\n> +                   filtered_.totalExposure > 0.8 * target_.totalExposure)\n> +                       speed = sqrt(speed);\n> +               filtered_.totalExposure = speed * target_.totalExposure +\n> +                                         filtered_.totalExposure * (1.0 - speed);\n> +       }\n> +       LOG(RPiAgc, Debug) << \"After filtering, totalExposure \" << filtered_.totalExposure\n> +                          << \" no dg \" << filtered_.totalExposureNoDG;\n> +}\n> +\n> +void AgcChannel::divideUpExposure()\n> +{\n> +       /*\n> +        * Sending the fixed shutter/gain cases through the same code may seem\n> +        * unnecessary, but it will make more sense when extend this to cover\n> +        * variable aperture.\n> +        */\n> +       Duration exposureValue = filtered_.totalExposureNoDG;\n> +       Duration shutterTime;\n> +       double analogueGain;\n> +       shutterTime = status_.fixedShutter ? status_.fixedShutter\n> +                                          : exposureMode_->shutter[0];\n> +       shutterTime = limitShutter(shutterTime);\n> +       analogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain\n> +                                                       : exposureMode_->gain[0];\n> +       analogueGain = limitGain(analogueGain);\n> +       if (shutterTime * analogueGain < exposureValue) {\n> +               for (unsigned int stage = 1;\n> +                    stage < exposureMode_->gain.size(); stage++) {\n> +                       if (!status_.fixedShutter) {\n> +                               Duration stageShutter =\n> +                                       limitShutter(exposureMode_->shutter[stage]);\n> +                               if (stageShutter * analogueGain >= exposureValue) {\n> +                                       shutterTime = exposureValue / analogueGain;\n> +                                       break;\n> +                               }\n> +                               shutterTime = stageShutter;\n> +                       }\n> +                       if (status_.fixedAnalogueGain == 0.0) {\n> +                               if (exposureMode_->gain[stage] * shutterTime >= exposureValue) {\n> +                                       analogueGain = exposureValue / shutterTime;\n> +                                       break;\n> +                               }\n> +                               analogueGain = exposureMode_->gain[stage];\n> +                               analogueGain = limitGain(analogueGain);\n> +                       }\n> +               }\n> +       }\n> +       LOG(RPiAgc, Debug) << \"Divided up shutter and gain are \" << shutterTime << \" and \"\n> +                          << analogueGain;\n> +       /*\n> +        * Finally adjust shutter time for flicker avoidance (require both\n> +        * shutter and gain not to be fixed).\n> +        */\n> +       if (!status_.fixedShutter && !status_.fixedAnalogueGain &&\n> +           status_.flickerPeriod) {\n> +               int flickerPeriods = shutterTime / status_.flickerPeriod;\n> +               if (flickerPeriods) {\n> +                       Duration newShutterTime = flickerPeriods * status_.flickerPeriod;\n> +                       analogueGain *= shutterTime / newShutterTime;\n> +                       /*\n> +                        * We should still not allow the ag to go over the\n> +                        * largest value in the exposure mode. Note that this\n> +                        * may force more of the total exposure into the digital\n> +                        * gain as a side-effect.\n> +                        */\n> +                       analogueGain = std::min(analogueGain, exposureMode_->gain.back());\n> +                       analogueGain = limitGain(analogueGain);\n> +                       shutterTime = newShutterTime;\n> +               }\n> +               LOG(RPiAgc, Debug) << \"After flicker avoidance, shutter \"\n> +                                  << shutterTime << \" gain \" << analogueGain;\n> +       }\n> +       filtered_.shutter = shutterTime;\n> +       filtered_.analogueGain = analogueGain;\n> +}\n> +\n> +void AgcChannel::writeAndFinish(Metadata *imageMetadata, bool desaturate)\n> +{\n> +       status_.totalExposureValue = filtered_.totalExposure;\n> +       status_.targetExposureValue = desaturate ? 0s : target_.totalExposureNoDG;\n> +       status_.shutterTime = filtered_.shutter;\n> +       status_.analogueGain = filtered_.analogueGain;\n> +       /*\n> +        * Write to metadata as well, in case anyone wants to update the camera\n> +        * immediately.\n> +        */\n> +       imageMetadata->set(\"agc.status\", status_);\n> +       LOG(RPiAgc, Debug) << \"Output written, total exposure requested is \"\n> +                          << filtered_.totalExposure;\n> +       LOG(RPiAgc, Debug) << \"Camera exposure update: shutter time \" << filtered_.shutter\n> +                          << \" analogue gain \" << filtered_.analogueGain;\n> +}\n> +\n> +Duration AgcChannel::limitShutter(Duration shutter)\n> +{\n> +       /*\n> +        * shutter == 0 is a special case for fixed shutter values, and must pass\n> +        * through unchanged\n> +        */\n> +       if (!shutter)\n> +               return shutter;\n> +\n> +       shutter = std::clamp(shutter, mode_.minShutter, maxShutter_);\n> +       return shutter;\n> +}\n> +\n> +double AgcChannel::limitGain(double gain) const\n> +{\n> +       /*\n> +        * Only limit the lower bounds of the gain value to what the sensor limits.\n> +        * The upper bound on analogue gain will be made up with additional digital\n> +        * gain applied by the ISP.\n> +        *\n> +        * gain == 0.0 is a special case for fixed shutter values, and must pass\n> +        * through unchanged\n> +        */\n> +       if (!gain)\n> +               return gain;\n> +\n> +       gain = std::max(gain, mode_.minAnalogueGain);\n> +       return gain;\n> +}\n> diff --git a/src/ipa/rpi/controller/rpi/agc_channel.h b/src/ipa/rpi/controller/rpi/agc_channel.h\n> new file mode 100644\n> index 00000000..dc4356f3\n> --- /dev/null\n> +++ b/src/ipa/rpi/controller/rpi/agc_channel.h\n> @@ -0,0 +1,135 @@\n> +/* SPDX-License-Identifier: BSD-2-Clause */\n> +/*\n> + * Copyright (C) 2019, Raspberry Pi Ltd\n> + *\n> + * agc.h - AGC/AEC control algorithm\n> + */\n> +#pragma once\n> +\n> +#include <mutex>\n> +#include <vector>\n> +\n> +#include <libcamera/base/utils.h>\n> +\n> +#include \"../agc_status.h\"\n> +#include \"../awb_status.h\"\n> +#include \"../pwl.h\"\n> +\n> +/* This is our implementation of AGC. */\n> +\n> +namespace RPiController {\n> +\n> +struct AgcMeteringMode {\n> +       std::vector<double> weights;\n> +       int read(const libcamera::YamlObject &params);\n> +};\n> +\n> +struct AgcExposureMode {\n> +       std::vector<libcamera::utils::Duration> shutter;\n> +       std::vector<double> gain;\n> +       int read(const libcamera::YamlObject &params);\n> +};\n> +\n> +struct AgcConstraint {\n> +       enum class Bound { LOWER = 0,\n> +                          UPPER = 1 };\n> +       Bound bound;\n> +       double qLo;\n> +       double qHi;\n> +       Pwl yTarget;\n> +       int read(const libcamera::YamlObject &params);\n> +};\n> +\n> +typedef std::vector<AgcConstraint> AgcConstraintMode;\n> +\n> +struct AgcConfig {\n> +       int read(const libcamera::YamlObject &params);\n> +       std::map<std::string, AgcMeteringMode> meteringModes;\n> +       std::map<std::string, AgcExposureMode> exposureModes;\n> +       std::map<std::string, AgcConstraintMode> constraintModes;\n> +       Pwl yTarget;\n> +       double speed;\n> +       uint16_t startupFrames;\n> +       unsigned int convergenceFrames;\n> +       double maxChange;\n> +       double minChange;\n> +       double fastReduceThreshold;\n> +       double speedUpThreshold;\n> +       std::string defaultMeteringMode;\n> +       std::string defaultExposureMode;\n> +       std::string defaultConstraintMode;\n> +       double baseEv;\n> +       libcamera::utils::Duration defaultExposureTime;\n> +       double defaultAnalogueGain;\n> +};\n> +\n> +class AgcChannel\n> +{\n> +public:\n> +       AgcChannel();\n> +       int read(const libcamera::YamlObject &params,\n> +                const Controller::HardwareConfig &hardwareConfig);\n> +       unsigned int getConvergenceFrames() const;\n> +       std::vector<double> const &getWeights() const;\n> +       void setEv(double ev);\n> +       void setFlickerPeriod(libcamera::utils::Duration flickerPeriod);\n> +       void setMaxShutter(libcamera::utils::Duration maxShutter);\n> +       void setFixedShutter(libcamera::utils::Duration fixedShutter);\n> +       void setFixedAnalogueGain(double fixedAnalogueGain);\n> +       void setMeteringMode(std::string const &meteringModeName);\n> +       void setExposureMode(std::string const &exposureModeName);\n> +       void setConstraintMode(std::string const &contraintModeName);\n> +       void enableAuto();\n> +       void disableAuto();\n> +       void switchMode(CameraMode const &cameraMode, Metadata *metadata);\n> +       void prepare(Metadata *imageMetadata);\n> +       void process(StatisticsPtr &stats, Metadata *imageMetadata);\n> +\n> +private:\n> +       bool updateLockStatus(DeviceStatus const &deviceStatus);\n> +       AgcConfig config_;\n> +       void housekeepConfig();\n> +       void fetchCurrentExposure(Metadata *imageMetadata);\n> +       void fetchAwbStatus(Metadata *imageMetadata);\n> +       void computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,\n> +                        double &gain, double &targetY);\n> +       void computeTargetExposure(double gain);\n> +       void filterExposure();\n> +       bool applyDigitalGain(double gain, double targetY);\n> +       void divideUpExposure();\n> +       void writeAndFinish(Metadata *imageMetadata, bool desaturate);\n> +       libcamera::utils::Duration limitShutter(libcamera::utils::Duration shutter);\n> +       double limitGain(double gain) const;\n> +       AgcMeteringMode *meteringMode_;\n> +       AgcExposureMode *exposureMode_;\n> +       AgcConstraintMode *constraintMode_;\n> +       CameraMode mode_;\n> +       uint64_t frameCount_;\n> +       AwbStatus awb_;\n> +       struct ExposureValues {\n> +               ExposureValues();\n> +\n> +               libcamera::utils::Duration shutter;\n> +               double analogueGain;\n> +               libcamera::utils::Duration totalExposure;\n> +               libcamera::utils::Duration totalExposureNoDG; /* without digital gain */\n> +       };\n> +       ExposureValues current_; /* values for the current frame */\n> +       ExposureValues target_; /* calculate the values we want here */\n> +       ExposureValues filtered_; /* these values are filtered towards target */\n> +       AgcStatus status_;\n> +       int lockCount_;\n> +       DeviceStatus lastDeviceStatus_;\n> +       libcamera::utils::Duration lastTargetExposure_;\n> +       /* Below here the \"settings\" that applications can change. */\n> +       std::string meteringModeName_;\n> +       std::string exposureModeName_;\n> +       std::string constraintModeName_;\n> +       double ev_;\n> +       libcamera::utils::Duration flickerPeriod_;\n> +       libcamera::utils::Duration maxShutter_;\n> +       libcamera::utils::Duration fixedShutter_;\n> +       double fixedAnalogueGain_;\n> +};\n> +\n> +} /* namespace RPiController */\n> --\n> 2.30.2\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 91F30BF415\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 22 Aug 2023 12:32:20 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D689A61E08;\n\tTue, 22 Aug 2023 14:32:19 +0200 (CEST)","from mail-yw1-x1132.google.com (mail-yw1-x1132.google.com\n\t[IPv6:2607:f8b0:4864:20::1132])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 71C8C6055E\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 22 Aug 2023 14:32:17 +0200 (CEST)","by mail-yw1-x1132.google.com with SMTP id\n\t00721157ae682-58de42f9f05so46978507b3.2\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 22 Aug 2023 05:32:17 -0700 (PDT)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1692707539;\n\tbh=EelUGdGtjao6I6fTKKxQEK7LRXgU1fLKn42JdVtTUpI=;\n\th=References:In-Reply-To:Date:To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=VfBP2y4cIyTMxfoW2BIuGx22rbLjzWWiD5wH1rWbQt1f9n5HwaWSoW6vt31ypk+Q+\n\trp60sU5gt9eH9LwzCZ7GL0suaEnA//hcBUt8JI84Uxp5XWcH5vDRm0j3fHBXd45awV\n\tzQ53BujnmUWNvvT1Rwc2YYimOGcOLTTCAs5zpPCXnn4RwptmoT2GE6/bzSV3Js+s8W\n\t9D7Xw+F7YrycLCy4bDMN10/VMSC2Q7A+1rsT8wNOcFy0dk7hsoeTKXFut5xvBMweSv\n\tQHQ+rBgojR0qVX8nJfH3+nUZ09McOohEEJ12HLO6GjSH9XAtb46vz6vWWSafhejSW7\n\tknzZPrElCjE1A==","v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1692707536; x=1693312336;\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=ZwZBqmk8BxuZ2OcOrAkkj9110k5AbK6cuHxczIpulD8=;\n\tb=UlGpi0fXm6kfFDRDneFA0bOK8mfzKvIyRWnWyM6mYxyl+f+5bZKFnDBiLtnvsgi6S1\n\t5wvDBFNqnK1YlTeMgnGx0zbSfzcBDOEHfV3Vz6wUSpi2MjXyndJiBE2PY630MFoP3SxO\n\ttYdxfx2U+D1DEq3qjshtNg7rG4uucP3fPdRRzOfw0Odg/TUcsdPzIoBXONPYaXCJ8NGv\n\tMOZoSbgNRP8vsfmtRAZVvzrECBLmXkc0hVciijPnaYRknTBOq4hZBWJC9eHcQYTzrnwD\n\tfIbXnrOV4AMTp1LSO+ubDbGlCQWdAI5Ng0WqARZ9u1QbKKtSiLiwoGNKS69qVnvlUhXX\n\tG+4g=="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key; \n\tunprotected) header.d=raspberrypi.com\n\theader.i=@raspberrypi.com\n\theader.b=\"UlGpi0fX\"; dkim-atps=neutral","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20221208; t=1692707536; x=1693312336;\n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:x-gm-message-state:from:to:cc:subject:date:message-id\n\t:reply-to;\n\tbh=ZwZBqmk8BxuZ2OcOrAkkj9110k5AbK6cuHxczIpulD8=;\n\tb=Hjkhh0FVnGI6gIxjB+79u8xbFMaw0t+gqwpErpe4MB7FC8ZeroTXJ/M7yhiohoDeTu\n\tIE7S8R+6QNg7/lrH4JNALbyf3V7jZq7nwap2y14qaOmptrBSEej5YFlw8PxZmWS3cfzZ\n\t/2jrTHLOa3ooqSjhcaxktmrTMnHrQatOI78YnKcbZjuzf/pxgPt88HgtTbpaXPSscXk8\n\tlUktRWCjoWji4C0Z988N+hblUfQxM7JIjC0ZyQSXkigNFOdUVOFGofTkSZ0FTVQ8/FyP\n\tD38tny1zP5g0nlRVWkptAqx6C8FJLIgZn4aQonGi7JejWpQZmZ7xenFvhSt9qljaHo42\n\ttT5w==","X-Gm-Message-State":"AOJu0YyCRYlmZy4+cCWUuNE2cyDAgJFTIwoi++J/1uI4nFX6CyB2wkil\n\thHWve4bLKoUPG2F9wSEy6RrSf99UU18ovDEnmc9H13yXa1cCl4ezRO0=","X-Google-Smtp-Source":"AGHT+IFAezyAqc5DkdLogw8GYC+tg1AAy9wgTryNMBNR33j38gSLu6yqSekg6w3dBgxsPEwUAfbQJoS1hXEkcMCPRdc=","X-Received":"by 2002:a81:6055:0:b0:586:a216:a348 with SMTP id\n\tu82-20020a816055000000b00586a216a348mr9609047ywb.18.1692707534790;\n\tTue, 22 Aug 2023 05:32:14 -0700 (PDT)","MIME-Version":"1.0","References":"<20230731094641.73646-1-david.plowman@raspberrypi.com>\n\t<20230731094641.73646-3-david.plowman@raspberrypi.com>","In-Reply-To":"<20230731094641.73646-3-david.plowman@raspberrypi.com>","Date":"Tue, 22 Aug 2023 13:32:08 +0100","Message-ID":"<CAEmqJPqPXijrHGZDwinEqvKS-T8fC7uV74b6Y+m0i4M_jxHpXQ@mail.gmail.com>","To":"David Plowman <david.plowman@raspberrypi.com>","Content-Type":"text/plain; charset=\"UTF-8\"","Subject":"Re: [libcamera-devel] [PATCH 2/5] ipa: rpi: agc: Reorganise code\n\tfor multi-channel AGC","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>","From":"Naushir Patuck via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Naushir Patuck <naush@raspberrypi.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":27687,"web_url":"https://patchwork.libcamera.org/comment/27687/","msgid":"<CAEmqJPrdEZs95w6RpPw7G1we_=O4CKGNPmreQr-EPo3V3TEKSQ@mail.gmail.com>","date":"2023-08-22T13:20:11","subject":"Re: [libcamera-devel] [PATCH 2/5] ipa: rpi: agc: Reorganise code\n\tfor multi-channel AGC","submitter":{"id":34,"url":"https://patchwork.libcamera.org/api/people/34/","name":"Naushir Patuck","email":"naush@raspberrypi.com"},"content":"On Tue, 22 Aug 2023 at 13:32, Naushir Patuck <naush@raspberrypi.com> wrote:\n>\n> Hi David,\n>\n> Thank you for your patch.\n>\n> On Mon, 31 Jul 2023 at 10:47, David Plowman via libcamera-devel\n> <libcamera-devel@lists.libcamera.org> wrote:\n> >\n> > This commit does the basic reorganisation of the code in order to\n> > implement multi-channel AGC. The main changes are:\n> >\n> > * The previous Agc class (in agc.cpp) has become the AgcChannel class\n> >   in (agc_channel.cpp).\n> >\n> > * A new Agc class is introduced which is a wrapper round a number of\n> >   AgcChannels.\n> >\n> > * The basic plumbing from ipa_base.cpp to Agc is updated to include a\n> >   channel number. All the existing controls are hardwired to talk\n> >   directly to channel 0.\n> >\n> > There are a couple of limitations which we expect to apply to\n> > multi-channel AGC. We're not allowing different frame durations to be\n> > applied to the channels, nor are we allowing separate metering\n> > modes. To be fair, supporting these things is not impossible, but\n> > there are reasons why it may be tricky so they remain \"TBD\" for now.\n> >\n> > This patch only includes the basic reorganisation and plumbing. It\n> > does not yet update the important methods (switchMode, prepare and\n> > process) to implement multi-channel AGC properly. This will appear in\n> > a subsequent commit. For now, these functions are hard-coded just to\n> > use channel 0, thereby preserving the existing behaviour.\n> >\n> > Signed-off-by: David Plowman <david.plowman@raspberrypi.com>\n>\n> Don't see anything wrong with this change\n>\n> Reviewed-by: Naushir Patuck <naush@raspberrypi.com>\n\nSorry for the double tag on this patch.  This was actually meant for patch 3/5!\n\nNaush","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 24DB1BE080\n\tfor <parsemail@patchwork.libcamera.org>;\n\tTue, 22 Aug 2023 13:20:20 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id D9E62627E1;\n\tTue, 22 Aug 2023 15:20:19 +0200 (CEST)","from mail-yw1-x112a.google.com (mail-yw1-x112a.google.com\n\t[IPv6:2607:f8b0:4864:20::112a])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 8287561E08\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 22 Aug 2023 15:20:18 +0200 (CEST)","by mail-yw1-x112a.google.com with SMTP id\n\t00721157ae682-5921a962adfso23355987b3.1\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tTue, 22 Aug 2023 06:20:18 -0700 (PDT)"],"DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/simple; d=libcamera.org;\n\ts=mail; t=1692710419;\n\tbh=YtZbnhkGpKCOlTklyEgtCmCkRG/8eQ8eEbBmN38VxqI=;\n\th=References:In-Reply-To:Date:To:Subject:List-Id:List-Unsubscribe:\n\tList-Archive:List-Post:List-Help:List-Subscribe:From:Reply-To:Cc:\n\tFrom;\n\tb=chHs+zxvCttI4qLVxwleRUlt/0UpsJnuaUaJc/dyVfspqrc4g0rv7GGKGe6YTlKBl\n\tEIHDFIn8lg3/b5XObRjgw0hTYKy9NQ+nyvZSngFxA++fSpA2OS4xgCWQYL2PuybGXE\n\tGWXBrdwr8+5nU/XU02NtI13BZdlQbReEibya+b6OZqdpuVixdc9mj+1OaMXUpuQ8xT\n\t/4Jx3fcQqJgipjff1lCX/JtfJgx9WQa/4xJMfwwZ7LqfOnOXacWMfvhcuoa8VHDQ07\n\t5LEj8iLPfbYvS8+mMwT2lcAXIfWAGR90zo60sZB6iGxPNFLPm4EZ8dc+kElb5fgFsx\n\to5vtMVki3bz9Q==","v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=raspberrypi.com; s=google; t=1692710417; x=1693315217;\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=PgG1ojNDbSRJatKo/Uja1L/3kPteHeK8DMNzPUj39es=;\n\tb=J3frwIFsYc2+lIE17fwyRVVSWuzhfmlEM2NsWdZoa5QOEyqqeNDmENjkuZ29RlPZNJ\n\tvOJYBwJVY8cKDCP8CX7ToYBLTj9hUzIxgRwvrsp+QJe7wDvna+2T9bDxQkwM82cmmcUt\n\tJSnT7yG1EGOiXR5OkLwuowA6vmag34P8qRRfZpseOVOicy0dBTuP9K+ezZI7rpxoSCiv\n\t8YMViZ3quPbQHGa1maPgdOJHo7P69ueFiZ+A3ABzpY1Y6/D5XYdOB69zCDfGVR0UMuCx\n\tlSdvklz0Wfp+Jrdd/CTy47tsU0qWx3O1rINCS1OsRZBRN78UB5M7LgE2n0u/8DQgE/3x\n\thIKA=="],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key; \n\tunprotected) header.d=raspberrypi.com\n\theader.i=@raspberrypi.com\n\theader.b=\"J3frwIFs\"; dkim-atps=neutral","X-Google-DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/relaxed;\n\td=1e100.net; s=20221208; t=1692710417; x=1693315217;\n\th=cc:to:subject:message-id:date:from:in-reply-to:references\n\t:mime-version:x-gm-message-state:from:to:cc:subject:date:message-id\n\t:reply-to;\n\tbh=PgG1ojNDbSRJatKo/Uja1L/3kPteHeK8DMNzPUj39es=;\n\tb=gbj30gZmP22TaeuqmX/2RCZqOY4PGgGak4q/3iDd4k0cb/jbPGGMixIuoPk1CyRXtd\n\tTvBeGRMsFOrQu6OKjwpE40ZSpe4slvgea3cRSQSJ101XbdH9zH9Tikj9BlKbfGaZ/3XB\n\te2k4dbeJdpElJyvfAWO7TqKOOxtpjeKPpTWEDMw7FdYgDvTpTYXhXAon4bZyj7iAFPms\n\tEHlhJaFNN1gana8JdqLEY9//aS9C38yqqHBaaNk615QZN/l/ylIObemJB3ff+4Xqy2M+\n\tbXdtVFWULUF3jMnNmukzTJUqAAJAtPUF6ZGbMwWbngKQNPrKBAML7U2ikaodnHqHY6d9\n\tY4KA==","X-Gm-Message-State":"AOJu0Yx6rPLpfTzsuhxBGUQF3buVTh0G0bhmwoUQ7kX46cWIgQwqnRwJ\n\ti3gdle5D34IYLPyimfFgwxbUBxF/fYOkESx5u7PfWA==","X-Google-Smtp-Source":"AGHT+IEd+KSTU1+OYdyb4HxhqTLGo9iKi+lHy9Zz7hFu+IUYLDW4CkbucBHnOVmHmrOwvmb5+kGIZEbX+XQENftq2Ok=","X-Received":"by 2002:a81:8409:0:b0:56f:fffc:56ff with SMTP id\n\tu9-20020a818409000000b0056ffffc56ffmr11660169ywf.42.1692710417457;\n\tTue, 22 Aug 2023 06:20:17 -0700 (PDT)","MIME-Version":"1.0","References":"<20230731094641.73646-1-david.plowman@raspberrypi.com>\n\t<20230731094641.73646-3-david.plowman@raspberrypi.com>\n\t<CAEmqJPqPXijrHGZDwinEqvKS-T8fC7uV74b6Y+m0i4M_jxHpXQ@mail.gmail.com>","In-Reply-To":"<CAEmqJPqPXijrHGZDwinEqvKS-T8fC7uV74b6Y+m0i4M_jxHpXQ@mail.gmail.com>","Date":"Tue, 22 Aug 2023 14:20:11 +0100","Message-ID":"<CAEmqJPrdEZs95w6RpPw7G1we_=O4CKGNPmreQr-EPo3V3TEKSQ@mail.gmail.com>","To":"David Plowman <david.plowman@raspberrypi.com>","Content-Type":"text/plain; charset=\"UTF-8\"","Subject":"Re: [libcamera-devel] [PATCH 2/5] ipa: rpi: agc: Reorganise code\n\tfor multi-channel AGC","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>","From":"Naushir Patuck via libcamera-devel\n\t<libcamera-devel@lists.libcamera.org>","Reply-To":"Naushir Patuck <naush@raspberrypi.com>","Cc":"libcamera-devel@lists.libcamera.org","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]