[{"id":39180,"web_url":"https://patchwork.libcamera.org/comment/39180/","msgid":"<178180198392.1557007.1618335019176518883@ping.linuxembedded.co.uk>","date":"2026-06-18T16:59:43","subject":"Re: [PATCH 13/14] ipa: rppx1: Add AWB algorithm","submitter":{"id":4,"url":"https://patchwork.libcamera.org/api/people/4/","name":"Kieran Bingham","email":"kieran.bingham@ideasonboard.com"},"content":"Quoting Jacopo Mondi (2026-06-18 11:18:52)\n> Add Awb algorithm to the RPP-X1 IPA.\n> \n> The implementation is based on libIPA AwbAlgorithm.\n> \n> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>\n> ---\n>  src/ipa/rppx1/algorithms/awb.cpp     | 298 +++++++++++++++++++++++++++++++++++\n>  src/ipa/rppx1/algorithms/awb.h       |  48 ++++++\n>  src/ipa/rppx1/algorithms/meson.build |   5 +\n>  src/ipa/rppx1/ipa_context.cpp        |  30 ++++\n>  src/ipa/rppx1/ipa_context.h          |  10 ++\n>  src/ipa/rppx1/meson.build            |   4 +\n>  6 files changed, 395 insertions(+)\n> \n> diff --git a/src/ipa/rppx1/algorithms/awb.cpp b/src/ipa/rppx1/algorithms/awb.cpp\n> new file mode 100644\n> index 000000000000..6316606b4b0f\n> --- /dev/null\n> +++ b/src/ipa/rppx1/algorithms/awb.cpp\n> @@ -0,0 +1,298 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2026, Ideas On Board\n> + *\n> + * RPP-X1 AWB control algorithm\n> + */\n> +\n> +\n> +#include \"awb.h\"\n> +\n> +#include <libcamera/base/log.h>\n> +#include <libcamera/internal/vector.h>\n> +\n> +/**\n> + * \\file awb.h\n> + * \\brief RPP-X1 Auto White Balance\n> + */\n> +\n> +namespace libcamera {\n> +\n> +namespace ipa::rppx1::algorithms {\n> +\n> +/* \\todo RPP-X1 doesn't support the Lux algorithm. */\n> +static constexpr unsigned int kDefaultLux = 500;\n> +\n> +LOG_DEFINE_CATEGORY(RppX1Awb)\n> +\n> +namespace {\n> +\n> +/* Quantize a gain to the UQ6.12 register format. */\n> +constexpr uint32_t quantizeGain(double gain)\n> +{\n> +       return UQ<6, 12>(static_cast<float>(gain)).quantized();\n> +}\n> +\n> +}; /* namespace */\n> +\n> +/*\n> + * The RPP-X1 ISP has a bus width of 24 bits and all the measurement limits are\n> + * expressed as 24 bit values. In order to make it simpler to express limits\n> + * in code use an 8-bit value and left-shift by 16 to match the 24 bit width of\n> + * the ISP processing pipeline.\n> + */\n> +constexpr unsigned int kRPPX1BusWidthShift = 16;\n> +\n> +class RppX1AwbStats final : public AwbStats\n> +{\n> +public:\n> +       RppX1AwbStats() {}\n> +       RppX1AwbStats(const RGB<double> &rgbMeans)\n> +               : AwbStats(rgbMeans)\n> +       {\n> +       }\n> +\n> +       /* Minimum mean value below which AWB can't operate. */\n> +       double minColourValue() const override\n> +       {\n> +               return 2.0;\n> +       }\n> +};\n> +\n> +/**\n> + * \\class Awb\n> + * \\brief RPP-X1 white balance correction algorithm\n> + */\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::init\n> + */\n> +int Awb::init(IPAContext &context, const ValueNode &tuningData)\n> +{\n> +       return awbAlgo_.init(tuningData, context.ctrlMap);\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::configure\n> + */\n> +int Awb::configure(IPAContext &context,\n> +                  const IPACameraSensorInfo &configInfo)\n> +{\n> +       awbAlgo_.configure(context.activeState.awb);\n> +\n> +       /*\n> +        * Define the measurement window for AWB as a centered rectangle\n> +        * covering 3/4 of the image width and height.\n> +        */\n\nSome time we should really make the windows initialised and defined by\ncommon controls as well in libipa, but this is fine for now.\n\n> +       context.configuration.awb.measureWindow.h_offs = configInfo.outputSize.width / 8;\n> +       context.configuration.awb.measureWindow.v_offs = configInfo.outputSize.height / 8;\n> +       context.configuration.awb.measureWindow.h_size = 3 * configInfo.outputSize.width / 4;\n> +       context.configuration.awb.measureWindow.v_size = 3 * configInfo.outputSize.height / 4;\n> +\n\nAnd when it's in common libipa, all the users will report the configured\nwindow through the metadata too ... but that can be later. Lets get the\ncurrent updates progressing first.\n\n\n> +       context.configuration.awb.enabled = true;\n> +\n> +       return 0;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::queueRequest\n> + */\n> +void Awb::queueRequest(IPAContext &context, const uint32_t frame,\n> +                      IPAFrameContext &frameContext,\n> +                      const ControlList &controls)\n> +{\n> +       awbAlgo_.queueRequest(context.activeState.awb, frame, frameContext.awb,\n> +                             controls);\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::prepare\n> + */\n> +void Awb::prepare(IPAContext &context, const uint32_t frame,\n> +                 IPAFrameContext &frameContext, RppX1Params *params)\n> +{\n> +       awbAlgo_.prepare(context.activeState.awb, frameContext.awb);\n> +\n> +       auto gainConfig = params->block<BlockType::AwbgPre1>();\n> +       gainConfig.setEnabled(true);\n> +\n> +       gainConfig->gain_green_b = quantizeGain(frameContext.awb.gains.g());\n> +       gainConfig->gain_blue = quantizeGain(frameContext.awb.gains.b());\n> +       gainConfig->gain_red = quantizeGain(frameContext.awb.gains.r());\n> +       gainConfig->gain_green_r = quantizeGain(frameContext.awb.gains.g());\n> +\n> +       if (frame > 0)\n> +               return;\n> +\n> +       /*\n> +        * If this is the first frame, program the white balance measurement\n> +        * engine.\n> +        */\n> +\n> +       auto awbConfig = params->block<BlockType::WbmeasPost>();\n> +       awbConfig.setEnabled(true);\n> +\n> +       /* Configure the measure window for AWB. */\n> +       awbConfig->wnd = context.configuration.awb.measureWindow;\n> +\n> +       /* Number of frames to use to estimate the means (0 means 1 frame). */\n> +       awbConfig->frames = 0;\n> +\n> +       /* Use YCbCr measurement mode. */\n> +       awbConfig->mode = RPPX1_WBMEAS_MODE_YCBCR;\n> +\n> +       /* Set the reference Cr and Cb (AWB target) to white. */\n> +       awbConfig->ref_cb_max_b = 128 << kRPPX1BusWidthShift;\n> +       awbConfig->ref_cr_max_r = 128 << kRPPX1BusWidthShift;\n> +\n> +       /*\n> +        * Filter out pixels based on luminance and chrominance values.\n> +        * The acceptable luma values are specified as a [16, 250]\n> +        * range, while the acceptable chroma values are specified\n> +        * with a minimum of 16 and a maximum Cb+Cr sum of 250;\n> +        */\n> +       awbConfig->min_y_max_g = 16 << kRPPX1BusWidthShift;\n> +       awbConfig->max_y = 250 << kRPPX1BusWidthShift;\n> +       awbConfig->min_c = 16 << kRPPX1BusWidthShift;\n> +       awbConfig->max_csum = 250 << kRPPX1BusWidthShift;\n> +\n> +       /* \\todo Disable ymax_cmp even if we program 'max_y' */\n> +       awbConfig->ymax_cmp = 0;\n> +\n> +       /*\n> +        * Program coefficients and offsets to perform RGB-to-YCbCr\n> +        * conversion according to the BT.601 specification for limited\n> +        * range YUV.\n> +        *\n> +        *  Y = 16 + 0.2500 R + 0.5000 G + 0.1094 B\n> +        *  Cb = 128 - 0.1406 R - 0.2969 G + 0.4375 B\n> +        *  Cr = 128 + 0.4375 R - 0.3750 G - 0.0625 B\n> +        *\n> +        * which resuls in the following register values:\n> +        *\n> +        * - coefficients are signed Q4.12 format\n> +        * - their numerical value is then 'reg / 2^12'\n> +        * - negative numbers need to reverse the 2's complement\n> +        *   (!(reg & !BIT(16)) + 1) / 2^12\n> +        *\n> +        * coeff G0 0x00000800; = 0.500\n> +        * coeff B0 0x000001c0; = 0.1094\n> +        * coeff R0 0x00000400; = 0.2500\n> +        * coeff G1 0x0000fb40; = -0.2969\n> +        * coeff B1 0x00000700; = 0.4375\n> +        * coeff R1 0x0000fdc0; = -0.1406\n> +        * coeff G2 0x0000fa00; = -0.3750\n> +        * coeff B2 0x0000ff00; = -0.0625\n> +        * coeff R2 0x00000700; = 0.4375\n> +        * offset R 0x00100000; offset_r = (16 << 16)\n> +        * offset G 0x00800000; offset_g = (128 << 16)\n> +        * offset B 0x00800000; offset_b = (128 << 16)\n> +        *\n> +        * Use the inverse of this matrix in calculateRgbMeans() to\n> +        * reverse the colorspace conversion.\n> +        */\n> +       awbConfig->ccor_coeff[0][0] = 0x00000800;\n> +       awbConfig->ccor_coeff[0][1] = 0x000001c0;\n> +       awbConfig->ccor_coeff[0][2] = 0x00000400;\n> +       awbConfig->ccor_coeff[1][0] = 0x0000fb40;\n> +       awbConfig->ccor_coeff[1][1] = 0x00000700;\n> +       awbConfig->ccor_coeff[1][2] = 0x0000fdc0;\n> +       awbConfig->ccor_coeff[2][0] = 0x0000fa00;\n> +       awbConfig->ccor_coeff[2][1] = 0x0000ff00;\n> +       awbConfig->ccor_coeff[2][2] = 0x00000700;\n\nSo you already defined quantizeGain() for Q6.12, and that means it would\nwork here too:\n\tawbConfig->ccor_coeff[0][0] = quantizeGain(0.500);\n\tawbConfig->ccor_coeff[0][1] = quantizeGain(0.1094);\n\t...\n\nperhaps with quantizeGain() renamed to quantize() ?\n\nOr you could encode directly\n\n        awbConfig->ccor_coeff[0][0] = Q<4, 12>(0.500).quantized();\n        awbConfig->ccor_coeff[0][1] = Q<4, 12>(0.1094).quantized();\n        ...\n\nWould that make sense? or is it better to put hex values directly ? Did\nyou calculate them or were they specified in a datasheet?\n\n\nMaybe one day I need to support a Matrix<Q<4,12>, 3, 3> ... :S\n\n\n> +\n> +       awbConfig->ccor_offs[0] = 16 << kRPPX1BusWidthShift;\n> +       awbConfig->ccor_offs[1] = 128 << kRPPX1BusWidthShift;\n> +       awbConfig->ccor_offs[2] = 128 << kRPPX1BusWidthShift;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::process\n> + */\n> +void Awb::process(IPAContext &context,\n> +                 [[maybe_unused]] const uint32_t frame,\n> +                 IPAFrameContext &frameContext,\n> +                 const RppX1Stats *stats,\n> +                 ControlList &metadata)\n> +{\n> +       RppX1AwbStats awbStats = calculateRgbMeans(frameContext, stats);\n> +\n> +       awbAlgo_.process(context.activeState.awb, frameContext.awb, awbStats,\n> +                        kDefaultLux, metadata);\n> +}\n> +\n> +RppX1AwbStats Awb::calculateRgbMeans(const IPAFrameContext &frameContext,\n> +                                    const RppX1Stats *stats) const\n> +{\n> +       if (!stats)\n> +               return {};\n> +\n> +       const auto awb = stats->block<StatsType::WbmeasPost>();\n> +       if (!awb)\n> +               return {};\n> +\n> +       if (awb->cnt == 0) {\n> +               LOG(RppX1Awb, Debug) << \"AWB statistics are empty\";\n> +               return {};\n> +       }\n> +\n> +       Vector<double, 3> rgbMeans;\n> +\n> +       /* Get the YCbCr mean values: we use YCbCr measurement mode. */\n> +       Vector<double, 3> yuvMeans({\n> +               static_cast<double>(awb->mean_y_or_g),\n> +               static_cast<double>(awb->mean_cb_or_b),\n> +               static_cast<double>(awb->mean_cr_or_r)\n> +       });\n> +\n> +       /*\n> +        * Convert from YCbCr to RGB. The statistics engine has been\n> +        * programmed with the following matrix:\n> +        *\n> +        * Y  =  16 + 0.2500 R + 0.5000 G + 0.1094 B\n> +        * Cb = 128 - 0.1406 R - 0.2969 G + 0.4375 B\n> +        * Cr = 128 + 0.4375 R - 0.3750 G - 0.0625 B\n> +        *\n> +        * Use the inverse matrix here.\n> +        */\n> +       static const Matrix<double, 3, 3> yuv2rgbMatrix({\n> +               1.1636, -0.0623,  1.6008,\n> +               1.1636, -0.4045, -0.7949,\n> +               1.1636,  1.9912, -0.0250\n> +       });\n> +       static const Vector<double, 3> yuv2rgbOffset({\n> +               16, 128, 128\n> +       });\n\nPerhaps we should really have these conversion helpers in\nsrc/ipa/libipa/colours.h and avoid duplication from rkisp1.\n\n> +\n> +       rgbMeans = yuv2rgbMatrix * (yuvMeans - yuv2rgbOffset);\n> +\n> +       /*\n> +        * Due to hardware rounding errors in the YCbCr means, the\n> +        * calculated RGB means may be negative. This would lead to\n> +        * negative gains, messing up calculation. Prevent this by\n> +        * clamping the means to positive values.\n> +        */\n> +       rgbMeans = rgbMeans.max(0.0);\n> +\n> +       /*\n> +        * \\todo\n> +        * The ISP computes the AWB means after applying the CCM. Apply\n> +        * the inverse as we want to get the raw means before the colour gains.\n> +        * rgbMeans = frameContext.ccm.ccm.inverse() * rgbMeans;\n> +        */\n\nThis sounds quite important? ... and easy to already put in ? or is this\nhere to decide if it's right or just the one below should be used?\n\n\n> +\n> +       /*\n> +        * The ISP computes the AWB means after applying the colour gains,\n> +        * divide by the gains that were used to get the raw means from the\n> +        * sensor. Apply a minimum value to avoid divisions by near-zero.\n> +        */\n> +       rgbMeans /= frameContext.awb.gains.max(0.01);\n> +\n> +       return RppX1AwbStats(rgbMeans);\n> +}\n> +\n> +REGISTER_IPA_ALGORITHM(Awb, \"Awb\")\n> +\n> +} /* namespace ipa::rppx1::algorithms */\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/ipa/rppx1/algorithms/awb.h b/src/ipa/rppx1/algorithms/awb.h\n> new file mode 100644\n> index 000000000000..2e4f67f11bc0\n> --- /dev/null\n> +++ b/src/ipa/rppx1/algorithms/awb.h\n> @@ -0,0 +1,48 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2026, Ideas On Board\n> + *\n> + * AWB control algorithm\n> + */\n> +\n> +#pragma once\n> +\n> +#include \"libipa/awb.h\"\n> +#include \"libipa/fixedpoint.h\"\n> +\n> +#include \"algorithm.h\"\n> +\n> +namespace libcamera {\n> +\n> +namespace ipa::rppx1::algorithms {\n> +\n> +class RppX1AwbStats;\n> +\n> +class Awb : public Algorithm\n> +{\n> +public:\n> +       Awb() = default;\n> +       ~Awb() = default;\n> +\n> +       int init(IPAContext &context, const ValueNode &tuningData) override;\n> +       int configure(IPAContext &context, const IPACameraSensorInfo &configInfo) override;\n> +       void queueRequest(IPAContext &context, const uint32_t frame,\n> +                         IPAFrameContext &frameContext,\n> +                         const ControlList &controls) override;\n> +       void prepare(IPAContext &context, const uint32_t frame,\n> +                    IPAFrameContext &frameContext,\n> +                    RppX1Params *params) override;\n> +       void process(IPAContext &context, const uint32_t frame,\n> +                    IPAFrameContext &frameContext,\n> +                    const RppX1Stats *stats,\n> +                    ControlList &metadata) override;\n> +\n> +private:\n> +       RppX1AwbStats calculateRgbMeans(const IPAFrameContext &frameContext,\n> +                                       const RppX1Stats *stats) const;\n> +\n> +       AwbAlgorithm<UQ<6, 12>> awbAlgo_;\n> +};\n> +\n> +} /* namespace ipa::rppx1::algorithms */\n> +} /* namespace libcamera */\n> diff --git a/src/ipa/rppx1/algorithms/meson.build b/src/ipa/rppx1/algorithms/meson.build\n> new file mode 100644\n> index 000000000000..70368ad9bac9\n> --- /dev/null\n> +++ b/src/ipa/rppx1/algorithms/meson.build\n> @@ -0,0 +1,5 @@\n> +# SPDX-License-Identifier: CC0-1.0\n> +\n> +rppx1_ipa_algorithms = files([\n> +    'awb.cpp',\n> +])\n> diff --git a/src/ipa/rppx1/ipa_context.cpp b/src/ipa/rppx1/ipa_context.cpp\n> index 5ac60dfc7bf6..ea7008f5685d 100644\n> --- a/src/ipa/rppx1/ipa_context.cpp\n> +++ b/src/ipa/rppx1/ipa_context.cpp\n> @@ -16,21 +16,51 @@\n>  \n>  namespace libcamera::ipa::rppx1 {\n>  \n> +/**\n> + * \\struct RppX1AwbSession\n> + * \\brief RPP-X1 Awb session configuration\n> + */\n> +\n> +/**\n> + * \\var RppX1AwbSession::measureWindow\n> + * \\brief Awb measurement window\n> + */\n> +\n> +/**\n> + * \\var RppX1AwbSession::enabled\n> + * \\brief Awb enabled flag\n> + */\n> +\n>  /**\n>   * \\struct IPASessionConfiguration\n>   * \\brief Session configuration for the IPA module\n>   */\n>  \n> +/**\n> + * \\var IPASessionConfiguration::awb\n> + * \\brief Awb session configuration\n> + */\n> +\n>  /**\n>   * \\struct IPAActiveState\n>   * \\brief Active state for algorithms\n>   */\n>  \n> +/**\n> + * \\var IPAActiveState::awb\n> + * \\copydoc ipa::awb::ActiveState\n> + */\n> +\n>  /**\n>   * \\struct IPAFrameContext\n>   * \\brief Per-frame context for algorithms\n>   */\n>  \n> +/**\n> + * \\var IPAFrameContext::awb\n> + * \\copydoc ipa::awb::FrameContext\n> + */\n> +\n>  /**\n>   * \\struct IPAContext\n>   * \\brief Global IPA context data shared between all algorithms\n> diff --git a/src/ipa/rppx1/ipa_context.h b/src/ipa/rppx1/ipa_context.h\n> index f268a35081bc..57197d865d3f 100644\n> --- a/src/ipa/rppx1/ipa_context.h\n> +++ b/src/ipa/rppx1/ipa_context.h\n> @@ -20,17 +20,27 @@\n>  #include <libipa/camera_sensor_helper.h>\n>  #include <libipa/fc_queue.h>\n>  \n> +#include \"libipa/awb.h\"\n> +\n>  namespace libcamera {\n>  \n>  namespace ipa::rppx1 {\n>  \n> +struct RppX1AwbSession {\n> +       struct rppx1_window measureWindow;\n> +       bool enabled;\n> +};\n> +\n>  struct IPASessionConfiguration {\n> +       struct RppX1AwbSession awb;\n>  };\n>  \n>  struct IPAActiveState {\n> +       ipa::awb::ActiveState awb;\n>  };\n>  \n>  struct IPAFrameContext : public FrameContext {\n> +       ipa::awb::FrameContext awb;\n>  };\n>  \n>  struct IPAContext {\n> diff --git a/src/ipa/rppx1/meson.build b/src/ipa/rppx1/meson.build\n> index 8034fe24d241..8b8c79e997c1 100644\n> --- a/src/ipa/rppx1/meson.build\n> +++ b/src/ipa/rppx1/meson.build\n> @@ -1,4 +1,6 @@\n>  # SPDX-License-Identifier: CC0-1.0\n> +#\n> +subdir('algorithms')\n>  \n>  ipa_name = 'ipa_rppx1'\n>  \n> @@ -7,6 +9,8 @@ rppx1_ipa_sources = files([\n>      'rppx1.cpp',\n>  ])\n>  \n> +rppx1_ipa_sources += rppx1_ipa_algorithms\n> +\n>  mod = shared_module(ipa_name, rppx1_ipa_sources,\n>                      name_prefix : '',\n>                      include_directories : [ipa_includes],\n> \n> -- \n> 2.54.0\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 28C46C3261\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 18 Jun 2026 16:59:49 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id 35FA262C6B;\n\tThu, 18 Jun 2026 18:59:48 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 656DA61754\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 18 Jun 2026 18:59:46 +0200 (CEST)","from monstersaurus.ideasonboard.com\n\t(cpc89244-aztw30-2-0-cust6594.18-1.cable.virginm.net [86.31.185.195])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 290E18E0;\n\tThu, 18 Jun 2026 18:59:11 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"ieY7UnJm\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1781801951;\n\tbh=R8yS1dnSHUPfTj/OPaBjDbzKlFOLuoQ/L+ZHXHBH75k=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=ieY7UnJmk/TMIFU4nh6qxhzdQ7IiP+Z0Qvi96N7GEZhEjwHObxX85nsV/LYw2Tpcs\n\t+bMx40N/xuZAkMCHUshDyujpJ7V9b5zgIHs3AFX2jMxVXDvNvrmqvGEU9doopIUfs9\n\tA7l76K61xNza/MiZgntViFtSGOwml4S/SQE+emVU=","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<20260618-rppx1-ipa-v1-13-32337264cfcd@ideasonboard.com>","References":"<20260618-rppx1-ipa-v1-0-32337264cfcd@ideasonboard.com>\n\t<20260618-rppx1-ipa-v1-13-32337264cfcd@ideasonboard.com>","Subject":"Re: [PATCH 13/14] ipa: rppx1: Add AWB algorithm","From":"Kieran Bingham <kieran.bingham@ideasonboard.com>","Cc":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>","To":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>, Niklas =?utf-8?b?U8O2?=\n\t=?utf-8?q?derlund?= <niklas.soderlund+renesas@ragnatech.se>,\n\tlibcamera-devel@lists.libcamera.org","Date":"Thu, 18 Jun 2026 17:59:43 +0100","Message-ID":"<178180198392.1557007.1618335019176518883@ping.linuxembedded.co.uk>","User-Agent":"alot/0.9.1","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":39181,"web_url":"https://patchwork.libcamera.org/comment/39181/","msgid":"<178180212901.1557007.2222308762619446850@ping.linuxembedded.co.uk>","date":"2026-06-18T17:02:09","subject":"Re: [PATCH 13/14] ipa: rppx1: Add AWB algorithm","submitter":{"id":4,"url":"https://patchwork.libcamera.org/api/people/4/","name":"Kieran Bingham","email":"kieran.bingham@ideasonboard.com"},"content":"Quoting Kieran Bingham (2026-06-18 17:59:43)\n\n<snip>\n\n> > +\n> > +       rgbMeans = yuv2rgbMatrix * (yuvMeans - yuv2rgbOffset);\n> > +\n> > +       /*\n> > +        * Due to hardware rounding errors in the YCbCr means, the\n> > +        * calculated RGB means may be negative. This would lead to\n> > +        * negative gains, messing up calculation. Prevent this by\n> > +        * clamping the means to positive values.\n> > +        */\n> > +       rgbMeans = rgbMeans.max(0.0);\n> > +\n> > +       /*\n> > +        * \\todo\n> > +        * The ISP computes the AWB means after applying the CCM. Apply\n> > +        * the inverse as we want to get the raw means before the colour gains.\n> > +        * rgbMeans = frameContext.ccm.ccm.inverse() * rgbMeans;\n> > +        */\n> \n> This sounds quite important? ... and easy to already put in ? or is this\n> here to decide if it's right or just the one below should be used?\n\nOk ok, I just read patch 14 and now I feel silly.\n\nEven with the discussion points in the rest of the file, this was the\nonly part keeping me from doing this and now I understand why so:\n\nReviewed-by: Kieran Bingham <kieran.bingham@ideasonboard.com>","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 226F1C3261\n\tfor <parsemail@patchwork.libcamera.org>;\n\tThu, 18 Jun 2026 17:02:15 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id E770462C79;\n\tThu, 18 Jun 2026 19:02:13 +0200 (CEST)","from perceval.ideasonboard.com (perceval.ideasonboard.com\n\t[IPv6:2001:4b98:dc2:55:216:3eff:fef7:d647])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 58F9161754\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tThu, 18 Jun 2026 19:02:12 +0200 (CEST)","from monstersaurus.ideasonboard.com\n\t(cpc89244-aztw30-2-0-cust6594.18-1.cable.virginm.net [86.31.185.195])\n\tby perceval.ideasonboard.com (Postfix) with ESMTPSA id 193671E6;\n\tThu, 18 Jun 2026 19:01:37 +0200 (CEST)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (1024-bit key;\n\tunprotected) header.d=ideasonboard.com header.i=@ideasonboard.com\n\theader.b=\"KZ7GOIBB\"; dkim-atps=neutral","DKIM-Signature":"v=1; a=rsa-sha256; c=relaxed/simple; d=ideasonboard.com;\n\ts=mail; t=1781802097;\n\tbh=p4tdeXbMuRFA66imy0dAdC7X+bfyXRTXP6a4pRJhKRQ=;\n\th=In-Reply-To:References:Subject:From:Cc:To:Date:From;\n\tb=KZ7GOIBBoNRk/bzEM7nZZXHeIBndm6nZL6s6m5MvUOm2IHz0LCsBTkzmrEDwue0fn\n\tlUpaHzOSG0V7bcMfyrKqqgcRj+LvM3XH7KrJd8FzuEuyF3Vk5L+RaD7XpSTpfvXE4H\n\tcd424cko3SaY5tEZg0NRwLTrgQqjUlVpMIIcF+Hk=","Content-Type":"text/plain; charset=\"utf-8\"","MIME-Version":"1.0","Content-Transfer-Encoding":"quoted-printable","In-Reply-To":"<178180198392.1557007.1618335019176518883@ping.linuxembedded.co.uk>","References":"<20260618-rppx1-ipa-v1-0-32337264cfcd@ideasonboard.com>\n\t<20260618-rppx1-ipa-v1-13-32337264cfcd@ideasonboard.com>\n\t<178180198392.1557007.1618335019176518883@ping.linuxembedded.co.uk>","Subject":"Re: [PATCH 13/14] ipa: rppx1: Add AWB algorithm","From":"Kieran Bingham <kieran.bingham@ideasonboard.com>","Cc":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>","To":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>, Niklas =?utf-8?b?U8O2?=\n\t=?utf-8?q?derlund?= <niklas.soderlund+renesas@ragnatech.se>,\n\tlibcamera-devel@lists.libcamera.org","Date":"Thu, 18 Jun 2026 18:02:09 +0100","Message-ID":"<178180212901.1557007.2222308762619446850@ping.linuxembedded.co.uk>","User-Agent":"alot/0.9.1","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}},{"id":39196,"web_url":"https://patchwork.libcamera.org/comment/39196/","msgid":"<20260619211824.GB3631325@ragnatech.se>","date":"2026-06-19T21:18:24","subject":"Re: [PATCH 13/14] ipa: rppx1: Add AWB algorithm","submitter":{"id":230,"url":"https://patchwork.libcamera.org/api/people/230/","name":"Niklas Söderlund","email":"niklas.soderlund+renesas@ragnatech.se"},"content":"Hi Jacopo,\n\nThanks for your work.\n\nOn 2026-06-18 12:18:52 +0200, Jacopo Mondi wrote:\n> Add Awb algorithm to the RPP-X1 IPA.\n> \n> The implementation is based on libIPA AwbAlgorithm.\n> \n> Signed-off-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>\n\nReviewed-by: Niklas Söderlund <niklas.soderlund+renesas@ragnatech.se>\n\n> ---\n>  src/ipa/rppx1/algorithms/awb.cpp     | 298 +++++++++++++++++++++++++++++++++++\n>  src/ipa/rppx1/algorithms/awb.h       |  48 ++++++\n>  src/ipa/rppx1/algorithms/meson.build |   5 +\n>  src/ipa/rppx1/ipa_context.cpp        |  30 ++++\n>  src/ipa/rppx1/ipa_context.h          |  10 ++\n>  src/ipa/rppx1/meson.build            |   4 +\n>  6 files changed, 395 insertions(+)\n> \n> diff --git a/src/ipa/rppx1/algorithms/awb.cpp b/src/ipa/rppx1/algorithms/awb.cpp\n> new file mode 100644\n> index 000000000000..6316606b4b0f\n> --- /dev/null\n> +++ b/src/ipa/rppx1/algorithms/awb.cpp\n> @@ -0,0 +1,298 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2026, Ideas On Board\n> + *\n> + * RPP-X1 AWB control algorithm\n> + */\n> +\n> +\n> +#include \"awb.h\"\n> +\n> +#include <libcamera/base/log.h>\n> +#include <libcamera/internal/vector.h>\n> +\n> +/**\n> + * \\file awb.h\n> + * \\brief RPP-X1 Auto White Balance\n> + */\n> +\n> +namespace libcamera {\n> +\n> +namespace ipa::rppx1::algorithms {\n> +\n> +/* \\todo RPP-X1 doesn't support the Lux algorithm. */\n> +static constexpr unsigned int kDefaultLux = 500;\n> +\n> +LOG_DEFINE_CATEGORY(RppX1Awb)\n> +\n> +namespace {\n> +\n> +/* Quantize a gain to the UQ6.12 register format. */\n> +constexpr uint32_t quantizeGain(double gain)\n> +{\n> +\treturn UQ<6, 12>(static_cast<float>(gain)).quantized();\n> +}\n> +\n> +}; /* namespace */\n> +\n> +/*\n> + * The RPP-X1 ISP has a bus width of 24 bits and all the measurement limits are\n> + * expressed as 24 bit values. In order to make it simpler to express limits\n> + * in code use an 8-bit value and left-shift by 16 to match the 24 bit width of\n> + * the ISP processing pipeline.\n> + */\n> +constexpr unsigned int kRPPX1BusWidthShift = 16;\n> +\n> +class RppX1AwbStats final : public AwbStats\n> +{\n> +public:\n> +\tRppX1AwbStats() {}\n> +\tRppX1AwbStats(const RGB<double> &rgbMeans)\n> +\t\t: AwbStats(rgbMeans)\n> +\t{\n> +\t}\n> +\n> +\t/* Minimum mean value below which AWB can't operate. */\n> +\tdouble minColourValue() const override\n> +\t{\n> +\t\treturn 2.0;\n> +\t}\n> +};\n> +\n> +/**\n> + * \\class Awb\n> + * \\brief RPP-X1 white balance correction algorithm\n> + */\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::init\n> + */\n> +int Awb::init(IPAContext &context, const ValueNode &tuningData)\n> +{\n> +\treturn awbAlgo_.init(tuningData, context.ctrlMap);\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::configure\n> + */\n> +int Awb::configure(IPAContext &context,\n> +\t\t   const IPACameraSensorInfo &configInfo)\n> +{\n> +\tawbAlgo_.configure(context.activeState.awb);\n> +\n> +\t/*\n> +\t * Define the measurement window for AWB as a centered rectangle\n> +\t * covering 3/4 of the image width and height.\n> +\t */\n> +\tcontext.configuration.awb.measureWindow.h_offs = configInfo.outputSize.width / 8;\n> +\tcontext.configuration.awb.measureWindow.v_offs = configInfo.outputSize.height / 8;\n> +\tcontext.configuration.awb.measureWindow.h_size = 3 * configInfo.outputSize.width / 4;\n> +\tcontext.configuration.awb.measureWindow.v_size = 3 * configInfo.outputSize.height / 4;\n> +\n> +\tcontext.configuration.awb.enabled = true;\n> +\n> +\treturn 0;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::queueRequest\n> + */\n> +void Awb::queueRequest(IPAContext &context, const uint32_t frame,\n> +\t\t       IPAFrameContext &frameContext,\n> +\t\t       const ControlList &controls)\n> +{\n> +\tawbAlgo_.queueRequest(context.activeState.awb, frame, frameContext.awb,\n> +\t\t\t      controls);\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::prepare\n> + */\n> +void Awb::prepare(IPAContext &context, const uint32_t frame,\n> +\t\t  IPAFrameContext &frameContext, RppX1Params *params)\n> +{\n> +\tawbAlgo_.prepare(context.activeState.awb, frameContext.awb);\n> +\n> +\tauto gainConfig = params->block<BlockType::AwbgPre1>();\n> +\tgainConfig.setEnabled(true);\n> +\n> +\tgainConfig->gain_green_b = quantizeGain(frameContext.awb.gains.g());\n> +\tgainConfig->gain_blue = quantizeGain(frameContext.awb.gains.b());\n> +\tgainConfig->gain_red = quantizeGain(frameContext.awb.gains.r());\n> +\tgainConfig->gain_green_r = quantizeGain(frameContext.awb.gains.g());\n> +\n> +\tif (frame > 0)\n> +\t\treturn;\n> +\n> +\t/*\n> +\t * If this is the first frame, program the white balance measurement\n> +\t * engine.\n> +\t */\n> +\n> +\tauto awbConfig = params->block<BlockType::WbmeasPost>();\n> +\tawbConfig.setEnabled(true);\n> +\n> +\t/* Configure the measure window for AWB. */\n> +\tawbConfig->wnd = context.configuration.awb.measureWindow;\n> +\n> +\t/* Number of frames to use to estimate the means (0 means 1 frame). */\n> +\tawbConfig->frames = 0;\n> +\n> +\t/* Use YCbCr measurement mode. */\n> +\tawbConfig->mode = RPPX1_WBMEAS_MODE_YCBCR;\n> +\n> +\t/* Set the reference Cr and Cb (AWB target) to white. */\n> +\tawbConfig->ref_cb_max_b = 128 << kRPPX1BusWidthShift;\n> +\tawbConfig->ref_cr_max_r = 128 << kRPPX1BusWidthShift;\n> +\n> +\t/*\n> +\t * Filter out pixels based on luminance and chrominance values.\n> +\t * The acceptable luma values are specified as a [16, 250]\n> +\t * range, while the acceptable chroma values are specified\n> +\t * with a minimum of 16 and a maximum Cb+Cr sum of 250;\n> +\t */\n> +\tawbConfig->min_y_max_g = 16 << kRPPX1BusWidthShift;\n> +\tawbConfig->max_y = 250 << kRPPX1BusWidthShift;\n> +\tawbConfig->min_c = 16 << kRPPX1BusWidthShift;\n> +\tawbConfig->max_csum = 250 << kRPPX1BusWidthShift;\n> +\n> +\t/* \\todo Disable ymax_cmp even if we program 'max_y' */\n> +\tawbConfig->ymax_cmp = 0;\n> +\n> +\t/*\n> +\t * Program coefficients and offsets to perform RGB-to-YCbCr\n> +\t * conversion according to the BT.601 specification for limited\n> +\t * range YUV.\n> +\t *\n> +\t *  Y = 16 + 0.2500 R + 0.5000 G + 0.1094 B\n> +\t *  Cb = 128 - 0.1406 R - 0.2969 G + 0.4375 B\n> +\t *  Cr = 128 + 0.4375 R - 0.3750 G - 0.0625 B\n> +\t *\n> +\t * which resuls in the following register values:\n> +\t *\n> +\t * - coefficients are signed Q4.12 format\n> +\t * - their numerical value is then 'reg / 2^12'\n> +\t * - negative numbers need to reverse the 2's complement\n> +\t *   (!(reg & !BIT(16)) + 1) / 2^12\n> +\t *\n> +\t * coeff G0 0x00000800; = 0.500\n> +\t * coeff B0 0x000001c0; = 0.1094\n> +\t * coeff R0 0x00000400; = 0.2500\n> +\t * coeff G1 0x0000fb40; = -0.2969\n> +\t * coeff B1 0x00000700; = 0.4375\n> +\t * coeff R1 0x0000fdc0; = -0.1406\n> +\t * coeff G2 0x0000fa00; = -0.3750\n> +\t * coeff B2 0x0000ff00; = -0.0625\n> +\t * coeff R2 0x00000700; = 0.4375\n> +\t * offset R 0x00100000; offset_r = (16 << 16)\n> +\t * offset G 0x00800000; offset_g = (128 << 16)\n> +\t * offset B 0x00800000; offset_b = (128 << 16)\n> +\t *\n> +\t * Use the inverse of this matrix in calculateRgbMeans() to\n> +\t * reverse the colorspace conversion.\n> +\t */\n> +\tawbConfig->ccor_coeff[0][0] = 0x00000800;\n> +\tawbConfig->ccor_coeff[0][1] = 0x000001c0;\n> +\tawbConfig->ccor_coeff[0][2] = 0x00000400;\n> +\tawbConfig->ccor_coeff[1][0] = 0x0000fb40;\n> +\tawbConfig->ccor_coeff[1][1] = 0x00000700;\n> +\tawbConfig->ccor_coeff[1][2] = 0x0000fdc0;\n> +\tawbConfig->ccor_coeff[2][0] = 0x0000fa00;\n> +\tawbConfig->ccor_coeff[2][1] = 0x0000ff00;\n> +\tawbConfig->ccor_coeff[2][2] = 0x00000700;\n> +\n> +\tawbConfig->ccor_offs[0] = 16 << kRPPX1BusWidthShift;\n> +\tawbConfig->ccor_offs[1] = 128 << kRPPX1BusWidthShift;\n> +\tawbConfig->ccor_offs[2] = 128 << kRPPX1BusWidthShift;\n> +}\n> +\n> +/**\n> + * \\copydoc libcamera::ipa::Algorithm::process\n> + */\n> +void Awb::process(IPAContext &context,\n> +\t\t  [[maybe_unused]] const uint32_t frame,\n> +\t\t  IPAFrameContext &frameContext,\n> +\t\t  const RppX1Stats *stats,\n> +\t\t  ControlList &metadata)\n> +{\n> +\tRppX1AwbStats awbStats = calculateRgbMeans(frameContext, stats);\n> +\n> +\tawbAlgo_.process(context.activeState.awb, frameContext.awb, awbStats,\n> +\t\t\t kDefaultLux, metadata);\n> +}\n> +\n> +RppX1AwbStats Awb::calculateRgbMeans(const IPAFrameContext &frameContext,\n> +\t\t\t\t     const RppX1Stats *stats) const\n> +{\n> +\tif (!stats)\n> +\t\treturn {};\n> +\n> +\tconst auto awb = stats->block<StatsType::WbmeasPost>();\n> +\tif (!awb)\n> +\t\treturn {};\n> +\n> +\tif (awb->cnt == 0) {\n> +\t\tLOG(RppX1Awb, Debug) << \"AWB statistics are empty\";\n> +\t\treturn {};\n> +\t}\n> +\n> +\tVector<double, 3> rgbMeans;\n> +\n> +\t/* Get the YCbCr mean values: we use YCbCr measurement mode. */\n> +\tVector<double, 3> yuvMeans({\n> +\t\tstatic_cast<double>(awb->mean_y_or_g),\n> +\t\tstatic_cast<double>(awb->mean_cb_or_b),\n> +\t\tstatic_cast<double>(awb->mean_cr_or_r)\n> +\t});\n> +\n> +\t/*\n> +\t * Convert from YCbCr to RGB. The statistics engine has been\n> +\t * programmed with the following matrix:\n> +\t *\n> +\t * Y  =  16 + 0.2500 R + 0.5000 G + 0.1094 B\n> +\t * Cb = 128 - 0.1406 R - 0.2969 G + 0.4375 B\n> +\t * Cr = 128 + 0.4375 R - 0.3750 G - 0.0625 B\n> +\t *\n> +\t * Use the inverse matrix here.\n> +\t */\n> +\tstatic const Matrix<double, 3, 3> yuv2rgbMatrix({\n> +\t\t1.1636, -0.0623,  1.6008,\n> +\t\t1.1636, -0.4045, -0.7949,\n> +\t\t1.1636,  1.9912, -0.0250\n> +\t});\n> +\tstatic const Vector<double, 3> yuv2rgbOffset({\n> +\t\t16, 128, 128\n> +\t});\n> +\n> +\trgbMeans = yuv2rgbMatrix * (yuvMeans - yuv2rgbOffset);\n> +\n> +\t/*\n> +\t * Due to hardware rounding errors in the YCbCr means, the\n> +\t * calculated RGB means may be negative. This would lead to\n> +\t * negative gains, messing up calculation. Prevent this by\n> +\t * clamping the means to positive values.\n> +\t */\n> +\trgbMeans = rgbMeans.max(0.0);\n> +\n> +\t/*\n> +\t * \\todo\n> +\t * The ISP computes the AWB means after applying the CCM. Apply\n> +\t * the inverse as we want to get the raw means before the colour gains.\n> +\t * rgbMeans = frameContext.ccm.ccm.inverse() * rgbMeans;\n> +\t */\n> +\n> +\t/*\n> +\t * The ISP computes the AWB means after applying the colour gains,\n> +\t * divide by the gains that were used to get the raw means from the\n> +\t * sensor. Apply a minimum value to avoid divisions by near-zero.\n> +\t */\n> +\trgbMeans /= frameContext.awb.gains.max(0.01);\n> +\n> +\treturn RppX1AwbStats(rgbMeans);\n> +}\n> +\n> +REGISTER_IPA_ALGORITHM(Awb, \"Awb\")\n> +\n> +} /* namespace ipa::rppx1::algorithms */\n> +\n> +} /* namespace libcamera */\n> diff --git a/src/ipa/rppx1/algorithms/awb.h b/src/ipa/rppx1/algorithms/awb.h\n> new file mode 100644\n> index 000000000000..2e4f67f11bc0\n> --- /dev/null\n> +++ b/src/ipa/rppx1/algorithms/awb.h\n> @@ -0,0 +1,48 @@\n> +/* SPDX-License-Identifier: LGPL-2.1-or-later */\n> +/*\n> + * Copyright (C) 2026, Ideas On Board\n> + *\n> + * AWB control algorithm\n> + */\n> +\n> +#pragma once\n> +\n> +#include \"libipa/awb.h\"\n> +#include \"libipa/fixedpoint.h\"\n> +\n> +#include \"algorithm.h\"\n> +\n> +namespace libcamera {\n> +\n> +namespace ipa::rppx1::algorithms {\n> +\n> +class RppX1AwbStats;\n> +\n> +class Awb : public Algorithm\n> +{\n> +public:\n> +\tAwb() = default;\n> +\t~Awb() = default;\n> +\n> +\tint init(IPAContext &context, const ValueNode &tuningData) override;\n> +\tint configure(IPAContext &context, const IPACameraSensorInfo &configInfo) override;\n> +\tvoid queueRequest(IPAContext &context, const uint32_t frame,\n> +\t\t\t  IPAFrameContext &frameContext,\n> +\t\t\t  const ControlList &controls) override;\n> +\tvoid prepare(IPAContext &context, const uint32_t frame,\n> +\t\t     IPAFrameContext &frameContext,\n> +\t\t     RppX1Params *params) override;\n> +\tvoid process(IPAContext &context, const uint32_t frame,\n> +\t\t     IPAFrameContext &frameContext,\n> +\t\t     const RppX1Stats *stats,\n> +\t\t     ControlList &metadata) override;\n> +\n> +private:\n> +\tRppX1AwbStats calculateRgbMeans(const IPAFrameContext &frameContext,\n> +\t\t\t\t\tconst RppX1Stats *stats) const;\n> +\n> +\tAwbAlgorithm<UQ<6, 12>> awbAlgo_;\n> +};\n> +\n> +} /* namespace ipa::rppx1::algorithms */\n> +} /* namespace libcamera */\n> diff --git a/src/ipa/rppx1/algorithms/meson.build b/src/ipa/rppx1/algorithms/meson.build\n> new file mode 100644\n> index 000000000000..70368ad9bac9\n> --- /dev/null\n> +++ b/src/ipa/rppx1/algorithms/meson.build\n> @@ -0,0 +1,5 @@\n> +# SPDX-License-Identifier: CC0-1.0\n> +\n> +rppx1_ipa_algorithms = files([\n> +    'awb.cpp',\n> +])\n> diff --git a/src/ipa/rppx1/ipa_context.cpp b/src/ipa/rppx1/ipa_context.cpp\n> index 5ac60dfc7bf6..ea7008f5685d 100644\n> --- a/src/ipa/rppx1/ipa_context.cpp\n> +++ b/src/ipa/rppx1/ipa_context.cpp\n> @@ -16,21 +16,51 @@\n>  \n>  namespace libcamera::ipa::rppx1 {\n>  \n> +/**\n> + * \\struct RppX1AwbSession\n> + * \\brief RPP-X1 Awb session configuration\n> + */\n> +\n> +/**\n> + * \\var RppX1AwbSession::measureWindow\n> + * \\brief Awb measurement window\n> + */\n> +\n> +/**\n> + * \\var RppX1AwbSession::enabled\n> + * \\brief Awb enabled flag\n> + */\n> +\n>  /**\n>   * \\struct IPASessionConfiguration\n>   * \\brief Session configuration for the IPA module\n>   */\n>  \n> +/**\n> + * \\var IPASessionConfiguration::awb\n> + * \\brief Awb session configuration\n> + */\n> +\n>  /**\n>   * \\struct IPAActiveState\n>   * \\brief Active state for algorithms\n>   */\n>  \n> +/**\n> + * \\var IPAActiveState::awb\n> + * \\copydoc ipa::awb::ActiveState\n> + */\n> +\n>  /**\n>   * \\struct IPAFrameContext\n>   * \\brief Per-frame context for algorithms\n>   */\n>  \n> +/**\n> + * \\var IPAFrameContext::awb\n> + * \\copydoc ipa::awb::FrameContext\n> + */\n> +\n>  /**\n>   * \\struct IPAContext\n>   * \\brief Global IPA context data shared between all algorithms\n> diff --git a/src/ipa/rppx1/ipa_context.h b/src/ipa/rppx1/ipa_context.h\n> index f268a35081bc..57197d865d3f 100644\n> --- a/src/ipa/rppx1/ipa_context.h\n> +++ b/src/ipa/rppx1/ipa_context.h\n> @@ -20,17 +20,27 @@\n>  #include <libipa/camera_sensor_helper.h>\n>  #include <libipa/fc_queue.h>\n>  \n> +#include \"libipa/awb.h\"\n> +\n>  namespace libcamera {\n>  \n>  namespace ipa::rppx1 {\n>  \n> +struct RppX1AwbSession {\n> +\tstruct rppx1_window measureWindow;\n> +\tbool enabled;\n> +};\n> +\n>  struct IPASessionConfiguration {\n> +\tstruct RppX1AwbSession awb;\n>  };\n>  \n>  struct IPAActiveState {\n> +\tipa::awb::ActiveState awb;\n>  };\n>  \n>  struct IPAFrameContext : public FrameContext {\n> +\tipa::awb::FrameContext awb;\n>  };\n>  \n>  struct IPAContext {\n> diff --git a/src/ipa/rppx1/meson.build b/src/ipa/rppx1/meson.build\n> index 8034fe24d241..8b8c79e997c1 100644\n> --- a/src/ipa/rppx1/meson.build\n> +++ b/src/ipa/rppx1/meson.build\n> @@ -1,4 +1,6 @@\n>  # SPDX-License-Identifier: CC0-1.0\n> +#\n> +subdir('algorithms')\n>  \n>  ipa_name = 'ipa_rppx1'\n>  \n> @@ -7,6 +9,8 @@ rppx1_ipa_sources = files([\n>      'rppx1.cpp',\n>  ])\n>  \n> +rppx1_ipa_sources += rppx1_ipa_algorithms\n> +\n>  mod = shared_module(ipa_name, rppx1_ipa_sources,\n>                      name_prefix : '',\n>                      include_directories : [ipa_includes],\n> \n> -- \n> 2.54.0\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 DBE2BC328C\n\tfor <parsemail@patchwork.libcamera.org>;\n\tFri, 19 Jun 2026 21:18:31 +0000 (UTC)","from lancelot.ideasonboard.com (localhost [IPv6:::1])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTP id DBFE6656D6;\n\tFri, 19 Jun 2026 23:18:30 +0200 (CEST)","from fhigh-a8-smtp.messagingengine.com\n\t(fhigh-a8-smtp.messagingengine.com [103.168.172.159])\n\tby lancelot.ideasonboard.com (Postfix) with ESMTPS id 6EE5B62899\n\tfor <libcamera-devel@lists.libcamera.org>;\n\tFri, 19 Jun 2026 23:18:28 +0200 (CEST)","from phl-compute-01.internal (phl-compute-01.internal\n\t[10.202.2.41])\n\tby mailfhigh.phl.internal (Postfix) with ESMTP id 617A91400167;\n\tFri, 19 Jun 2026 17:18:27 -0400 (EDT)","from phl-frontend-04 ([10.202.2.163])\n\tby phl-compute-01.internal (MEProxy); Fri, 19 Jun 2026 17:18:27 -0400","by mail.messagingengine.com (Postfix) with ESMTPA; Fri,\n\t19 Jun 2026 17:18:26 -0400 (EDT)"],"Authentication-Results":"lancelot.ideasonboard.com; dkim=pass (2048-bit key;\n\tunprotected) header.d=ragnatech.se header.i=@ragnatech.se\n\theader.b=\"hKEBTEOQ\"; dkim=pass (2048-bit key;\n\tunprotected) header.d=messagingengine.com\n\theader.i=@messagingengine.com header.b=\"Hw4rXZz6\"; \n\tdkim-atps=neutral","DKIM-Signature":["v=1; a=rsa-sha256; c=relaxed/relaxed; d=ragnatech.se; h=\n\tcc:cc:content-transfer-encoding:content-type:content-type:date\n\t:date:from:from:in-reply-to:in-reply-to:message-id:mime-version\n\t:references:reply-to:subject:subject:to:to; s=fm3; t=1781903907;\n\tx=1781990307; bh=pgj6p+BUUVO8So2o//g/PLQ14DfT49lErJUzw/5+h0k=; b=\n\thKEBTEOQgwrxSY/tsaQVro/c1BCOFPtt2xGB0FUuhqzXeWmizNaAgyz0wJjNDgWe\n\toO4liMb2iLf/jkxPABaZToWNvp8C2qmh839zJMzUO0JSBmY+o5JNNTw3obayc3KR\n\tBBmgDJgahbkUDJEYsXOCDvKnxpNJ/iyMc0rgD0U8TusQn0ouXkESuJ3A9xmea2Lo\n\t9L7AtMiY+5NN1hVrlLJwAusXS6+1dPRmRGWreSfYbR48dcqAydCWD5gOdstscYum\n\twef8rBLvH8YYNvI46XO0dGCDwgvKtA5e620LYer5+dZGY+MG4LW6UNCPcSTXkC7J\n\tCG1gRlT5mjSBTK4DlkBe0A==","v=1; a=rsa-sha256; c=relaxed/relaxed; d=\n\tmessagingengine.com; h=cc:cc:content-transfer-encoding\n\t:content-type:content-type:date:date:feedback-id:feedback-id\n\t:from:from:in-reply-to:in-reply-to:message-id:mime-version\n\t:references:reply-to:subject:subject:to:to:x-me-proxy\n\t:x-me-sender:x-me-sender:x-sasl-enc; s=fm1; t=1781903907; x=\n\t1781990307; bh=pgj6p+BUUVO8So2o//g/PLQ14DfT49lErJUzw/5+h0k=; b=H\n\tw4rXZz61UWbR621IgqiUJhTePRhJAMZ8+9kI7bUnzH0U0U3kWFGp4k3C1oVHedi5\n\tJL5RWRdb3U9GRxRIwnK6009HnREfVnT0mk3Endb908QI9qUu4T9X/s30VwEQ9Fnt\n\tsVjpTT1a4eF4h3lOzBcDDsQSqTTkPejcP52qUhXaFKeT8q9JqewsVQzhgfZevX2K\n\tMSlEKWY94zI7MeQV4ZkRDwH2tYuxwhkI17m6TyyrNziGZF5tVYzKquLO0sKX0gRt\n\t8aMMfz+CsG1FxcXfgLvhpdULRI5IaRSXXamIQ6k9iR+Fqvq3NMA/6wCbqXRpLnM1\n\tVXrMRPzDNXnerkZq4cIIA=="],"X-ME-Sender":"<xms:IrI1asGFhCd9QsR_MCnk2ciFtGEaG_Fi4X6kCo06rNVt3WxidI9vpw>\n\t<xme:IrI1arX4F2j3PYBN9jc1113vIAetJtqTfZANib_ruC2122ZQYGqz2igVRoeeGrZkr\n\ttW-_HUVbBKIvLhspgzekXCFL1j5XjTpq9R6G248SOd-c1ybNrM>","X-ME-Received":"<xmr:IrI1ajx5zEzC7Wee3vPeBwJ7ge4DF_A1GdS1FT3SAIjpX8o1Kbu-IiUwAnJVQprDOatRVN8iN5HzPsw7KWjAYxh79cLq>","X-ME-Proxy-Cause":"dmFkZTEbV0Y5uy4XqRt9hhduKTjRruODgl8l15pHKE7tWtoudzG/wNC72nIcpeoFjd2apM\n\tvTz8+9HCE32e9pjKVfGfELeHWImwjIZOuO/+sdVkOjREZNOHUWTBXgJsiw0wtRXrztj76y\n\ttH2FWRLtfDTQrIplaTg1zq2dQa8+WeJ3GTCb812RpqBTs9JtIdeIWTfYco0YqF1gO7rMJ7\n\tCpliW7oNHhuZwa7bWKukcQv48oEGAlCUusFd1PjtmYGI81uKvpbG+VQ7z4hQDYphralmxR\n\tdqPUo7poyDmjbOU21BB6JY1w2GdMZsCfxXz4HdtfhswvSvgt7MZptUCXW9+JFwCw4P/rBO\n\tyw5AdXBXmYzp0B0KhGW77R7n4U6RewLuqIMl4NMn0IIVN1D9cJ4S+EUfZwLRXrS9CgYgIs\n\tG3aCSvw7HKh9Po6bVwaX7MgszoPfRWFZFnxxBtazoMqsz487mfC0bXN5V1j0XqZU1AbCFk\n\tVCOX0jUctgc79OniQINkrxWEE7zoFS+Jw9N3e6vYQ2KMXloe41aFWVs2bpd4GQ+3wXDle8\n\tDkugR8uxE+O5f5cAVt+D5iGlQz9OV5P4SLRGqh0n5AoR97VUq5zf5Qlg43bHl8EOJU9s2i\n\tfivrkO2G9iZCx2+CsZtR224d9X0BuEUNslxh72B5aTpz6c2Zbz3RTMydmqeQ","X-ME-Proxy":"<xmx:IrI1apPPJVtnpeE57pQ76UpgJhHo880FWP0YHdYQQAWyyrmjRg5zZQ>\n\t<xmx:IrI1ak7x4YaqzdSHk05WnEtiiRot7ElO8oOoFbpnbTcZgCR6mwx9bg>\n\t<xmx:IrI1aqO9fyKggKNS8jhda1GCwxL7QKEatcKrqA-E_Nmhag5_yl_UJg>\n\t<xmx:IrI1ainjnHz6zjHrmS5p4AD2WvPvnKlCztL-W3YeihS4egekX2-f2w>\n\t<xmx:I7I1auZ4P9A2-gznfBnn4EBEFWPmbtlpcEYnu-xH-rVHpAwPcOoHYNtB>","Feedback-ID":"i80c9496c:Fastmail","Date":"Fri, 19 Jun 2026 23:18:24 +0200","From":"Niklas =?utf-8?q?S=C3=B6derlund?=\n\t<niklas.soderlund+renesas@ragnatech.se>","To":"Jacopo Mondi <jacopo.mondi@ideasonboard.com>","Cc":"libcamera-devel@lists.libcamera.org","Subject":"Re: [PATCH 13/14] ipa: rppx1: Add AWB algorithm","Message-ID":"<20260619211824.GB3631325@ragnatech.se>","References":"<20260618-rppx1-ipa-v1-0-32337264cfcd@ideasonboard.com>\n\t<20260618-rppx1-ipa-v1-13-32337264cfcd@ideasonboard.com>","MIME-Version":"1.0","Content-Type":"text/plain; charset=utf-8","Content-Disposition":"inline","Content-Transfer-Encoding":"8bit","In-Reply-To":"<20260618-rppx1-ipa-v1-13-32337264cfcd@ideasonboard.com>","X-BeenThere":"libcamera-devel@lists.libcamera.org","X-Mailman-Version":"2.1.29","Precedence":"list","List-Id":"<libcamera-devel.lists.libcamera.org>","List-Unsubscribe":"<https://lists.libcamera.org/options/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=unsubscribe>","List-Archive":"<https://lists.libcamera.org/pipermail/libcamera-devel/>","List-Post":"<mailto:libcamera-devel@lists.libcamera.org>","List-Help":"<mailto:libcamera-devel-request@lists.libcamera.org?subject=help>","List-Subscribe":"<https://lists.libcamera.org/listinfo/libcamera-devel>,\n\t<mailto:libcamera-devel-request@lists.libcamera.org?subject=subscribe>","Errors-To":"libcamera-devel-bounces@lists.libcamera.org","Sender":"\"libcamera-devel\" <libcamera-devel-bounces@lists.libcamera.org>"}}]