From 726e9274ea95fa46352556d340c5793a8da51fcd Mon Sep 17 00:00:00 2001
From: Naushir Patuck <naush@raspberrypi.com>
Date: Wed, 3 May 2023 13:20:27 +0100
Subject: pipeline: ipa: raspberrypi: Refactor and move the Raspberry Pi code

Split the Raspberry Pi pipeline handler and IPA source code into common
and VC4/BCM2835 specific file structures.

For the pipeline handler, the common code files now live in
src/libcamera/pipeline/rpi/common/
and the VC4-specific files in src/libcamera/pipeline/rpi/vc4/.

For the IPA, the common code files now live in
src/ipa/rpi/{cam_helper,controller}/
and the vc4 specific files in src/ipa/rpi/vc4/. With this change, the
camera tuning files are now installed under share/libcamera/ipa/rpi/vc4/.

To build the pipeline and IPA, the meson configuration options have now
changed from "raspberrypi" to "rpi/vc4":

meson setup build -Dipas=rpi/vc4 -Dpipelines=rpi/vc4

Signed-off-by: Naushir Patuck <naush@raspberrypi.com>
Reviewed-by: Jacopo Mondi <jacopo.mondi@ideasonboard.com>
Reviewed-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
Signed-off-by: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
---
 src/ipa/rpi/README.md                        |   25 +
 src/ipa/rpi/cam_helper/cam_helper.cpp        |  265 ++++
 src/ipa/rpi/cam_helper/cam_helper.h          |  132 ++
 src/ipa/rpi/cam_helper/cam_helper_imx219.cpp |  115 ++
 src/ipa/rpi/cam_helper/cam_helper_imx290.cpp |   68 +
 src/ipa/rpi/cam_helper/cam_helper_imx296.cpp |   83 ++
 src/ipa/rpi/cam_helper/cam_helper_imx477.cpp |  197 +++
 src/ipa/rpi/cam_helper/cam_helper_imx519.cpp |  196 +++
 src/ipa/rpi/cam_helper/cam_helper_imx708.cpp |  359 +++++
 src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp |  109 ++
 src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp |   66 +
 src/ipa/rpi/cam_helper/md_parser.h           |  155 +++
 src/ipa/rpi/cam_helper/md_parser_smia.cpp    |  149 +++
 src/ipa/rpi/cam_helper/meson.build           |   26 +
 src/ipa/rpi/controller/af_algorithm.h        |   76 ++
 src/ipa/rpi/controller/af_status.h           |   35 +
 src/ipa/rpi/controller/agc_algorithm.h       |   33 +
 src/ipa/rpi/controller/agc_status.h          |   37 +
 src/ipa/rpi/controller/algorithm.cpp         |   56 +
 src/ipa/rpi/controller/algorithm.h           |   68 +
 src/ipa/rpi/controller/alsc_status.h         |   22 +
 src/ipa/rpi/controller/awb_algorithm.h       |   25 +
 src/ipa/rpi/controller/awb_status.h          |   20 +
 src/ipa/rpi/controller/black_level_status.h  |   15 +
 src/ipa/rpi/controller/camera_mode.h         |   59 +
 src/ipa/rpi/controller/ccm_algorithm.h       |   21 +
 src/ipa/rpi/controller/ccm_status.h          |   14 +
 src/ipa/rpi/controller/contrast_algorithm.h  |   22 +
 src/ipa/rpi/controller/contrast_status.h     |   20 +
 src/ipa/rpi/controller/controller.cpp        |  181 +++
 src/ipa/rpi/controller/controller.h          |   73 +
 src/ipa/rpi/controller/denoise_algorithm.h   |   23 +
 src/ipa/rpi/controller/denoise_status.h      |   16 +
 src/ipa/rpi/controller/device_status.cpp     |   31 +
 src/ipa/rpi/controller/device_status.h       |   43 +
 src/ipa/rpi/controller/dpc_status.h          |   13 +
 src/ipa/rpi/controller/geq_status.h          |   14 +
 src/ipa/rpi/controller/histogram.cpp         |   64 +
 src/ipa/rpi/controller/histogram.h           |   53 +
 src/ipa/rpi/controller/lux_status.h          |   23 +
 src/ipa/rpi/controller/meson.build           |   29 +
 src/ipa/rpi/controller/metadata.h            |  126 ++
 src/ipa/rpi/controller/noise_status.h        |   14 +
 src/ipa/rpi/controller/pdaf_data.h           |   24 +
 src/ipa/rpi/controller/pwl.cpp               |  269 ++++
 src/ipa/rpi/controller/pwl.h                 |  127 ++
 src/ipa/rpi/controller/region_stats.h        |  123 ++
 src/ipa/rpi/controller/rpi/af.cpp            |  797 +++++++++++
 src/ipa/rpi/controller/rpi/af.h              |  165 +++
 src/ipa/rpi/controller/rpi/agc.cpp           |  922 +++++++++++++
 src/ipa/rpi/controller/rpi/agc.h             |  133 ++
 src/ipa/rpi/controller/rpi/alsc.cpp          |  865 ++++++++++++
 src/ipa/rpi/controller/rpi/alsc.h            |  174 +++
 src/ipa/rpi/controller/rpi/awb.cpp           |  734 ++++++++++
 src/ipa/rpi/controller/rpi/awb.h             |  191 +++
 src/ipa/rpi/controller/rpi/black_level.cpp   |   66 +
 src/ipa/rpi/controller/rpi/black_level.h     |   30 +
 src/ipa/rpi/controller/rpi/ccm.cpp           |  199 +++
 src/ipa/rpi/controller/rpi/ccm.h             |   75 ++
 src/ipa/rpi/controller/rpi/contrast.cpp      |  181 +++
 src/ipa/rpi/controller/rpi/contrast.h        |   51 +
 src/ipa/rpi/controller/rpi/dpc.cpp           |   59 +
 src/ipa/rpi/controller/rpi/dpc.h             |   32 +
 src/ipa/rpi/controller/rpi/focus.h           |   28 +
 src/ipa/rpi/controller/rpi/geq.cpp           |   89 ++
 src/ipa/rpi/controller/rpi/geq.h             |   34 +
 src/ipa/rpi/controller/rpi/lux.cpp           |  115 ++
 src/ipa/rpi/controller/rpi/lux.h             |   45 +
 src/ipa/rpi/controller/rpi/noise.cpp         |   89 ++
 src/ipa/rpi/controller/rpi/noise.h           |   32 +
 src/ipa/rpi/controller/rpi/sdn.cpp           |   80 ++
 src/ipa/rpi/controller/rpi/sdn.h             |   32 +
 src/ipa/rpi/controller/rpi/sharpen.cpp       |   92 ++
 src/ipa/rpi/controller/rpi/sharpen.h         |   34 +
 src/ipa/rpi/controller/sharpen_algorithm.h   |   21 +
 src/ipa/rpi/controller/sharpen_status.h      |   20 +
 src/ipa/rpi/controller/statistics.h          |   78 ++
 src/ipa/rpi/meson.build                      |   13 +
 src/ipa/rpi/vc4/data/imx219.json             |  486 +++++++
 src/ipa/rpi/vc4/data/imx219_noir.json        |  402 ++++++
 src/ipa/rpi/vc4/data/imx290.json             |  200 +++
 src/ipa/rpi/vc4/data/imx296.json             |  537 ++++++++
 src/ipa/rpi/vc4/data/imx296_mono.json        |  233 ++++
 src/ipa/rpi/vc4/data/imx378.json             |  413 ++++++
 src/ipa/rpi/vc4/data/imx477.json             |  518 +++++++
 src/ipa/rpi/vc4/data/imx477_noir.json        |  429 ++++++
 src/ipa/rpi/vc4/data/imx477_scientific.json  |  479 +++++++
 src/ipa/rpi/vc4/data/imx477_v1.json          |  516 +++++++
 src/ipa/rpi/vc4/data/imx519.json             |  413 ++++++
 src/ipa/rpi/vc4/data/imx708.json             |  556 ++++++++
 src/ipa/rpi/vc4/data/imx708_noir.json        |  556 ++++++++
 src/ipa/rpi/vc4/data/imx708_wide.json        |  459 +++++++
 src/ipa/rpi/vc4/data/imx708_wide_noir.json   |  459 +++++++
 src/ipa/rpi/vc4/data/meson.build             |   26 +
 src/ipa/rpi/vc4/data/ov5647.json             |  487 +++++++
 src/ipa/rpi/vc4/data/ov5647_noir.json        |  403 ++++++
 src/ipa/rpi/vc4/data/ov9281_mono.json        |  123 ++
 src/ipa/rpi/vc4/data/se327m12.json           |  418 ++++++
 src/ipa/rpi/vc4/data/uncalibrated.json       |  118 ++
 src/ipa/rpi/vc4/meson.build                  |   47 +
 src/ipa/rpi/vc4/raspberrypi.cpp              | 1853 ++++++++++++++++++++++++++
 101 files changed, 19321 insertions(+)
 create mode 100644 src/ipa/rpi/README.md
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper.cpp
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper.h
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper_imx219.cpp
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper_imx290.cpp
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper_imx296.cpp
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper_imx477.cpp
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper_imx519.cpp
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper_imx708.cpp
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp
 create mode 100644 src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp
 create mode 100644 src/ipa/rpi/cam_helper/md_parser.h
 create mode 100644 src/ipa/rpi/cam_helper/md_parser_smia.cpp
 create mode 100644 src/ipa/rpi/cam_helper/meson.build
 create mode 100644 src/ipa/rpi/controller/af_algorithm.h
 create mode 100644 src/ipa/rpi/controller/af_status.h
 create mode 100644 src/ipa/rpi/controller/agc_algorithm.h
 create mode 100644 src/ipa/rpi/controller/agc_status.h
 create mode 100644 src/ipa/rpi/controller/algorithm.cpp
 create mode 100644 src/ipa/rpi/controller/algorithm.h
 create mode 100644 src/ipa/rpi/controller/alsc_status.h
 create mode 100644 src/ipa/rpi/controller/awb_algorithm.h
 create mode 100644 src/ipa/rpi/controller/awb_status.h
 create mode 100644 src/ipa/rpi/controller/black_level_status.h
 create mode 100644 src/ipa/rpi/controller/camera_mode.h
 create mode 100644 src/ipa/rpi/controller/ccm_algorithm.h
 create mode 100644 src/ipa/rpi/controller/ccm_status.h
 create mode 100644 src/ipa/rpi/controller/contrast_algorithm.h
 create mode 100644 src/ipa/rpi/controller/contrast_status.h
 create mode 100644 src/ipa/rpi/controller/controller.cpp
 create mode 100644 src/ipa/rpi/controller/controller.h
 create mode 100644 src/ipa/rpi/controller/denoise_algorithm.h
 create mode 100644 src/ipa/rpi/controller/denoise_status.h
 create mode 100644 src/ipa/rpi/controller/device_status.cpp
 create mode 100644 src/ipa/rpi/controller/device_status.h
 create mode 100644 src/ipa/rpi/controller/dpc_status.h
 create mode 100644 src/ipa/rpi/controller/geq_status.h
 create mode 100644 src/ipa/rpi/controller/histogram.cpp
 create mode 100644 src/ipa/rpi/controller/histogram.h
 create mode 100644 src/ipa/rpi/controller/lux_status.h
 create mode 100644 src/ipa/rpi/controller/meson.build
 create mode 100644 src/ipa/rpi/controller/metadata.h
 create mode 100644 src/ipa/rpi/controller/noise_status.h
 create mode 100644 src/ipa/rpi/controller/pdaf_data.h
 create mode 100644 src/ipa/rpi/controller/pwl.cpp
 create mode 100644 src/ipa/rpi/controller/pwl.h
 create mode 100644 src/ipa/rpi/controller/region_stats.h
 create mode 100644 src/ipa/rpi/controller/rpi/af.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/af.h
 create mode 100644 src/ipa/rpi/controller/rpi/agc.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/agc.h
 create mode 100644 src/ipa/rpi/controller/rpi/alsc.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/alsc.h
 create mode 100644 src/ipa/rpi/controller/rpi/awb.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/awb.h
 create mode 100644 src/ipa/rpi/controller/rpi/black_level.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/black_level.h
 create mode 100644 src/ipa/rpi/controller/rpi/ccm.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/ccm.h
 create mode 100644 src/ipa/rpi/controller/rpi/contrast.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/contrast.h
 create mode 100644 src/ipa/rpi/controller/rpi/dpc.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/dpc.h
 create mode 100644 src/ipa/rpi/controller/rpi/focus.h
 create mode 100644 src/ipa/rpi/controller/rpi/geq.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/geq.h
 create mode 100644 src/ipa/rpi/controller/rpi/lux.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/lux.h
 create mode 100644 src/ipa/rpi/controller/rpi/noise.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/noise.h
 create mode 100644 src/ipa/rpi/controller/rpi/sdn.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/sdn.h
 create mode 100644 src/ipa/rpi/controller/rpi/sharpen.cpp
 create mode 100644 src/ipa/rpi/controller/rpi/sharpen.h
 create mode 100644 src/ipa/rpi/controller/sharpen_algorithm.h
 create mode 100644 src/ipa/rpi/controller/sharpen_status.h
 create mode 100644 src/ipa/rpi/controller/statistics.h
 create mode 100644 src/ipa/rpi/meson.build
 create mode 100644 src/ipa/rpi/vc4/data/imx219.json
 create mode 100644 src/ipa/rpi/vc4/data/imx219_noir.json
 create mode 100644 src/ipa/rpi/vc4/data/imx290.json
 create mode 100644 src/ipa/rpi/vc4/data/imx296.json
 create mode 100644 src/ipa/rpi/vc4/data/imx296_mono.json
 create mode 100644 src/ipa/rpi/vc4/data/imx378.json
 create mode 100644 src/ipa/rpi/vc4/data/imx477.json
 create mode 100644 src/ipa/rpi/vc4/data/imx477_noir.json
 create mode 100644 src/ipa/rpi/vc4/data/imx477_scientific.json
 create mode 100644 src/ipa/rpi/vc4/data/imx477_v1.json
 create mode 100644 src/ipa/rpi/vc4/data/imx519.json
 create mode 100644 src/ipa/rpi/vc4/data/imx708.json
 create mode 100644 src/ipa/rpi/vc4/data/imx708_noir.json
 create mode 100644 src/ipa/rpi/vc4/data/imx708_wide.json
 create mode 100644 src/ipa/rpi/vc4/data/imx708_wide_noir.json
 create mode 100644 src/ipa/rpi/vc4/data/meson.build
 create mode 100644 src/ipa/rpi/vc4/data/ov5647.json
 create mode 100644 src/ipa/rpi/vc4/data/ov5647_noir.json
 create mode 100644 src/ipa/rpi/vc4/data/ov9281_mono.json
 create mode 100644 src/ipa/rpi/vc4/data/se327m12.json
 create mode 100644 src/ipa/rpi/vc4/data/uncalibrated.json
 create mode 100644 src/ipa/rpi/vc4/meson.build
 create mode 100644 src/ipa/rpi/vc4/raspberrypi.cpp

(limited to 'src/ipa/rpi')

diff --git a/src/ipa/rpi/README.md b/src/ipa/rpi/README.md
new file mode 100644
index 00000000..94a8ccc8
--- /dev/null
+++ b/src/ipa/rpi/README.md
@@ -0,0 +1,25 @@
+.. SPDX-License-Identifier: BSD-2-Clause
+
+# _libcamera_ for the Raspberry Pi
+
+Raspberry Pi provides a fully featured pipeline handler and control algorithms
+(IPAs, or "Image Processing Algorithms") to work with _libcamera_. Support is
+included for all existing Raspberry Pi camera modules.
+
+_libcamera_ for the Raspberry Pi allows users to:
+
+1. Use their existing Raspberry Pi cameras.
+1. Change the tuning of the image processing for their Raspberry Pi cameras.
+1. Alter or amend the control algorithms (such as AGC/AEC, AWB or any others)
+   that control the sensor and ISP.
+1. Implement their own custom control algorithms.
+1. Supply new tunings and/or algorithms for completely new sensors.
+
+## How to install and run _libcamera_ on the Raspberry Pi
+
+Please follow the instructions [here](https://www.raspberrypi.com/documentation/accessories/camera.html).
+
+## Documentation
+
+Full documentation for the _Raspberry Pi Camera Algorithm and Tuning Guide_ can
+be found [here](https://datasheets.raspberrypi.com/camera/raspberry-pi-camera-guide.pdf).
diff --git a/src/ipa/rpi/cam_helper/cam_helper.cpp b/src/ipa/rpi/cam_helper/cam_helper.cpp
new file mode 100644
index 00000000..ddd5e9a4
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper.cpp
@@ -0,0 +1,265 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * cam_helper.cpp - helper information for different sensors
+ */
+
+#include <linux/videodev2.h>
+
+#include <limits>
+#include <map>
+#include <string.h>
+
+#include "libcamera/internal/v4l2_videodevice.h"
+
+#include "cam_helper.h"
+#include "md_parser.h"
+
+using namespace RPiController;
+using namespace libcamera;
+using libcamera::utils::Duration;
+using namespace std::literals::chrono_literals;
+
+namespace libcamera {
+LOG_DECLARE_CATEGORY(IPARPI)
+}
+
+namespace {
+
+std::map<std::string, CamHelperCreateFunc> &camHelpers()
+{
+	static std::map<std::string, CamHelperCreateFunc> helpers;
+	return helpers;
+}
+
+} /* namespace */
+
+CamHelper *CamHelper::create(std::string const &camName)
+{
+	/*
+	 * CamHelpers get registered by static RegisterCamHelper
+	 * initialisers.
+	 */
+	for (auto &p : camHelpers()) {
+		if (camName.find(p.first) != std::string::npos)
+			return p.second();
+	}
+
+	return nullptr;
+}
+
+CamHelper::CamHelper(std::unique_ptr<MdParser> parser, unsigned int frameIntegrationDiff)
+	: parser_(std::move(parser)), frameIntegrationDiff_(frameIntegrationDiff)
+{
+}
+
+CamHelper::~CamHelper()
+{
+}
+
+void CamHelper::prepare(Span<const uint8_t> buffer,
+			Metadata &metadata)
+{
+	parseEmbeddedData(buffer, metadata);
+}
+
+void CamHelper::process([[maybe_unused]] StatisticsPtr &stats,
+			[[maybe_unused]] Metadata &metadata)
+{
+}
+
+uint32_t CamHelper::exposureLines(const Duration exposure, const Duration lineLength) const
+{
+	return exposure / lineLength;
+}
+
+Duration CamHelper::exposure(uint32_t exposureLines, const Duration lineLength) const
+{
+	return exposureLines * lineLength;
+}
+
+std::pair<uint32_t, uint32_t> CamHelper::getBlanking(Duration &exposure,
+						     Duration minFrameDuration,
+						     Duration maxFrameDuration) const
+{
+	uint32_t frameLengthMin, frameLengthMax, vblank, hblank;
+	Duration lineLength = mode_.minLineLength;
+
+	/*
+	 * minFrameDuration and maxFrameDuration are clamped by the caller
+	 * based on the limits for the active sensor mode.
+	 *
+	 * frameLengthMax gets calculated on the smallest line length as we do
+	 * not want to extend that unless absolutely necessary.
+	 */
+	frameLengthMin = minFrameDuration / mode_.minLineLength;
+	frameLengthMax = maxFrameDuration / mode_.minLineLength;
+
+	/*
+	 * Watch out for (exposureLines + frameIntegrationDiff_) overflowing a
+	 * uint32_t in the std::clamp() below when the exposure time is
+	 * extremely (extremely!) long - as happens when the IPA calculates the
+	 * maximum possible exposure time.
+	 */
+	uint32_t exposureLines = std::min(CamHelper::exposureLines(exposure, lineLength),
+					  std::numeric_limits<uint32_t>::max() - frameIntegrationDiff_);
+	uint32_t frameLengthLines = std::clamp(exposureLines + frameIntegrationDiff_,
+					       frameLengthMin, frameLengthMax);
+
+	/*
+	 * If our frame length lines is above the maximum allowed, see if we can
+	 * extend the line length to accommodate the requested frame length.
+	 */
+	if (frameLengthLines > mode_.maxFrameLength) {
+		Duration lineLengthAdjusted = lineLength * frameLengthLines / mode_.maxFrameLength;
+		lineLength = std::min(mode_.maxLineLength, lineLengthAdjusted);
+		frameLengthLines = mode_.maxFrameLength;
+	}
+
+	hblank = lineLengthToHblank(lineLength);
+	vblank = frameLengthLines - mode_.height;
+
+	/*
+	 * Limit the exposure to the maximum frame duration requested, and
+	 * re-calculate if it has been clipped.
+	 */
+	exposureLines = std::min(frameLengthLines - frameIntegrationDiff_,
+				 CamHelper::exposureLines(exposure, lineLength));
+	exposure = CamHelper::exposure(exposureLines, lineLength);
+
+	return { vblank, hblank };
+}
+
+Duration CamHelper::hblankToLineLength(uint32_t hblank) const
+{
+	return (mode_.width + hblank) * (1.0s / mode_.pixelRate);
+}
+
+uint32_t CamHelper::lineLengthToHblank(const Duration &lineLength) const
+{
+	return (lineLength * mode_.pixelRate / 1.0s) - mode_.width;
+}
+
+Duration CamHelper::lineLengthPckToDuration(uint32_t lineLengthPck) const
+{
+	return lineLengthPck * (1.0s / mode_.pixelRate);
+}
+
+void CamHelper::setCameraMode(const CameraMode &mode)
+{
+	mode_ = mode;
+	if (parser_) {
+		parser_->reset();
+		parser_->setBitsPerPixel(mode.bitdepth);
+		parser_->setLineLengthBytes(0); /* We use SetBufferSize. */
+	}
+}
+
+void CamHelper::getDelays(int &exposureDelay, int &gainDelay,
+			  int &vblankDelay, int &hblankDelay) const
+{
+	/*
+	 * These values are correct for many sensors. Other sensors will
+	 * need to over-ride this function.
+	 */
+	exposureDelay = 2;
+	gainDelay = 1;
+	vblankDelay = 2;
+	hblankDelay = 2;
+}
+
+bool CamHelper::sensorEmbeddedDataPresent() const
+{
+	return false;
+}
+
+double CamHelper::getModeSensitivity([[maybe_unused]] const CameraMode &mode) const
+{
+	/*
+	 * Most sensors have the same sensitivity in every mode, but this
+	 * function can be overridden for those that do not. Note that it is
+	 * called before mode_ is set, so it must return the sensitivity
+	 * of the mode that is passed in.
+	 */
+	return 1.0;
+}
+
+unsigned int CamHelper::hideFramesStartup() const
+{
+	/*
+	 * The number of frames when a camera first starts that shouldn't be
+	 * displayed as they are invalid in some way.
+	 */
+	return 0;
+}
+
+unsigned int CamHelper::hideFramesModeSwitch() const
+{
+	/* After a mode switch, many sensors return valid frames immediately. */
+	return 0;
+}
+
+unsigned int CamHelper::mistrustFramesStartup() const
+{
+	/* Many sensors return a single bad frame on start-up. */
+	return 1;
+}
+
+unsigned int CamHelper::mistrustFramesModeSwitch() const
+{
+	/* Many sensors return valid metadata immediately. */
+	return 0;
+}
+
+void CamHelper::parseEmbeddedData(Span<const uint8_t> buffer,
+				  Metadata &metadata)
+{
+	MdParser::RegisterMap registers;
+	Metadata parsedMetadata;
+
+	if (buffer.empty())
+		return;
+
+	if (parser_->parse(buffer, registers) != MdParser::Status::OK) {
+		LOG(IPARPI, Error) << "Embedded data buffer parsing failed";
+		return;
+	}
+
+	populateMetadata(registers, parsedMetadata);
+	metadata.merge(parsedMetadata);
+
+	/*
+	 * Overwrite the exposure/gain, line/frame length and sensor temperature values
+	 * in the existing DeviceStatus with values from the parsed embedded buffer.
+	 * Fetch it first in case any other fields were set meaningfully.
+	 */
+	DeviceStatus deviceStatus, parsedDeviceStatus;
+	if (metadata.get("device.status", deviceStatus) ||
+	    parsedMetadata.get("device.status", parsedDeviceStatus)) {
+		LOG(IPARPI, Error) << "DeviceStatus not found";
+		return;
+	}
+
+	deviceStatus.shutterSpeed = parsedDeviceStatus.shutterSpeed;
+	deviceStatus.analogueGain = parsedDeviceStatus.analogueGain;
+	deviceStatus.frameLength = parsedDeviceStatus.frameLength;
+	deviceStatus.lineLength = parsedDeviceStatus.lineLength;
+	if (parsedDeviceStatus.sensorTemperature)
+		deviceStatus.sensorTemperature = parsedDeviceStatus.sensorTemperature;
+
+	LOG(IPARPI, Debug) << "Metadata updated - " << deviceStatus;
+
+	metadata.set("device.status", deviceStatus);
+}
+
+void CamHelper::populateMetadata([[maybe_unused]] const MdParser::RegisterMap &registers,
+				 [[maybe_unused]] Metadata &metadata) const
+{
+}
+
+RegisterCamHelper::RegisterCamHelper(char const *camName,
+				     CamHelperCreateFunc createFunc)
+{
+	camHelpers()[std::string(camName)] = createFunc;
+}
diff --git a/src/ipa/rpi/cam_helper/cam_helper.h b/src/ipa/rpi/cam_helper/cam_helper.h
new file mode 100644
index 00000000..58a4b202
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper.h
@@ -0,0 +1,132 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * cam_helper.h - helper class providing camera information
+ */
+#pragma once
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include <libcamera/base/span.h>
+#include <libcamera/base/utils.h>
+
+#include "controller/camera_mode.h"
+#include "controller/controller.h"
+#include "controller/metadata.h"
+#include "md_parser.h"
+
+#include "libcamera/internal/v4l2_videodevice.h"
+
+namespace RPiController {
+
+/*
+ * The CamHelper class provides a number of facilities that anyone trying
+ * to drive a camera will need to know, but which are not provided by the
+ * standard driver framework. Specifically, it provides:
+ *
+ * A "CameraMode" structure to describe extra information about the chosen
+ * mode of the driver. For example, how it is cropped from the full sensor
+ * area, how it is scaled, whether pixels are averaged compared to the full
+ * resolution.
+ *
+ * The ability to convert between number of lines of exposure and actual
+ * exposure time, and to convert between the sensor's gain codes and actual
+ * gains.
+ *
+ * A function to return the number of frames of delay between updating exposure,
+ * analogue gain and vblanking, and for the changes to take effect. For many
+ * sensors these take the values 2, 1 and 2 respectively, but sensors that are
+ * different will need to over-ride the default function provided.
+ *
+ * A function to query if the sensor outputs embedded data that can be parsed.
+ *
+ * A function to return the sensitivity of a given camera mode.
+ *
+ * A parser to parse the embedded data buffers provided by some sensors (for
+ * example, the imx219 does; the ov5647 doesn't). This allows us to know for
+ * sure the exposure and gain of the frame we're looking at. CamHelper
+ * provides functions for converting analogue gains to and from the sensor's
+ * native gain codes.
+ *
+ * Finally, a set of functions that determine how to handle the vagaries of
+ * different camera modules on start-up or when switching modes. Some
+ * modules may produce one or more frames that are not yet correctly exposed,
+ * or where the metadata may be suspect. We have the following functions:
+ * HideFramesStartup(): Tell the pipeline handler not to return this many
+ *     frames at start-up. This can also be used to hide initial frames
+ *     while the AGC and other algorithms are sorting themselves out.
+ * HideFramesModeSwitch(): Tell the pipeline handler not to return this
+ *     many frames after a mode switch (other than start-up). Some sensors
+ *     may produce innvalid frames after a mode switch; others may not.
+ * MistrustFramesStartup(): At start-up a sensor may return frames for
+ *    which we should not run any control algorithms (for example, metadata
+ *    may be invalid).
+ * MistrustFramesModeSwitch(): The number of frames, after a mode switch
+ *    (other than start-up), for which control algorithms should not run
+ *    (for example, metadata may be unreliable).
+ */
+
+class CamHelper
+{
+public:
+	static CamHelper *create(std::string const &camName);
+	CamHelper(std::unique_ptr<MdParser> parser, unsigned int frameIntegrationDiff);
+	virtual ~CamHelper();
+	void setCameraMode(const CameraMode &mode);
+	virtual void prepare(libcamera::Span<const uint8_t> buffer,
+			     Metadata &metadata);
+	virtual void process(StatisticsPtr &stats, Metadata &metadata);
+	virtual uint32_t exposureLines(const libcamera::utils::Duration exposure,
+				       const libcamera::utils::Duration lineLength) const;
+	virtual libcamera::utils::Duration exposure(uint32_t exposureLines,
+						    const libcamera::utils::Duration lineLength) const;
+	virtual std::pair<uint32_t, uint32_t> getBlanking(libcamera::utils::Duration &exposure,
+							  libcamera::utils::Duration minFrameDuration,
+							  libcamera::utils::Duration maxFrameDuration) const;
+	libcamera::utils::Duration hblankToLineLength(uint32_t hblank) const;
+	uint32_t lineLengthToHblank(const libcamera::utils::Duration &duration) const;
+	libcamera::utils::Duration lineLengthPckToDuration(uint32_t lineLengthPck) const;
+	virtual uint32_t gainCode(double gain) const = 0;
+	virtual double gain(uint32_t gainCode) const = 0;
+	virtual void getDelays(int &exposureDelay, int &gainDelay,
+			       int &vblankDelay, int &hblankDelay) const;
+	virtual bool sensorEmbeddedDataPresent() const;
+	virtual double getModeSensitivity(const CameraMode &mode) const;
+	virtual unsigned int hideFramesStartup() const;
+	virtual unsigned int hideFramesModeSwitch() const;
+	virtual unsigned int mistrustFramesStartup() const;
+	virtual unsigned int mistrustFramesModeSwitch() const;
+
+protected:
+	void parseEmbeddedData(libcamera::Span<const uint8_t> buffer,
+			       Metadata &metadata);
+	virtual void populateMetadata(const MdParser::RegisterMap &registers,
+				      Metadata &metadata) const;
+
+	std::unique_ptr<MdParser> parser_;
+	CameraMode mode_;
+
+private:
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	unsigned int frameIntegrationDiff_;
+};
+
+/*
+ * This is for registering camera helpers with the system, so that the
+ * CamHelper::Create function picks them up automatically.
+ */
+
+typedef CamHelper *(*CamHelperCreateFunc)();
+struct RegisterCamHelper
+{
+	RegisterCamHelper(char const *camName,
+			  CamHelperCreateFunc createFunc);
+};
+
+} /* namespace RPi */
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx219.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx219.cpp
new file mode 100644
index 00000000..c3337ed0
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx219.cpp
@@ -0,0 +1,115 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * cam_helper_imx219.cpp - camera helper for imx219 sensor
+ */
+
+#include <assert.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+/*
+ * We have observed that the imx219 embedded data stream randomly returns junk
+ * register values. Do not rely on embedded data until this has been resolved.
+ */
+#define ENABLE_EMBEDDED_DATA 0
+
+#include "cam_helper.h"
+#if ENABLE_EMBEDDED_DATA
+#include "md_parser.h"
+#endif
+
+using namespace RPiController;
+
+/*
+ * We care about one gain register and a pair of exposure registers. Their I2C
+ * addresses from the Sony IMX219 datasheet:
+ */
+constexpr uint32_t gainReg = 0x157;
+constexpr uint32_t expHiReg = 0x15a;
+constexpr uint32_t expLoReg = 0x15b;
+constexpr uint32_t frameLengthHiReg = 0x160;
+constexpr uint32_t frameLengthLoReg = 0x161;
+constexpr uint32_t lineLengthHiReg = 0x162;
+constexpr uint32_t lineLengthLoReg = 0x163;
+constexpr std::initializer_list<uint32_t> registerList [[maybe_unused]]
+	= { expHiReg, expLoReg, gainReg, frameLengthHiReg, frameLengthLoReg,
+	    lineLengthHiReg, lineLengthLoReg };
+
+class CamHelperImx219 : public CamHelper
+{
+public:
+	CamHelperImx219();
+	uint32_t gainCode(double gain) const override;
+	double gain(uint32_t gainCode) const override;
+	unsigned int mistrustFramesModeSwitch() const override;
+	bool sensorEmbeddedDataPresent() const override;
+
+private:
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	static constexpr int frameIntegrationDiff = 4;
+
+	void populateMetadata(const MdParser::RegisterMap &registers,
+			      Metadata &metadata) const override;
+};
+
+CamHelperImx219::CamHelperImx219()
+#if ENABLE_EMBEDDED_DATA
+	: CamHelper(std::make_unique<MdParserSmia>(registerList), frameIntegrationDiff)
+#else
+	: CamHelper({}, frameIntegrationDiff)
+#endif
+{
+}
+
+uint32_t CamHelperImx219::gainCode(double gain) const
+{
+	return (uint32_t)(256 - 256 / gain);
+}
+
+double CamHelperImx219::gain(uint32_t gainCode) const
+{
+	return 256.0 / (256 - gainCode);
+}
+
+unsigned int CamHelperImx219::mistrustFramesModeSwitch() const
+{
+	/*
+	 * For reasons unknown, we do occasionally get a bogus metadata frame
+	 * at a mode switch (though not at start-up). Possibly warrants some
+	 * investigation, though not a big deal.
+	 */
+	return 1;
+}
+
+bool CamHelperImx219::sensorEmbeddedDataPresent() const
+{
+	return ENABLE_EMBEDDED_DATA;
+}
+
+void CamHelperImx219::populateMetadata(const MdParser::RegisterMap &registers,
+				       Metadata &metadata) const
+{
+	DeviceStatus deviceStatus;
+
+	deviceStatus.lineLength = lineLengthPckToDuration(registers.at(lineLengthHiReg) * 256 +
+							  registers.at(lineLengthLoReg));
+	deviceStatus.shutterSpeed = exposure(registers.at(expHiReg) * 256 + registers.at(expLoReg),
+					     deviceStatus.lineLength);
+	deviceStatus.analogueGain = gain(registers.at(gainReg));
+	deviceStatus.frameLength = registers.at(frameLengthHiReg) * 256 + registers.at(frameLengthLoReg);
+
+	metadata.set("device.status", deviceStatus);
+}
+
+static CamHelper *create()
+{
+	return new CamHelperImx219();
+}
+
+static RegisterCamHelper reg("imx219", &create);
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp
new file mode 100644
index 00000000..7d6f5b54
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2021, Raspberry Pi Ltd
+ *
+ * cam_helper_imx290.cpp - camera helper for imx290 sensor
+ */
+
+#include <math.h>
+
+#include "cam_helper.h"
+
+using namespace RPiController;
+
+class CamHelperImx290 : public CamHelper
+{
+public:
+	CamHelperImx290();
+	uint32_t gainCode(double gain) const override;
+	double gain(uint32_t gainCode) const override;
+	void getDelays(int &exposureDelay, int &gainDelay,
+		       int &vblankDelay, int &hblankDelay) const override;
+	unsigned int hideFramesModeSwitch() const override;
+
+private:
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	static constexpr int frameIntegrationDiff = 2;
+};
+
+CamHelperImx290::CamHelperImx290()
+	: CamHelper({}, frameIntegrationDiff)
+{
+}
+
+uint32_t CamHelperImx290::gainCode(double gain) const
+{
+	int code = 66.6667 * log10(gain);
+	return std::max(0, std::min(code, 0xf0));
+}
+
+double CamHelperImx290::gain(uint32_t gainCode) const
+{
+	return pow(10, 0.015 * gainCode);
+}
+
+void CamHelperImx290::getDelays(int &exposureDelay, int &gainDelay,
+				int &vblankDelay, int &hblankDelay) const
+{
+	exposureDelay = 2;
+	gainDelay = 2;
+	vblankDelay = 2;
+	hblankDelay = 2;
+}
+
+unsigned int CamHelperImx290::hideFramesModeSwitch() const
+{
+	/* After a mode switch, we seem to get 1 bad frame. */
+	return 1;
+}
+
+static CamHelper *create()
+{
+	return new CamHelperImx290();
+}
+
+static RegisterCamHelper reg("imx290", &create);
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp
new file mode 100644
index 00000000..ecb845e7
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp
@@ -0,0 +1,83 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * cam_helper_imx296.cpp - Camera helper for IMX296 sensor
+ */
+
+#include <algorithm>
+#include <cmath>
+#include <stddef.h>
+
+#include "cam_helper.h"
+
+using namespace RPiController;
+using libcamera::utils::Duration;
+using namespace std::literals::chrono_literals;
+
+class CamHelperImx296 : public CamHelper
+{
+public:
+	CamHelperImx296();
+	uint32_t gainCode(double gain) const override;
+	double gain(uint32_t gainCode) const override;
+	uint32_t exposureLines(const Duration exposure, const Duration lineLength) const override;
+	Duration exposure(uint32_t exposureLines, const Duration lineLength) const override;
+	void getDelays(int &exposureDelay, int &gainDelay,
+		       int &vblankDelay, int &hblankDelay) const override;
+
+private:
+	static constexpr uint32_t minExposureLines = 1;
+	static constexpr uint32_t maxGainCode = 239;
+	static constexpr Duration timePerLine = 550.0 / 37.125e6 * 1.0s;
+
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	static constexpr int frameIntegrationDiff = 4;
+};
+
+CamHelperImx296::CamHelperImx296()
+	: CamHelper(nullptr, frameIntegrationDiff)
+{
+}
+
+uint32_t CamHelperImx296::gainCode(double gain) const
+{
+	uint32_t code = 20 * std::log10(gain) * 10;
+	return std::min(code, maxGainCode);
+}
+
+double CamHelperImx296::gain(uint32_t gainCode) const
+{
+	return std::pow(10.0, gainCode / 200.0);
+}
+
+uint32_t CamHelperImx296::exposureLines(const Duration exposure,
+					[[maybe_unused]] const Duration lineLength) const
+{
+	return std::max<uint32_t>(minExposureLines, (exposure - 14.26us) / timePerLine);
+}
+
+Duration CamHelperImx296::exposure(uint32_t exposureLines,
+				   [[maybe_unused]] const Duration lineLength) const
+{
+	return std::max<uint32_t>(minExposureLines, exposureLines) * timePerLine + 14.26us;
+}
+
+void CamHelperImx296::getDelays(int &exposureDelay, int &gainDelay,
+				int &vblankDelay, int &hblankDelay) const
+{
+	exposureDelay = 2;
+	gainDelay = 2;
+	vblankDelay = 2;
+	hblankDelay = 2;
+}
+
+static CamHelper *create()
+{
+	return new CamHelperImx296();
+}
+
+static RegisterCamHelper reg("imx296", &create);
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp
new file mode 100644
index 00000000..bc769ca7
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp
@@ -0,0 +1,197 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * cam_helper_imx477.cpp - camera helper for imx477 sensor
+ */
+
+#include <algorithm>
+#include <assert.h>
+#include <cmath>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <libcamera/base/log.h>
+
+#include "cam_helper.h"
+#include "md_parser.h"
+
+using namespace RPiController;
+using namespace libcamera;
+using libcamera::utils::Duration;
+
+namespace libcamera {
+LOG_DECLARE_CATEGORY(IPARPI)
+}
+
+/*
+ * We care about two gain registers and a pair of exposure registers. Their
+ * I2C addresses from the Sony IMX477 datasheet:
+ */
+constexpr uint32_t expHiReg = 0x0202;
+constexpr uint32_t expLoReg = 0x0203;
+constexpr uint32_t gainHiReg = 0x0204;
+constexpr uint32_t gainLoReg = 0x0205;
+constexpr uint32_t frameLengthHiReg = 0x0340;
+constexpr uint32_t frameLengthLoReg = 0x0341;
+constexpr uint32_t lineLengthHiReg = 0x0342;
+constexpr uint32_t lineLengthLoReg = 0x0343;
+constexpr uint32_t temperatureReg = 0x013a;
+constexpr std::initializer_list<uint32_t> registerList =
+	{ expHiReg, expLoReg, gainHiReg, gainLoReg, frameLengthHiReg, frameLengthLoReg,
+	  lineLengthHiReg, lineLengthLoReg, temperatureReg };
+
+class CamHelperImx477 : public CamHelper
+{
+public:
+	CamHelperImx477();
+	uint32_t gainCode(double gain) const override;
+	double gain(uint32_t gainCode) const override;
+	void prepare(libcamera::Span<const uint8_t> buffer, Metadata &metadata) override;
+	std::pair<uint32_t, uint32_t> getBlanking(Duration &exposure, Duration minFrameDuration,
+						  Duration maxFrameDuration) const override;
+	void getDelays(int &exposureDelay, int &gainDelay,
+		       int &vblankDelay, int &hblankDelay) const override;
+	bool sensorEmbeddedDataPresent() const override;
+
+private:
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	static constexpr int frameIntegrationDiff = 22;
+	/* Maximum frame length allowable for long exposure calculations. */
+	static constexpr int frameLengthMax = 0xffdc;
+	/* Largest long exposure scale factor given as a left shift on the frame length. */
+	static constexpr int longExposureShiftMax = 7;
+
+	void populateMetadata(const MdParser::RegisterMap &registers,
+			      Metadata &metadata) const override;
+};
+
+CamHelperImx477::CamHelperImx477()
+	: CamHelper(std::make_unique<MdParserSmia>(registerList), frameIntegrationDiff)
+{
+}
+
+uint32_t CamHelperImx477::gainCode(double gain) const
+{
+	return static_cast<uint32_t>(1024 - 1024 / gain);
+}
+
+double CamHelperImx477::gain(uint32_t gainCode) const
+{
+	return 1024.0 / (1024 - gainCode);
+}
+
+void CamHelperImx477::prepare(libcamera::Span<const uint8_t> buffer, Metadata &metadata)
+{
+	MdParser::RegisterMap registers;
+	DeviceStatus deviceStatus;
+
+	if (metadata.get("device.status", deviceStatus)) {
+		LOG(IPARPI, Error) << "DeviceStatus not found from DelayedControls";
+		return;
+	}
+
+	parseEmbeddedData(buffer, metadata);
+
+	/*
+	 * The DeviceStatus struct is first populated with values obtained from
+	 * DelayedControls. If this reports frame length is > frameLengthMax,
+	 * it means we are using a long exposure mode. Since the long exposure
+	 * scale factor is not returned back through embedded data, we must rely
+	 * on the existing exposure lines and frame length values returned by
+	 * DelayedControls.
+	 *
+	 * Otherwise, all values are updated with what is reported in the
+	 * embedded data.
+	 */
+	if (deviceStatus.frameLength > frameLengthMax) {
+		DeviceStatus parsedDeviceStatus;
+
+		metadata.get("device.status", parsedDeviceStatus);
+		parsedDeviceStatus.shutterSpeed = deviceStatus.shutterSpeed;
+		parsedDeviceStatus.frameLength = deviceStatus.frameLength;
+		metadata.set("device.status", parsedDeviceStatus);
+
+		LOG(IPARPI, Debug) << "Metadata updated for long exposure: "
+				   << parsedDeviceStatus;
+	}
+}
+
+std::pair<uint32_t, uint32_t> CamHelperImx477::getBlanking(Duration &exposure,
+							   Duration minFrameDuration,
+							   Duration maxFrameDuration) const
+{
+	uint32_t frameLength, exposureLines;
+	unsigned int shift = 0;
+
+	auto [vblank, hblank] = CamHelper::getBlanking(exposure, minFrameDuration,
+						       maxFrameDuration);
+
+	frameLength = mode_.height + vblank;
+	Duration lineLength = hblankToLineLength(hblank);
+
+	/*
+	 * Check if the frame length calculated needs to be setup for long
+	 * exposure mode. This will require us to use a long exposure scale
+	 * factor provided by a shift operation in the sensor.
+	 */
+	while (frameLength > frameLengthMax) {
+		if (++shift > longExposureShiftMax) {
+			shift = longExposureShiftMax;
+			frameLength = frameLengthMax;
+			break;
+		}
+		frameLength >>= 1;
+	}
+
+	if (shift) {
+		/* Account for any rounding in the scaled frame length value. */
+		frameLength <<= shift;
+		exposureLines = CamHelperImx477::exposureLines(exposure, lineLength);
+		exposureLines = std::min(exposureLines, frameLength - frameIntegrationDiff);
+		exposure = CamHelperImx477::exposure(exposureLines, lineLength);
+	}
+
+	return { frameLength - mode_.height, hblank };
+}
+
+void CamHelperImx477::getDelays(int &exposureDelay, int &gainDelay,
+				int &vblankDelay, int &hblankDelay) const
+{
+	exposureDelay = 2;
+	gainDelay = 2;
+	vblankDelay = 3;
+	hblankDelay = 3;
+}
+
+bool CamHelperImx477::sensorEmbeddedDataPresent() const
+{
+	return true;
+}
+
+void CamHelperImx477::populateMetadata(const MdParser::RegisterMap &registers,
+				       Metadata &metadata) const
+{
+	DeviceStatus deviceStatus;
+
+	deviceStatus.lineLength = lineLengthPckToDuration(registers.at(lineLengthHiReg) * 256 +
+							  registers.at(lineLengthLoReg));
+	deviceStatus.shutterSpeed = exposure(registers.at(expHiReg) * 256 + registers.at(expLoReg),
+					     deviceStatus.lineLength);
+	deviceStatus.analogueGain = gain(registers.at(gainHiReg) * 256 + registers.at(gainLoReg));
+	deviceStatus.frameLength = registers.at(frameLengthHiReg) * 256 + registers.at(frameLengthLoReg);
+	deviceStatus.sensorTemperature = std::clamp<int8_t>(registers.at(temperatureReg), -20, 80);
+
+	metadata.set("device.status", deviceStatus);
+}
+
+static CamHelper *create()
+{
+	return new CamHelperImx477();
+}
+
+static RegisterCamHelper reg("imx477", &create);
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp
new file mode 100644
index 00000000..c7262aa0
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp
@@ -0,0 +1,196 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Based on cam_helper_imx477.cpp
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * cam_helper_imx519.cpp - camera helper for imx519 sensor
+ * Copyright (C) 2021, Arducam Technology co., Ltd.
+ */
+
+#include <assert.h>
+#include <cmath>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <libcamera/base/log.h>
+
+#include "cam_helper.h"
+#include "md_parser.h"
+
+using namespace RPiController;
+using namespace libcamera;
+using libcamera::utils::Duration;
+
+namespace libcamera {
+LOG_DECLARE_CATEGORY(IPARPI)
+}
+
+/*
+ * We care about two gain registers and a pair of exposure registers. Their
+ * I2C addresses from the Sony IMX519 datasheet:
+ */
+constexpr uint32_t expHiReg = 0x0202;
+constexpr uint32_t expLoReg = 0x0203;
+constexpr uint32_t gainHiReg = 0x0204;
+constexpr uint32_t gainLoReg = 0x0205;
+constexpr uint32_t frameLengthHiReg = 0x0340;
+constexpr uint32_t frameLengthLoReg = 0x0341;
+constexpr uint32_t lineLengthHiReg = 0x0342;
+constexpr uint32_t lineLengthLoReg = 0x0343;
+constexpr std::initializer_list<uint32_t> registerList =
+	{ expHiReg, expLoReg, gainHiReg, gainLoReg, frameLengthHiReg, frameLengthLoReg,
+	  lineLengthHiReg, lineLengthLoReg };
+
+class CamHelperImx519 : public CamHelper
+{
+public:
+	CamHelperImx519();
+	uint32_t gainCode(double gain) const override;
+	double gain(uint32_t gainCode) const override;
+	void prepare(libcamera::Span<const uint8_t> buffer, Metadata &metadata) override;
+	std::pair<uint32_t, uint32_t> getBlanking(Duration &exposure, Duration minFrameDuration,
+						  Duration maxFrameDuration) const override;
+	void getDelays(int &exposureDelay, int &gainDelay,
+		       int &vblankDelay, int &hblankDelay) const override;
+	bool sensorEmbeddedDataPresent() const override;
+
+private:
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	static constexpr int frameIntegrationDiff = 32;
+	/* Maximum frame length allowable for long exposure calculations. */
+	static constexpr int frameLengthMax = 0xffdc;
+	/* Largest long exposure scale factor given as a left shift on the frame length. */
+	static constexpr int longExposureShiftMax = 7;
+
+	void populateMetadata(const MdParser::RegisterMap &registers,
+			      Metadata &metadata) const override;
+};
+
+CamHelperImx519::CamHelperImx519()
+	: CamHelper(std::make_unique<MdParserSmia>(registerList), frameIntegrationDiff)
+{
+}
+
+uint32_t CamHelperImx519::gainCode(double gain) const
+{
+	return static_cast<uint32_t>(1024 - 1024 / gain);
+}
+
+double CamHelperImx519::gain(uint32_t gainCode) const
+{
+	return 1024.0 / (1024 - gainCode);
+}
+
+void CamHelperImx519::prepare(libcamera::Span<const uint8_t> buffer, Metadata &metadata)
+{
+	MdParser::RegisterMap registers;
+	DeviceStatus deviceStatus;
+
+	if (metadata.get("device.status", deviceStatus)) {
+		LOG(IPARPI, Error) << "DeviceStatus not found from DelayedControls";
+		return;
+	}
+
+	parseEmbeddedData(buffer, metadata);
+
+	/*
+	 * The DeviceStatus struct is first populated with values obtained from
+	 * DelayedControls. If this reports frame length is > frameLengthMax,
+	 * it means we are using a long exposure mode. Since the long exposure
+	 * scale factor is not returned back through embedded data, we must rely
+	 * on the existing exposure lines and frame length values returned by
+	 * DelayedControls.
+	 *
+	 * Otherwise, all values are updated with what is reported in the
+	 * embedded data.
+	 */
+	if (deviceStatus.frameLength > frameLengthMax) {
+		DeviceStatus parsedDeviceStatus;
+
+		metadata.get("device.status", parsedDeviceStatus);
+		parsedDeviceStatus.shutterSpeed = deviceStatus.shutterSpeed;
+		parsedDeviceStatus.frameLength = deviceStatus.frameLength;
+		metadata.set("device.status", parsedDeviceStatus);
+
+		LOG(IPARPI, Debug) << "Metadata updated for long exposure: "
+				   << parsedDeviceStatus;
+	}
+}
+
+std::pair<uint32_t, uint32_t> CamHelperImx519::getBlanking(Duration &exposure,
+							   Duration minFrameDuration,
+							   Duration maxFrameDuration) const
+{
+	uint32_t frameLength, exposureLines;
+	unsigned int shift = 0;
+
+	auto [vblank, hblank] = CamHelper::getBlanking(exposure, minFrameDuration,
+						       maxFrameDuration);
+
+	frameLength = mode_.height + vblank;
+	Duration lineLength = hblankToLineLength(hblank);
+
+	/*
+	 * Check if the frame length calculated needs to be setup for long
+	 * exposure mode. This will require us to use a long exposure scale
+	 * factor provided by a shift operation in the sensor.
+	 */
+	while (frameLength > frameLengthMax) {
+		if (++shift > longExposureShiftMax) {
+			shift = longExposureShiftMax;
+			frameLength = frameLengthMax;
+			break;
+		}
+		frameLength >>= 1;
+	}
+
+	if (shift) {
+		/* Account for any rounding in the scaled frame length value. */
+		frameLength <<= shift;
+		exposureLines = CamHelperImx519::exposureLines(exposure, lineLength);
+		exposureLines = std::min(exposureLines, frameLength - frameIntegrationDiff);
+		exposure = CamHelperImx519::exposure(exposureLines, lineLength);
+	}
+
+	return { frameLength - mode_.height, hblank };
+}
+
+void CamHelperImx519::getDelays(int &exposureDelay, int &gainDelay,
+				int &vblankDelay, int &hblankDelay) const
+{
+	exposureDelay = 2;
+	gainDelay = 2;
+	vblankDelay = 3;
+	hblankDelay = 3;
+}
+
+bool CamHelperImx519::sensorEmbeddedDataPresent() const
+{
+	return true;
+}
+
+void CamHelperImx519::populateMetadata(const MdParser::RegisterMap &registers,
+				       Metadata &metadata) const
+{
+	DeviceStatus deviceStatus;
+
+	deviceStatus.lineLength = lineLengthPckToDuration(registers.at(lineLengthHiReg) * 256 +
+							  registers.at(lineLengthLoReg));
+	deviceStatus.shutterSpeed = exposure(registers.at(expHiReg) * 256 + registers.at(expLoReg),
+					     deviceStatus.lineLength);
+	deviceStatus.analogueGain = gain(registers.at(gainHiReg) * 256 + registers.at(gainLoReg));
+	deviceStatus.frameLength = registers.at(frameLengthHiReg) * 256 + registers.at(frameLengthLoReg);
+
+	metadata.set("device.status", deviceStatus);
+}
+
+static CamHelper *create()
+{
+	return new CamHelperImx519();
+}
+
+static RegisterCamHelper reg("imx519", &create);
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp
new file mode 100644
index 00000000..641ba18f
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp
@@ -0,0 +1,359 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022, Raspberry Pi Ltd
+ *
+ * cam_helper_imx708.cpp - camera helper for imx708 sensor
+ */
+
+#include <cmath>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <libcamera/base/log.h>
+
+#include "controller/pdaf_data.h"
+
+#include "cam_helper.h"
+#include "md_parser.h"
+
+using namespace RPiController;
+using namespace libcamera;
+using libcamera::utils::Duration;
+
+namespace libcamera {
+LOG_DECLARE_CATEGORY(IPARPI)
+}
+
+/*
+ * We care about two gain registers and a pair of exposure registers. Their
+ * I2C addresses from the Sony imx708 datasheet:
+ */
+constexpr uint32_t expHiReg = 0x0202;
+constexpr uint32_t expLoReg = 0x0203;
+constexpr uint32_t gainHiReg = 0x0204;
+constexpr uint32_t gainLoReg = 0x0205;
+constexpr uint32_t frameLengthHiReg = 0x0340;
+constexpr uint32_t frameLengthLoReg = 0x0341;
+constexpr uint32_t lineLengthHiReg = 0x0342;
+constexpr uint32_t lineLengthLoReg = 0x0343;
+constexpr uint32_t temperatureReg = 0x013a;
+constexpr std::initializer_list<uint32_t> registerList =
+	{ expHiReg, expLoReg, gainHiReg, gainLoReg, lineLengthHiReg,
+	  lineLengthLoReg, frameLengthHiReg, frameLengthLoReg, temperatureReg };
+
+class CamHelperImx708 : public CamHelper
+{
+public:
+	CamHelperImx708();
+	uint32_t gainCode(double gain) const override;
+	double gain(uint32_t gain_code) const override;
+	void prepare(libcamera::Span<const uint8_t> buffer, Metadata &metadata) override;
+	void process(StatisticsPtr &stats, Metadata &metadata) override;
+	std::pair<uint32_t, uint32_t> getBlanking(Duration &exposure, Duration minFrameDuration,
+						  Duration maxFrameDuration) const override;
+	void getDelays(int &exposureDelay, int &gainDelay,
+		       int &vblankDelay, int &hblankDelay) const override;
+	bool sensorEmbeddedDataPresent() const override;
+	double getModeSensitivity(const CameraMode &mode) const override;
+	unsigned int hideFramesModeSwitch() const override { return 1; } // seems to be required for HDR
+
+private:
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	static constexpr int frameIntegrationDiff = 22;
+	/* Maximum frame length allowable for long exposure calculations. */
+	static constexpr int frameLengthMax = 0xffdc;
+	/* Largest long exposure scale factor given as a left shift on the frame length. */
+	static constexpr int longExposureShiftMax = 7;
+
+	static constexpr int pdafStatsRows = 12;
+	static constexpr int pdafStatsCols = 16;
+
+	void populateMetadata(const MdParser::RegisterMap &registers,
+			      Metadata &metadata) const override;
+
+	static bool parsePdafData(const uint8_t *ptr, size_t len, unsigned bpp,
+				  PdafRegions &pdaf);
+
+	bool parseAEHist(const uint8_t *ptr, size_t len, unsigned bpp);
+	void putAGCStatistics(StatisticsPtr stats);
+
+	Histogram aeHistLinear_;
+	uint32_t aeHistAverage_;
+	bool aeHistValid_;
+};
+
+CamHelperImx708::CamHelperImx708()
+	: CamHelper(std::make_unique<MdParserSmia>(registerList), frameIntegrationDiff),
+	  aeHistLinear_{}, aeHistAverage_(0), aeHistValid_(false)
+{
+}
+
+uint32_t CamHelperImx708::gainCode(double gain) const
+{
+	return static_cast<uint32_t>(1024 - 1024 / gain);
+}
+
+double CamHelperImx708::gain(uint32_t gain_code) const
+{
+	return 1024.0 / (1024 - gain_code);
+}
+
+void CamHelperImx708::prepare(libcamera::Span<const uint8_t> buffer, Metadata &metadata)
+{
+	MdParser::RegisterMap registers;
+	DeviceStatus deviceStatus;
+
+	LOG(IPARPI, Debug) << "Embedded buffer size: " << buffer.size();
+
+	if (metadata.get("device.status", deviceStatus)) {
+		LOG(IPARPI, Error) << "DeviceStatus not found from DelayedControls";
+		return;
+	}
+
+	parseEmbeddedData(buffer, metadata);
+
+	/*
+	 * Parse PDAF data, which we expect to occupy the third scanline
+	 * of embedded data. As PDAF is quite sensor-specific, it's parsed here.
+	 */
+	size_t bytesPerLine = (mode_.width * mode_.bitdepth) >> 3;
+
+	if (buffer.size() > 2 * bytesPerLine) {
+		PdafRegions pdaf;
+		if (parsePdafData(&buffer[2 * bytesPerLine],
+				  buffer.size() - 2 * bytesPerLine,
+				  mode_.bitdepth, pdaf))
+			metadata.set("pdaf.regions", pdaf);
+	}
+
+	/* Parse AE-HIST data where present */
+	if (buffer.size() > 3 * bytesPerLine) {
+		aeHistValid_ = parseAEHist(&buffer[3 * bytesPerLine],
+					   buffer.size() - 3 * bytesPerLine,
+					   mode_.bitdepth);
+	}
+
+	/*
+	 * The DeviceStatus struct is first populated with values obtained from
+	 * DelayedControls. If this reports frame length is > frameLengthMax,
+	 * it means we are using a long exposure mode. Since the long exposure
+	 * scale factor is not returned back through embedded data, we must rely
+	 * on the existing exposure lines and frame length values returned by
+	 * DelayedControls.
+	 *
+	 * Otherwise, all values are updated with what is reported in the
+	 * embedded data.
+	 */
+	if (deviceStatus.frameLength > frameLengthMax) {
+		DeviceStatus parsedDeviceStatus;
+
+		metadata.get("device.status", parsedDeviceStatus);
+		parsedDeviceStatus.shutterSpeed = deviceStatus.shutterSpeed;
+		parsedDeviceStatus.frameLength = deviceStatus.frameLength;
+		metadata.set("device.status", parsedDeviceStatus);
+
+		LOG(IPARPI, Debug) << "Metadata updated for long exposure: "
+				   << parsedDeviceStatus;
+	}
+}
+
+void CamHelperImx708::process(StatisticsPtr &stats, [[maybe_unused]] Metadata &metadata)
+{
+	if (aeHistValid_)
+		putAGCStatistics(stats);
+}
+
+std::pair<uint32_t, uint32_t> CamHelperImx708::getBlanking(Duration &exposure,
+							   Duration minFrameDuration,
+							   Duration maxFrameDuration) const
+{
+	uint32_t frameLength, exposureLines;
+	unsigned int shift = 0;
+
+	auto [vblank, hblank] = CamHelper::getBlanking(exposure, minFrameDuration,
+						       maxFrameDuration);
+
+	frameLength = mode_.height + vblank;
+	Duration lineLength = hblankToLineLength(hblank);
+
+	/*
+	 * Check if the frame length calculated needs to be setup for long
+	 * exposure mode. This will require us to use a long exposure scale
+	 * factor provided by a shift operation in the sensor.
+	 */
+	while (frameLength > frameLengthMax) {
+		if (++shift > longExposureShiftMax) {
+			shift = longExposureShiftMax;
+			frameLength = frameLengthMax;
+			break;
+		}
+		frameLength >>= 1;
+	}
+
+	if (shift) {
+		/* Account for any rounding in the scaled frame length value. */
+		frameLength <<= shift;
+		exposureLines = CamHelper::exposureLines(exposure, lineLength);
+		exposureLines = std::min(exposureLines, frameLength - frameIntegrationDiff);
+		exposure = CamHelper::exposure(exposureLines, lineLength);
+	}
+
+	return { frameLength - mode_.height, hblank };
+}
+
+void CamHelperImx708::getDelays(int &exposureDelay, int &gainDelay,
+				int &vblankDelay, int &hblankDelay) const
+{
+	exposureDelay = 2;
+	gainDelay = 2;
+	vblankDelay = 3;
+	hblankDelay = 3;
+}
+
+bool CamHelperImx708::sensorEmbeddedDataPresent() const
+{
+	return true;
+}
+
+double CamHelperImx708::getModeSensitivity(const CameraMode &mode) const
+{
+	/* In binned modes, sensitivity increases by a factor of 2 */
+	return (mode.width > 2304) ? 1.0 : 2.0;
+}
+
+void CamHelperImx708::populateMetadata(const MdParser::RegisterMap &registers,
+				       Metadata &metadata) const
+{
+	DeviceStatus deviceStatus;
+
+	deviceStatus.lineLength = lineLengthPckToDuration(registers.at(lineLengthHiReg) * 256 +
+							  registers.at(lineLengthLoReg));
+	deviceStatus.shutterSpeed = exposure(registers.at(expHiReg) * 256 + registers.at(expLoReg),
+					     deviceStatus.lineLength);
+	deviceStatus.analogueGain = gain(registers.at(gainHiReg) * 256 + registers.at(gainLoReg));
+	deviceStatus.frameLength = registers.at(frameLengthHiReg) * 256 + registers.at(frameLengthLoReg);
+	deviceStatus.sensorTemperature = std::clamp<int8_t>(registers.at(temperatureReg), -20, 80);
+
+	metadata.set("device.status", deviceStatus);
+}
+
+bool CamHelperImx708::parsePdafData(const uint8_t *ptr, size_t len,
+				    unsigned bpp, PdafRegions &pdaf)
+{
+	size_t step = bpp >> 1; /* bytes per PDAF grid entry */
+
+	if (bpp < 10 || bpp > 12 || len < 194 * step || ptr[0] != 0 || ptr[1] >= 0x40) {
+		LOG(IPARPI, Error) << "PDAF data in unsupported format";
+		return false;
+	}
+
+	pdaf.init({ pdafStatsCols, pdafStatsRows });
+
+	ptr += 2 * step;
+	for (unsigned i = 0; i < pdafStatsRows; ++i) {
+		for (unsigned j = 0; j < pdafStatsCols; ++j) {
+			unsigned c = (ptr[0] << 3) | (ptr[1] >> 5);
+			int p = (((ptr[1] & 0x0F) - (ptr[1] & 0x10)) << 6) | (ptr[2] >> 2);
+			PdafData pdafData;
+			pdafData.conf = c;
+			pdafData.phase = c ? p : 0;
+			pdaf.set(libcamera::Point(j, i), { pdafData, 1, 0 });
+			ptr += step;
+		}
+	}
+
+	return true;
+}
+
+bool CamHelperImx708::parseAEHist(const uint8_t *ptr, size_t len, unsigned bpp)
+{
+	static constexpr unsigned int PipelineBits = Statistics::NormalisationFactorPow2;
+
+	uint64_t count = 0, sum = 0;
+	size_t step = bpp >> 1; /* bytes per histogram bin */
+	uint32_t hist[128];
+
+	if (len < 144 * step)
+		return false;
+
+	/*
+	 * Read the 128 bin linear histogram, which by default covers
+	 * the full range of the HDR shortest exposure (small values are
+	 * expected to dominate, so pixel-value resolution will be poor).
+	 */
+	for (unsigned i = 0; i < 128; ++i) {
+		if (ptr[3] != 0x55)
+			return false;
+		uint32_t c = (ptr[0] << 14) + (ptr[1] << 6) + (ptr[2] >> 2);
+		hist[i] = c >> 2; /* pixels to quads */
+		if (i != 0) {
+			count += c;
+			sum += c *
+			       (i * (1u << (PipelineBits - 7)) +
+				(1u << (PipelineBits - 8)));
+		}
+		ptr += step;
+	}
+
+	/*
+	 * Now use the first 9 bins of the log histogram (these should be
+	 * subdivisions of the smallest linear bin), to get a more accurate
+	 * average value. Don't assume that AEHIST1_AVERAGE is present.
+	 */
+	for (unsigned i = 0; i < 9; ++i) {
+		if (ptr[3] != 0x55)
+			return false;
+		uint32_t c = (ptr[0] << 14) + (ptr[1] << 6) + (ptr[2] >> 2);
+		count += c;
+		sum += c *
+		       ((3u << PipelineBits) >> (17 - i));
+		ptr += step;
+	}
+	if ((unsigned)((ptr[0] << 12) + (ptr[1] << 4) + (ptr[2] >> 4)) !=
+	    hist[1]) {
+		LOG(IPARPI, Error) << "Lin/Log histogram mismatch";
+		return false;
+	}
+
+	aeHistLinear_ = Histogram(hist, 128);
+	aeHistAverage_ = count ? (sum / count) : 0;
+
+	return count != 0;
+}
+
+void CamHelperImx708::putAGCStatistics(StatisticsPtr stats)
+{
+	/*
+	 * For HDR mode, copy sensor's AE/AGC statistics over ISP's, so the
+	 * AGC algorithm sees a linear response to exposure and gain changes.
+	 *
+	 * Histogram: Just copy the "raw" histogram over the tone-mapped one,
+	 * although they have different distributions (raw values are lower).
+	 * Tuning should either ignore it, or constrain for highlights only.
+	 *
+	 * Average: Overwrite all regional averages with a global raw average,
+	 * scaled by a fiddle-factor so that a conventional (non-HDR) y_target
+	 * of e.g. 0.17 will map to a suitable level for HDR.
+	 */
+	stats->yHist = aeHistLinear_;
+
+	constexpr unsigned int HdrHeadroomFactor = 4;
+	uint64_t v = HdrHeadroomFactor * aeHistAverage_;
+	for (auto &region : stats->agcRegions) {
+		region.val.rSum = region.val.gSum = region.val.bSum = region.counted * v;
+	}
+}
+
+static CamHelper *create()
+{
+	return new CamHelperImx708();
+}
+
+static RegisterCamHelper reg("imx708", &create);
+static RegisterCamHelper regWide("imx708_wide", &create);
+static RegisterCamHelper regNoIr("imx708_noir", &create);
+static RegisterCamHelper regWideNoIr("imx708_wide_noir", &create);
diff --git a/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp b/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp
new file mode 100644
index 00000000..5a99083d
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp
@@ -0,0 +1,109 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * cam_helper_ov5647.cpp - camera information for ov5647 sensor
+ */
+
+#include <assert.h>
+
+#include "cam_helper.h"
+
+using namespace RPiController;
+
+class CamHelperOv5647 : public CamHelper
+{
+public:
+	CamHelperOv5647();
+	uint32_t gainCode(double gain) const override;
+	double gain(uint32_t gainCode) const override;
+	void getDelays(int &exposureDelay, int &gainDelay,
+		       int &vblankDelay, int &hblankDelay) const override;
+	unsigned int hideFramesStartup() const override;
+	unsigned int hideFramesModeSwitch() const override;
+	unsigned int mistrustFramesStartup() const override;
+	unsigned int mistrustFramesModeSwitch() const override;
+
+private:
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	static constexpr int frameIntegrationDiff = 4;
+};
+
+/*
+ * OV5647 doesn't output metadata, so we have to use the "unicam parser" which
+ * works by counting frames.
+ */
+
+CamHelperOv5647::CamHelperOv5647()
+	: CamHelper({}, frameIntegrationDiff)
+{
+}
+
+uint32_t CamHelperOv5647::gainCode(double gain) const
+{
+	return static_cast<uint32_t>(gain * 16.0);
+}
+
+double CamHelperOv5647::gain(uint32_t gainCode) const
+{
+	return static_cast<double>(gainCode) / 16.0;
+}
+
+void CamHelperOv5647::getDelays(int &exposureDelay, int &gainDelay,
+				int &vblankDelay, int &hblankDelay) const
+{
+	/*
+	 * We run this sensor in a mode where the gain delay is bumped up to
+	 * 2. It seems to be the only way to make the delays "predictable".
+	 */
+	exposureDelay = 2;
+	gainDelay = 2;
+	vblankDelay = 2;
+	hblankDelay = 2;
+}
+
+unsigned int CamHelperOv5647::hideFramesStartup() const
+{
+	/*
+	 * On startup, we get a couple of under-exposed frames which
+	 * we don't want shown.
+	 */
+	return 2;
+}
+
+unsigned int CamHelperOv5647::hideFramesModeSwitch() const
+{
+	/*
+	 * After a mode switch, we get a couple of under-exposed frames which
+	 * we don't want shown.
+	 */
+	return 2;
+}
+
+unsigned int CamHelperOv5647::mistrustFramesStartup() const
+{
+	/*
+	 * First couple of frames are under-exposed and are no good for control
+	 * algos.
+	 */
+	return 2;
+}
+
+unsigned int CamHelperOv5647::mistrustFramesModeSwitch() const
+{
+	/*
+	 * First couple of frames are under-exposed even after a simple
+	 * mode switch, and are no good for control algos.
+	 */
+	return 2;
+}
+
+static CamHelper *create()
+{
+	return new CamHelperOv5647();
+}
+
+static RegisterCamHelper reg("ov5647", &create);
diff --git a/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp b/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp
new file mode 100644
index 00000000..86c5bc4c
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2021, Raspberry Pi Ltd
+ *
+ * cam_helper_ov9281.cpp - camera information for ov9281 sensor
+ */
+
+#include <assert.h>
+
+#include "cam_helper.h"
+
+using namespace RPiController;
+
+class CamHelperOv9281 : public CamHelper
+{
+public:
+	CamHelperOv9281();
+	uint32_t gainCode(double gain) const override;
+	double gain(uint32_t gainCode) const override;
+	void getDelays(int &exposureDelay, int &gainDelay,
+		       int &vblankDelay, int &hblankDelay) const override;
+
+private:
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	static constexpr int frameIntegrationDiff = 4;
+};
+
+/*
+ * OV9281 doesn't output metadata, so we have to use the "unicam parser" which
+ * works by counting frames.
+ */
+
+CamHelperOv9281::CamHelperOv9281()
+	: CamHelper({}, frameIntegrationDiff)
+{
+}
+
+uint32_t CamHelperOv9281::gainCode(double gain) const
+{
+	return static_cast<uint32_t>(gain * 16.0);
+}
+
+double CamHelperOv9281::gain(uint32_t gainCode) const
+{
+	return static_cast<double>(gainCode) / 16.0;
+}
+
+void CamHelperOv9281::getDelays(int &exposureDelay, int &gainDelay,
+				int &vblankDelay, int &hblankDelay) const
+{
+	/* The driver appears to behave as follows: */
+	exposureDelay = 2;
+	gainDelay = 2;
+	vblankDelay = 2;
+	hblankDelay = 2;
+}
+
+static CamHelper *create()
+{
+	return new CamHelperOv9281();
+}
+
+static RegisterCamHelper reg("ov9281", &create);
diff --git a/src/ipa/rpi/cam_helper/md_parser.h b/src/ipa/rpi/cam_helper/md_parser.h
new file mode 100644
index 00000000..77d557aa
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/md_parser.h
@@ -0,0 +1,155 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * md_parser.h - image sensor metadata parser interface
+ */
+#pragma once
+
+#include <initializer_list>
+#include <map>
+#include <optional>
+#include <stdint.h>
+
+#include <libcamera/base/span.h>
+
+/*
+ * Camera metadata parser class. Usage as shown below.
+ *
+ * Setup:
+ *
+ * Usually the metadata parser will be made as part of the CamHelper class so
+ * application code doesn't have to worry which kind to instantiate. But for
+ * the sake of example let's suppose we're parsing imx219 metadata.
+ *
+ * MdParser *parser = new MdParserSmia({ expHiReg, expLoReg, gainReg });
+ * parser->SetBitsPerPixel(bpp);
+ * parser->SetLineLengthBytes(pitch);
+ * parser->SetNumLines(2);
+ *
+ * Note 1: if you don't know how many lines there are, the size of the input
+ * buffer is used as a limit instead.
+ *
+ * Note 2: if you don't know the line length, you can leave the line length unset
+ * (or set to zero) and the parser will hunt for the line start instead.
+ *
+ * Then on every frame:
+ *
+ * RegisterMap registers;
+ * if (parser->Parse(buffer, registers) != MdParser::OK)
+ *     much badness;
+ * Metadata metadata;
+ * CamHelper::PopulateMetadata(registers, metadata);
+ *
+ * (Note that the CamHelper class converts to/from exposure lines and time,
+ * and gain_code / actual gain.)
+ *
+ * If you suspect your embedded data may have changed its layout, change any line
+ * lengths, number of lines, bits per pixel etc. that are different, and
+ * then:
+ *
+ * parser->Reset();
+ *
+ * before calling Parse again.
+ */
+
+namespace RPiController {
+
+/* Abstract base class from which other metadata parsers are derived. */
+
+class MdParser
+{
+public:
+	using RegisterMap = std::map<uint32_t, uint32_t>;
+
+	/*
+	 * Parser status codes:
+	 * OK       - success
+	 * NOTFOUND - value such as exposure or gain was not found
+	 * ERROR    - all other errors
+	 */
+	enum Status {
+		OK = 0,
+		NOTFOUND = 1,
+		ERROR = 2
+	};
+
+	MdParser()
+		: reset_(true), bitsPerPixel_(0), numLines_(0), lineLengthBytes_(0)
+	{
+	}
+
+	virtual ~MdParser() = default;
+
+	void reset()
+	{
+		reset_ = true;
+	}
+
+	void setBitsPerPixel(int bpp)
+	{
+		bitsPerPixel_ = bpp;
+	}
+
+	void setNumLines(unsigned int numLines)
+	{
+		numLines_ = numLines;
+	}
+
+	void setLineLengthBytes(unsigned int numBytes)
+	{
+		lineLengthBytes_ = numBytes;
+	}
+
+	virtual Status parse(libcamera::Span<const uint8_t> buffer,
+			     RegisterMap &registers) = 0;
+
+protected:
+	bool reset_;
+	int bitsPerPixel_;
+	unsigned int numLines_;
+	unsigned int lineLengthBytes_;
+};
+
+/*
+ * This isn't a full implementation of a metadata parser for SMIA sensors,
+ * however, it does provide the findRegs function which will prove useful and
+ * make it easier to implement parsers for other SMIA-like sensors (see
+ * md_parser_imx219.cpp for an example).
+ */
+
+class MdParserSmia final : public MdParser
+{
+public:
+	MdParserSmia(std::initializer_list<uint32_t> registerList);
+
+	MdParser::Status parse(libcamera::Span<const uint8_t> buffer,
+			       RegisterMap &registers) override;
+
+private:
+	/* Maps register address to offset in the buffer. */
+	using OffsetMap = std::map<uint32_t, std::optional<uint32_t>>;
+
+	/*
+	 * Note that error codes > 0 are regarded as non-fatal; codes < 0
+	 * indicate a bad data buffer. Status codes are:
+	 * ParseOk     - found all registers, much happiness
+	 * MissingRegs - some registers found; should this be a hard error?
+	 * The remaining codes are all hard errors.
+	 */
+	enum ParseStatus {
+		ParseOk      =  0,
+		MissingRegs  =  1,
+		NoLineStart  = -1,
+		IllegalTag   = -2,
+		BadDummy     = -3,
+		BadLineEnd   = -4,
+		BadPadding   = -5
+	};
+
+	ParseStatus findRegs(libcamera::Span<const uint8_t> buffer);
+
+	OffsetMap offsets_;
+};
+
+} /* namespace RPi */
diff --git a/src/ipa/rpi/cam_helper/md_parser_smia.cpp b/src/ipa/rpi/cam_helper/md_parser_smia.cpp
new file mode 100644
index 00000000..210787ed
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/md_parser_smia.cpp
@@ -0,0 +1,149 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019-2021, Raspberry Pi Ltd
+ *
+ * md_parser_smia.cpp - SMIA specification based embedded data parser
+ */
+
+#include <libcamera/base/log.h>
+#include "md_parser.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+/*
+ * This function goes through the embedded data to find the offsets (not
+ * values!), in the data block, where the values of the given registers can
+ * subsequently be found.
+ *
+ * Embedded data tag bytes, from Sony IMX219 datasheet but general to all SMIA
+ * sensors, I think.
+ */
+
+constexpr unsigned int LineStart = 0x0a;
+constexpr unsigned int LineEndTag = 0x07;
+constexpr unsigned int RegHiBits = 0xaa;
+constexpr unsigned int RegLowBits = 0xa5;
+constexpr unsigned int RegValue = 0x5a;
+constexpr unsigned int RegSkip = 0x55;
+
+MdParserSmia::MdParserSmia(std::initializer_list<uint32_t> registerList)
+{
+	for (auto r : registerList)
+		offsets_[r] = {};
+}
+
+MdParser::Status MdParserSmia::parse(libcamera::Span<const uint8_t> buffer,
+				     RegisterMap &registers)
+{
+	if (reset_) {
+		/*
+		 * Search again through the metadata for all the registers
+		 * requested.
+		 */
+		ASSERT(bitsPerPixel_);
+
+		for (const auto &kv : offsets_)
+			offsets_[kv.first] = {};
+
+		ParseStatus ret = findRegs(buffer);
+		/*
+		 * > 0 means "worked partially but parse again next time",
+		 * < 0 means "hard error".
+		 *
+		 * In either case, we retry parsing on the next frame.
+		 */
+		if (ret != ParseOk)
+			return ERROR;
+
+		reset_ = false;
+	}
+
+	/* Populate the register values requested. */
+	registers.clear();
+	for (const auto &[reg, offset] : offsets_) {
+		if (!offset) {
+			reset_ = true;
+			return NOTFOUND;
+		}
+		registers[reg] = buffer[offset.value()];
+	}
+
+	return OK;
+}
+
+MdParserSmia::ParseStatus MdParserSmia::findRegs(libcamera::Span<const uint8_t> buffer)
+{
+	ASSERT(offsets_.size());
+
+	if (buffer[0] != LineStart)
+		return NoLineStart;
+
+	unsigned int currentOffset = 1; /* after the LineStart */
+	unsigned int currentLineStart = 0, currentLine = 0;
+	unsigned int regNum = 0, regsDone = 0;
+
+	while (1) {
+		int tag = buffer[currentOffset++];
+
+		if ((bitsPerPixel_ == 10 &&
+		     (currentOffset + 1 - currentLineStart) % 5 == 0) ||
+		    (bitsPerPixel_ == 12 &&
+		     (currentOffset + 1 - currentLineStart) % 3 == 0)) {
+			if (buffer[currentOffset++] != RegSkip)
+				return BadDummy;
+		}
+
+		int dataByte = buffer[currentOffset++];
+
+		if (tag == LineEndTag) {
+			if (dataByte != LineEndTag)
+				return BadLineEnd;
+
+			if (numLines_ && ++currentLine == numLines_)
+				return MissingRegs;
+
+			if (lineLengthBytes_) {
+				currentOffset = currentLineStart + lineLengthBytes_;
+
+				/* Require whole line to be in the buffer (if buffer size set). */
+				if (buffer.size() &&
+				    currentOffset + lineLengthBytes_ > buffer.size())
+					return MissingRegs;
+
+				if (buffer[currentOffset] != LineStart)
+					return NoLineStart;
+			} else {
+				/* allow a zero line length to mean "hunt for the next line" */
+				while (currentOffset < buffer.size() &&
+				       buffer[currentOffset] != LineStart)
+					currentOffset++;
+
+				if (currentOffset == buffer.size())
+					return NoLineStart;
+			}
+
+			/* inc currentOffset to after LineStart */
+			currentLineStart = currentOffset++;
+		} else {
+			if (tag == RegHiBits)
+				regNum = (regNum & 0xff) | (dataByte << 8);
+			else if (tag == RegLowBits)
+				regNum = (regNum & 0xff00) | dataByte;
+			else if (tag == RegSkip)
+				regNum++;
+			else if (tag == RegValue) {
+				auto reg = offsets_.find(regNum);
+
+				if (reg != offsets_.end()) {
+					offsets_[regNum] = currentOffset - 1;
+
+					if (++regsDone == offsets_.size())
+						return ParseOk;
+				}
+				regNum++;
+			} else
+				return IllegalTag;
+		}
+	}
+}
diff --git a/src/ipa/rpi/cam_helper/meson.build b/src/ipa/rpi/cam_helper/meson.build
new file mode 100644
index 00000000..bdf2db8e
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/meson.build
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: CC0-1.0
+
+rpi_ipa_cam_helper_sources = files([
+    'cam_helper.cpp',
+    'cam_helper_ov5647.cpp',
+    'cam_helper_imx219.cpp',
+    'cam_helper_imx290.cpp',
+    'cam_helper_imx296.cpp',
+    'cam_helper_imx477.cpp',
+    'cam_helper_imx519.cpp',
+    'cam_helper_imx708.cpp',
+    'cam_helper_ov9281.cpp',
+    'md_parser_smia.cpp',
+])
+
+rpi_ipa_cam_helper_includes = [
+    include_directories('..'),
+]
+
+rpi_ipa_cam_helper_deps = [
+    libcamera_private,
+]
+
+rpi_ipa_cam_helper_lib = static_library('rpi_ipa_cam_helper', rpi_ipa_cam_helper_sources,
+                                        include_directories : rpi_ipa_cam_helper_includes,
+                                        dependencies : rpi_ipa_cam_helper_deps)
diff --git a/src/ipa/rpi/controller/af_algorithm.h b/src/ipa/rpi/controller/af_algorithm.h
new file mode 100644
index 00000000..ad9b5754
--- /dev/null
+++ b/src/ipa/rpi/controller/af_algorithm.h
@@ -0,0 +1,76 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022, Raspberry Pi Ltd
+ *
+ * af_algorithm.hpp - auto focus algorithm interface
+ */
+#pragma once
+
+#include <optional>
+
+#include <libcamera/base/span.h>
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+class AfAlgorithm : public Algorithm
+{
+public:
+	AfAlgorithm(Controller *controller)
+		: Algorithm(controller) {}
+
+	/*
+	 * An autofocus algorithm should provide the following calls.
+	 *
+	 * Where a ControlList combines a change of AfMode with other AF
+	 * controls, setMode() should be called first, to ensure the
+	 * algorithm will be in the correct state to handle controls.
+	 *
+	 * setLensPosition() returns true if the mode was AfModeManual and
+	 * the lens position has changed, otherwise returns false. When it
+	 * returns true, hwpos should be sent immediately to the lens driver.
+	 *
+	 * getMode() is provided mainly for validating controls.
+	 * getLensPosition() is provided for populating DeviceStatus.
+	 */
+
+	enum AfRange { AfRangeNormal = 0,
+		       AfRangeMacro,
+		       AfRangeFull,
+		       AfRangeMax };
+
+	enum AfSpeed { AfSpeedNormal = 0,
+		       AfSpeedFast,
+		       AfSpeedMax };
+
+	enum AfMode { AfModeManual = 0,
+		      AfModeAuto,
+		      AfModeContinuous };
+
+	enum AfPause { AfPauseImmediate = 0,
+		       AfPauseDeferred,
+		       AfPauseResume };
+
+	virtual void setRange([[maybe_unused]] AfRange range)
+	{
+	}
+	virtual void setSpeed([[maybe_unused]] AfSpeed speed)
+	{
+	}
+	virtual void setMetering([[maybe_unused]] bool use_windows)
+	{
+	}
+	virtual void setWindows([[maybe_unused]] libcamera::Span<libcamera::Rectangle const> const &wins)
+	{
+	}
+	virtual void setMode(AfMode mode) = 0;
+	virtual AfMode getMode() const = 0;
+	virtual bool setLensPosition(double dioptres, int32_t *hwpos) = 0;
+	virtual std::optional<double> getLensPosition() const = 0;
+	virtual void triggerScan() = 0;
+	virtual void cancelScan() = 0;
+	virtual void pause(AfPause pause) = 0;
+};
+
+} // namespace RPiController
diff --git a/src/ipa/rpi/controller/af_status.h b/src/ipa/rpi/controller/af_status.h
new file mode 100644
index 00000000..92c08812
--- /dev/null
+++ b/src/ipa/rpi/controller/af_status.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022, Raspberry Pi Ltd
+ *
+ * af_status.h - AF control algorithm status
+ */
+#pragma once
+
+#include <optional>
+
+/*
+ * The AF algorithm should post the following structure into the image's
+ * "af.status" metadata. lensSetting should control the lens.
+ */
+
+enum class AfState {
+	Idle = 0,
+	Scanning,
+	Focused,
+	Failed
+};
+
+enum class AfPauseState {
+	Running = 0,
+	Pausing,
+	Paused
+};
+
+struct AfStatus {
+	/* state for reporting */
+	AfState state;
+	AfPauseState pauseState;
+	/* lensSetting should be sent to the lens driver, when valid */
+	std::optional<int> lensSetting;
+};
diff --git a/src/ipa/rpi/controller/agc_algorithm.h b/src/ipa/rpi/controller/agc_algorithm.h
new file mode 100644
index 00000000..36e6c110
--- /dev/null
+++ b/src/ipa/rpi/controller/agc_algorithm.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * agc_algorithm.h - AGC/AEC control algorithm interface
+ */
+#pragma once
+
+#include <libcamera/base/utils.h>
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+class AgcAlgorithm : public Algorithm
+{
+public:
+	AgcAlgorithm(Controller *controller) : Algorithm(controller) {}
+	/* An AGC algorithm must provide the following: */
+	virtual unsigned int getConvergenceFrames() const = 0;
+	virtual void setEv(double ev) = 0;
+	virtual void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) = 0;
+	virtual void setFixedShutter(libcamera::utils::Duration fixedShutter) = 0;
+	virtual void setMaxShutter(libcamera::utils::Duration maxShutter) = 0;
+	virtual void setFixedAnalogueGain(double fixedAnalogueGain) = 0;
+	virtual void setMeteringMode(std::string const &meteringModeName) = 0;
+	virtual void setExposureMode(std::string const &exposureModeName) = 0;
+	virtual void setConstraintMode(std::string const &contraintModeName) = 0;
+	virtual void enableAuto() = 0;
+	virtual void disableAuto() = 0;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/agc_status.h b/src/ipa/rpi/controller/agc_status.h
new file mode 100644
index 00000000..6abf09d9
--- /dev/null
+++ b/src/ipa/rpi/controller/agc_status.h
@@ -0,0 +1,37 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * agc_status.h - AGC/AEC control algorithm status
+ */
+#pragma once
+
+#include <libcamera/base/utils.h>
+
+/*
+ * The AGC algorithm should post the following structure into the image's
+ * "agc.status" metadata.
+ */
+
+/*
+ * Note: total_exposure_value will be reported as zero until the algorithm has
+ * seen statistics and calculated meaningful values. The contents should be
+ * ignored until then.
+ */
+
+struct AgcStatus {
+	libcamera::utils::Duration totalExposureValue; /* value for all exposure and gain for this image */
+	libcamera::utils::Duration targetExposureValue; /* (unfiltered) target total exposure AGC is aiming for */
+	libcamera::utils::Duration shutterTime;
+	double analogueGain;
+	char exposureMode[32];
+	char constraintMode[32];
+	char meteringMode[32];
+	double ev;
+	libcamera::utils::Duration flickerPeriod;
+	int floatingRegionEnable;
+	libcamera::utils::Duration fixedShutter;
+	double fixedAnalogueGain;
+	double digitalGain;
+	int locked;
+};
diff --git a/src/ipa/rpi/controller/algorithm.cpp b/src/ipa/rpi/controller/algorithm.cpp
new file mode 100644
index 00000000..a957fde5
--- /dev/null
+++ b/src/ipa/rpi/controller/algorithm.cpp
@@ -0,0 +1,56 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * algorithm.cpp - ISP control algorithms
+ */
+
+#include "algorithm.h"
+
+using namespace RPiController;
+
+int Algorithm::read([[maybe_unused]] const libcamera::YamlObject &params)
+{
+	return 0;
+}
+
+void Algorithm::initialise()
+{
+}
+
+void Algorithm::switchMode([[maybe_unused]] CameraMode const &cameraMode,
+			   [[maybe_unused]] Metadata *metadata)
+{
+}
+
+void Algorithm::prepare([[maybe_unused]] Metadata *imageMetadata)
+{
+}
+
+void Algorithm::process([[maybe_unused]] StatisticsPtr &stats,
+			[[maybe_unused]] Metadata *imageMetadata)
+{
+}
+
+/* For registering algorithms with the system: */
+
+namespace {
+
+std::map<std::string, AlgoCreateFunc> &algorithms()
+{
+	static std::map<std::string, AlgoCreateFunc> algorithms;
+	return algorithms;
+}
+
+} /* namespace */
+
+std::map<std::string, AlgoCreateFunc> const &RPiController::getAlgorithms()
+{
+	return algorithms();
+}
+
+RegisterAlgorithm::RegisterAlgorithm(char const *name,
+				     AlgoCreateFunc createFunc)
+{
+	algorithms()[std::string(name)] = createFunc;
+}
diff --git a/src/ipa/rpi/controller/algorithm.h b/src/ipa/rpi/controller/algorithm.h
new file mode 100644
index 00000000..4aa814eb
--- /dev/null
+++ b/src/ipa/rpi/controller/algorithm.h
@@ -0,0 +1,68 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * algorithm.h - ISP control algorithm interface
+ */
+#pragma once
+
+/*
+ * All algorithms should be derived from this class and made available to the
+ * Controller.
+ */
+
+#include <string>
+#include <memory>
+#include <map>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "controller.h"
+
+namespace RPiController {
+
+/* This defines the basic interface for all control algorithms. */
+
+class Algorithm
+{
+public:
+	Algorithm(Controller *controller)
+		: controller_(controller)
+	{
+	}
+	virtual ~Algorithm() = default;
+	virtual char const *name() const = 0;
+	virtual int read(const libcamera::YamlObject &params);
+	virtual void initialise();
+	virtual void switchMode(CameraMode const &cameraMode, Metadata *metadata);
+	virtual void prepare(Metadata *imageMetadata);
+	virtual void process(StatisticsPtr &stats, Metadata *imageMetadata);
+	Metadata &getGlobalMetadata() const
+	{
+		return controller_->getGlobalMetadata();
+	}
+	const std::string &getTarget() const
+	{
+		return controller_->getTarget();
+	}
+	const Controller::HardwareConfig &getHardwareConfig() const
+	{
+		return controller_->getHardwareConfig();
+	}
+
+private:
+	Controller *controller_;
+};
+
+/*
+ * This code is for automatic registration of Front End algorithms with the
+ * system.
+ */
+
+typedef Algorithm *(*AlgoCreateFunc)(Controller *controller);
+struct RegisterAlgorithm {
+	RegisterAlgorithm(char const *name, AlgoCreateFunc createFunc);
+};
+std::map<std::string, AlgoCreateFunc> const &getAlgorithms();
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/alsc_status.h b/src/ipa/rpi/controller/alsc_status.h
new file mode 100644
index 00000000..49a9f4a0
--- /dev/null
+++ b/src/ipa/rpi/controller/alsc_status.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * alsc_status.h - ALSC (auto lens shading correction) control algorithm status
+ */
+#pragma once
+
+#include <vector>
+
+/*
+ * The ALSC algorithm should post the following structure into the image's
+ * "alsc.status" metadata.
+ */
+
+struct AlscStatus {
+	std::vector<double> r;
+	std::vector<double> g;
+	std::vector<double> b;
+	unsigned int rows;
+	unsigned int cols;
+};
diff --git a/src/ipa/rpi/controller/awb_algorithm.h b/src/ipa/rpi/controller/awb_algorithm.h
new file mode 100644
index 00000000..8462c4db
--- /dev/null
+++ b/src/ipa/rpi/controller/awb_algorithm.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * awb_algorithm.h - AWB control algorithm interface
+ */
+#pragma once
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+class AwbAlgorithm : public Algorithm
+{
+public:
+	AwbAlgorithm(Controller *controller) : Algorithm(controller) {}
+	/* An AWB algorithm must provide the following: */
+	virtual unsigned int getConvergenceFrames() const = 0;
+	virtual void setMode(std::string const &modeName) = 0;
+	virtual void setManualGains(double manualR, double manualB) = 0;
+	virtual void enableAuto() = 0;
+	virtual void disableAuto() = 0;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/awb_status.h b/src/ipa/rpi/controller/awb_status.h
new file mode 100644
index 00000000..dd5a79e3
--- /dev/null
+++ b/src/ipa/rpi/controller/awb_status.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * awb_status.h - AWB control algorithm status
+ */
+#pragma once
+
+/*
+ * The AWB algorithm places its results into both the image and global metadata,
+ * under the tag "awb.status".
+ */
+
+struct AwbStatus {
+	char mode[32];
+	double temperatureK;
+	double gainR;
+	double gainG;
+	double gainB;
+};
diff --git a/src/ipa/rpi/controller/black_level_status.h b/src/ipa/rpi/controller/black_level_status.h
new file mode 100644
index 00000000..fd5e4ccb
--- /dev/null
+++ b/src/ipa/rpi/controller/black_level_status.h
@@ -0,0 +1,15 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * black_level_status.h - black level control algorithm status
+ */
+#pragma once
+
+/* The "black level" algorithm stores the black levels to use. */
+
+struct BlackLevelStatus {
+	uint16_t blackLevelR; /* out of 16 bits */
+	uint16_t blackLevelG;
+	uint16_t blackLevelB;
+};
diff --git a/src/ipa/rpi/controller/camera_mode.h b/src/ipa/rpi/controller/camera_mode.h
new file mode 100644
index 00000000..63b11778
--- /dev/null
+++ b/src/ipa/rpi/controller/camera_mode.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019-2020, Raspberry Pi Ltd
+ *
+ * camera_mode.h - description of a particular operating mode of a sensor
+ */
+#pragma once
+
+#include <libcamera/transform.h>
+
+#include <libcamera/base/utils.h>
+
+/*
+ * Description of a "camera mode", holding enough information for control
+ * algorithms to adapt their behaviour to the different modes of the camera,
+ * including binning, scaling, cropping etc.
+ */
+
+struct CameraMode {
+	/* bit depth of the raw camera output */
+	uint32_t bitdepth;
+	/* size in pixels of frames in this mode */
+	uint16_t width;
+	uint16_t height;
+	/* size of full resolution uncropped frame ("sensor frame") */
+	uint16_t sensorWidth;
+	uint16_t sensorHeight;
+	/* binning factor (1 = no binning, 2 = 2-pixel binning etc.) */
+	uint8_t binX;
+	uint8_t binY;
+	/* location of top left pixel in the sensor frame */
+	uint16_t cropX;
+	uint16_t cropY;
+	/* scaling factor (so if uncropped, width*scaleX is sensorWidth) */
+	double scaleX;
+	double scaleY;
+	/* scaling of the noise compared to the native sensor mode */
+	double noiseFactor;
+	/* minimum and maximum line time and frame durations */
+	libcamera::utils::Duration minLineLength;
+	libcamera::utils::Duration maxLineLength;
+	libcamera::utils::Duration minFrameDuration;
+	libcamera::utils::Duration maxFrameDuration;
+	/* any camera transform *not* reflected already in the camera tuning */
+	libcamera::Transform transform;
+	/* minimum and maximum frame lengths in units of lines */
+	uint32_t minFrameLength;
+	uint32_t maxFrameLength;
+	/* sensitivity of this mode */
+	double sensitivity;
+	/* pixel clock rate */
+	uint64_t pixelRate;
+	/* Mode specific shutter speed limits */
+	libcamera::utils::Duration minShutter;
+	libcamera::utils::Duration maxShutter;
+	/* Mode specific analogue gain limits */
+	double minAnalogueGain;
+	double maxAnalogueGain;
+};
diff --git a/src/ipa/rpi/controller/ccm_algorithm.h b/src/ipa/rpi/controller/ccm_algorithm.h
new file mode 100644
index 00000000..e2c4d771
--- /dev/null
+++ b/src/ipa/rpi/controller/ccm_algorithm.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * ccm_algorithm.h - CCM (colour correction matrix) control algorithm interface
+ */
+#pragma once
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+class CcmAlgorithm : public Algorithm
+{
+public:
+	CcmAlgorithm(Controller *controller) : Algorithm(controller) {}
+	/* A CCM algorithm must provide the following: */
+	virtual void setSaturation(double saturation) = 0;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/ccm_status.h b/src/ipa/rpi/controller/ccm_status.h
new file mode 100644
index 00000000..5e28ee7c
--- /dev/null
+++ b/src/ipa/rpi/controller/ccm_status.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * ccm_status.h - CCM (colour correction matrix) control algorithm status
+ */
+#pragma once
+
+/* The "ccm" algorithm generates an appropriate colour matrix. */
+
+struct CcmStatus {
+	double matrix[9];
+	double saturation;
+};
diff --git a/src/ipa/rpi/controller/contrast_algorithm.h b/src/ipa/rpi/controller/contrast_algorithm.h
new file mode 100644
index 00000000..ce17a4f9
--- /dev/null
+++ b/src/ipa/rpi/controller/contrast_algorithm.h
@@ -0,0 +1,22 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * contrast_algorithm.h - contrast (gamma) control algorithm interface
+ */
+#pragma once
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+class ContrastAlgorithm : public Algorithm
+{
+public:
+	ContrastAlgorithm(Controller *controller) : Algorithm(controller) {}
+	/* A contrast algorithm must provide the following: */
+	virtual void setBrightness(double brightness) = 0;
+	virtual void setContrast(double contrast) = 0;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/contrast_status.h b/src/ipa/rpi/controller/contrast_status.h
new file mode 100644
index 00000000..fb9fe4ba
--- /dev/null
+++ b/src/ipa/rpi/controller/contrast_status.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * contrast_status.h - contrast (gamma) control algorithm status
+ */
+#pragma once
+
+#include "pwl.h"
+
+/*
+ * The "contrast" algorithm creates a gamma curve, optionally doing a little bit
+ * of contrast stretching based on the AGC histogram.
+ */
+
+struct ContrastStatus {
+	RPiController::Pwl gammaCurve;
+	double brightness;
+	double contrast;
+};
diff --git a/src/ipa/rpi/controller/controller.cpp b/src/ipa/rpi/controller/controller.cpp
new file mode 100644
index 00000000..fa172113
--- /dev/null
+++ b/src/ipa/rpi/controller/controller.cpp
@@ -0,0 +1,181 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * controller.cpp - ISP controller
+ */
+
+#include <assert.h>
+
+#include <libcamera/base/file.h>
+#include <libcamera/base/log.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "algorithm.h"
+#include "controller.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiController)
+
+static const std::map<std::string, Controller::HardwareConfig> HardwareConfigMap = {
+	{
+		"bcm2835",
+		{
+			/*
+			 * There are only ever 15 AGC regions computed by the firmware
+			 * due to zoning, but the HW defines AGC_REGIONS == 16!
+			 */
+			.agcRegions = { 15 , 1 },
+			.agcZoneWeights = { 15 , 1 },
+			.awbRegions = { 16, 12 },
+			.focusRegions = { 4, 3 },
+			.numHistogramBins = 128,
+			.numGammaPoints = 33,
+			.pipelineWidth = 13
+		}
+	},
+};
+
+Controller::Controller()
+	: switchModeCalled_(false)
+{
+}
+
+Controller::~Controller() {}
+
+int Controller::read(char const *filename)
+{
+	File file(filename);
+	if (!file.open(File::OpenModeFlag::ReadOnly)) {
+		LOG(RPiController, Warning)
+			<< "Failed to open tuning file '" << filename << "'";
+		return -EINVAL;
+	}
+
+	std::unique_ptr<YamlObject> root = YamlParser::parse(file);
+	double version = (*root)["version"].get<double>(1.0);
+	target_ = (*root)["target"].get<std::string>("bcm2835");
+
+	if (version < 2.0) {
+		LOG(RPiController, Warning)
+			<< "This format of the tuning file will be deprecated soon!"
+			<< " Please use the convert_tuning.py utility to update to version 2.0.";
+
+		for (auto const &[key, value] : root->asDict()) {
+			int ret = createAlgorithm(key, value);
+			if (ret)
+				return ret;
+		}
+	} else if (version < 3.0) {
+		if (!root->contains("algorithms")) {
+			LOG(RPiController, Error)
+				<< "Tuning file " << filename
+				<< " does not have an \"algorithms\" list!";
+			return -EINVAL;
+		}
+
+		for (auto const &rootAlgo : (*root)["algorithms"].asList())
+			for (auto const &[key, value] : rootAlgo.asDict()) {
+				int ret = createAlgorithm(key, value);
+				if (ret)
+					return ret;
+			}
+	} else {
+		LOG(RPiController, Error)
+			<< "Unrecognised version " << version
+			<< " for the tuning file " << filename;
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+int Controller::createAlgorithm(const std::string &name, const YamlObject &params)
+{
+	auto it = getAlgorithms().find(name);
+	if (it == getAlgorithms().end()) {
+		LOG(RPiController, Warning)
+			<< "No algorithm found for \"" << name << "\"";
+		return 0;
+	}
+
+	Algorithm *algo = (*it->second)(this);
+	int ret = algo->read(params);
+	if (ret)
+		return ret;
+
+	algorithms_.push_back(AlgorithmPtr(algo));
+	return 0;
+}
+
+void Controller::initialise()
+{
+	for (auto &algo : algorithms_)
+		algo->initialise();
+}
+
+void Controller::switchMode(CameraMode const &cameraMode, Metadata *metadata)
+{
+	for (auto &algo : algorithms_)
+		algo->switchMode(cameraMode, metadata);
+	switchModeCalled_ = true;
+}
+
+void Controller::prepare(Metadata *imageMetadata)
+{
+	assert(switchModeCalled_);
+	for (auto &algo : algorithms_)
+		algo->prepare(imageMetadata);
+}
+
+void Controller::process(StatisticsPtr stats, Metadata *imageMetadata)
+{
+	assert(switchModeCalled_);
+	for (auto &algo : algorithms_)
+		algo->process(stats, imageMetadata);
+}
+
+Metadata &Controller::getGlobalMetadata()
+{
+	return globalMetadata_;
+}
+
+Algorithm *Controller::getAlgorithm(std::string const &name) const
+{
+	/*
+	 * The passed name must be the entire algorithm name, or must match the
+	 * last part of it with a period (.) just before.
+	 */
+	size_t nameLen = name.length();
+	for (auto &algo : algorithms_) {
+		char const *algoName = algo->name();
+		size_t algoNameLen = strlen(algoName);
+		if (algoNameLen >= nameLen &&
+		    strcasecmp(name.c_str(),
+			       algoName + algoNameLen - nameLen) == 0 &&
+		    (nameLen == algoNameLen ||
+		     algoName[algoNameLen - nameLen - 1] == '.'))
+			return algo.get();
+	}
+	return nullptr;
+}
+
+const std::string &Controller::getTarget() const
+{
+	return target_;
+}
+
+const Controller::HardwareConfig &Controller::getHardwareConfig() const
+{
+	auto cfg = HardwareConfigMap.find(getTarget());
+
+	/*
+	 * This really should not happen, the IPA ought to validate the target
+	 * on initialisation.
+	 */
+	ASSERT(cfg != HardwareConfigMap.end());
+	return cfg->second;
+}
diff --git a/src/ipa/rpi/controller/controller.h b/src/ipa/rpi/controller/controller.h
new file mode 100644
index 00000000..c6af5cd6
--- /dev/null
+++ b/src/ipa/rpi/controller/controller.h
@@ -0,0 +1,73 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * controller.h - ISP controller interface
+ */
+#pragma once
+
+/*
+ * The Controller is simply a container for a collecting together a number of
+ * "control algorithms" (such as AWB etc.) and for running them all in a
+ * convenient manner.
+ */
+
+#include <vector>
+#include <string>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "camera_mode.h"
+#include "device_status.h"
+#include "metadata.h"
+#include "statistics.h"
+
+namespace RPiController {
+
+class Algorithm;
+typedef std::unique_ptr<Algorithm> AlgorithmPtr;
+
+/*
+ * The Controller holds a pointer to some global_metadata, which is how
+ * different controllers and control algorithms within them can exchange
+ * information. The Prepare function returns a pointer to metadata for this
+ * specific image, and which should be passed on to the Process function.
+ */
+
+class Controller
+{
+public:
+	struct HardwareConfig {
+		libcamera::Size agcRegions;
+		libcamera::Size agcZoneWeights;
+		libcamera::Size awbRegions;
+		libcamera::Size focusRegions;
+		unsigned int numHistogramBins;
+		unsigned int numGammaPoints;
+		unsigned int pipelineWidth;
+	};
+
+	Controller();
+	~Controller();
+	int read(char const *filename);
+	void initialise();
+	void switchMode(CameraMode const &cameraMode, Metadata *metadata);
+	void prepare(Metadata *imageMetadata);
+	void process(StatisticsPtr stats, Metadata *imageMetadata);
+	Metadata &getGlobalMetadata();
+	Algorithm *getAlgorithm(std::string const &name) const;
+	const std::string &getTarget() const;
+	const HardwareConfig &getHardwareConfig() const;
+
+protected:
+	int createAlgorithm(const std::string &name, const libcamera::YamlObject &params);
+
+	Metadata globalMetadata_;
+	std::vector<AlgorithmPtr> algorithms_;
+	bool switchModeCalled_;
+
+private:
+	std::string target_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/denoise_algorithm.h b/src/ipa/rpi/controller/denoise_algorithm.h
new file mode 100644
index 00000000..52009ba9
--- /dev/null
+++ b/src/ipa/rpi/controller/denoise_algorithm.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2021, Raspberry Pi Ltd
+ *
+ * denoise.h - Denoise control algorithm interface
+ */
+#pragma once
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+enum class DenoiseMode { Off, ColourOff, ColourFast, ColourHighQuality };
+
+class DenoiseAlgorithm : public Algorithm
+{
+public:
+	DenoiseAlgorithm(Controller *controller) : Algorithm(controller) {}
+	/* A Denoise algorithm must provide the following: */
+	virtual void setMode(DenoiseMode mode) = 0;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/denoise_status.h b/src/ipa/rpi/controller/denoise_status.h
new file mode 100644
index 00000000..f6b9ee29
--- /dev/null
+++ b/src/ipa/rpi/controller/denoise_status.h
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019-2021, Raspberry Pi Ltd
+ *
+ * denoise_status.h - Denoise control algorithm status
+ */
+#pragma once
+
+/* This stores the parameters required for Denoise. */
+
+struct DenoiseStatus {
+	double noiseConstant;
+	double noiseSlope;
+	double strength;
+	unsigned int mode;
+};
diff --git a/src/ipa/rpi/controller/device_status.cpp b/src/ipa/rpi/controller/device_status.cpp
new file mode 100644
index 00000000..c907efdd
--- /dev/null
+++ b/src/ipa/rpi/controller/device_status.cpp
@@ -0,0 +1,31 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2021, Raspberry Pi Ltd
+ *
+ * device_status.cpp - device (image sensor) status
+ */
+#include "device_status.h"
+
+using namespace libcamera; /* for the Duration operator<< overload */
+
+std::ostream &operator<<(std::ostream &out, const DeviceStatus &d)
+{
+	out << "Exposure: " << d.shutterSpeed
+	    << " Frame length: " << d.frameLength
+	    << " Line length: " << d.lineLength
+	    << " Gain: " << d.analogueGain;
+
+	if (d.aperture)
+		out << " Aperture: " << *d.aperture;
+
+	if (d.lensPosition)
+		out << " Lens: " << *d.lensPosition;
+
+	if (d.flashIntensity)
+		out << " Flash: " << *d.flashIntensity;
+
+	if (d.sensorTemperature)
+		out << " Temperature: " << *d.sensorTemperature;
+
+	return out;
+}
diff --git a/src/ipa/rpi/controller/device_status.h b/src/ipa/rpi/controller/device_status.h
new file mode 100644
index 00000000..c45db749
--- /dev/null
+++ b/src/ipa/rpi/controller/device_status.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019-2021, Raspberry Pi Ltd
+ *
+ * device_status.h - device (image sensor) status
+ */
+#pragma once
+
+#include <iostream>
+#include <optional>
+
+#include <libcamera/base/utils.h>
+
+/*
+ * Definition of "device metadata" which stores things like shutter time and
+ * analogue gain that downstream control algorithms will want to know.
+ */
+
+struct DeviceStatus {
+	DeviceStatus()
+		: shutterSpeed(std::chrono::seconds(0)), frameLength(0),
+		  lineLength(std::chrono::seconds(0)), analogueGain(0.0)
+	{
+	}
+
+	friend std::ostream &operator<<(std::ostream &out, const DeviceStatus &d);
+
+	/* time shutter is open */
+	libcamera::utils::Duration shutterSpeed;
+	/* frame length given in number of lines */
+	uint32_t frameLength;
+	/* line length for the current frame */
+	libcamera::utils::Duration lineLength;
+	double analogueGain;
+	/* 1.0/distance-in-metres */
+	std::optional<double> lensPosition;
+	/* 1/f so that brightness quadruples when this doubles */
+	std::optional<double> aperture;
+	/* proportional to brightness with 0 = no flash, 1 = maximum flash */
+	std::optional<double> flashIntensity;
+	/* Sensor reported temperature value (in degrees) */
+	std::optional<double> sensorTemperature;
+};
diff --git a/src/ipa/rpi/controller/dpc_status.h b/src/ipa/rpi/controller/dpc_status.h
new file mode 100644
index 00000000..46d0cf34
--- /dev/null
+++ b/src/ipa/rpi/controller/dpc_status.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * dpc_status.h - DPC (defective pixel correction) control algorithm status
+ */
+#pragma once
+
+/* The "DPC" algorithm sets defective pixel correction strength. */
+
+struct DpcStatus {
+	int strength; /* 0 = "off", 1 = "normal", 2 = "strong" */
+};
diff --git a/src/ipa/rpi/controller/geq_status.h b/src/ipa/rpi/controller/geq_status.h
new file mode 100644
index 00000000..2d749fc9
--- /dev/null
+++ b/src/ipa/rpi/controller/geq_status.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * geq_status.h - GEQ (green equalisation) control algorithm status
+ */
+#pragma once
+
+/* The "GEQ" algorithm calculates the green equalisation thresholds */
+
+struct GeqStatus {
+	uint16_t offset;
+	double slope;
+};
diff --git a/src/ipa/rpi/controller/histogram.cpp b/src/ipa/rpi/controller/histogram.cpp
new file mode 100644
index 00000000..16a9207f
--- /dev/null
+++ b/src/ipa/rpi/controller/histogram.cpp
@@ -0,0 +1,64 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * histogram.cpp - histogram calculations
+ */
+#include <math.h>
+#include <stdio.h>
+
+#include "histogram.h"
+
+using namespace RPiController;
+
+uint64_t Histogram::cumulativeFreq(double bin) const
+{
+	if (bin <= 0)
+		return 0;
+	else if (bin >= bins())
+		return total();
+	int b = (int)bin;
+	return cumulative_[b] +
+	       (bin - b) * (cumulative_[b + 1] - cumulative_[b]);
+}
+
+double Histogram::quantile(double q, int first, int last) const
+{
+	if (first == -1)
+		first = 0;
+	if (last == -1)
+		last = cumulative_.size() - 2;
+	assert(first <= last);
+	uint64_t items = q * total();
+	while (first < last) /* binary search to find the right bin */
+	{
+		int middle = (first + last) / 2;
+		if (cumulative_[middle + 1] > items)
+			last = middle; /* between first and middle */
+		else
+			first = middle + 1; /* after middle */
+	}
+	assert(items >= cumulative_[first] && items <= cumulative_[last + 1]);
+	double frac = cumulative_[first + 1] == cumulative_[first] ? 0
+		      : (double)(items - cumulative_[first]) /
+				  (cumulative_[first + 1] - cumulative_[first]);
+	return first + frac;
+}
+
+double Histogram::interQuantileMean(double qLo, double qHi) const
+{
+	assert(qHi > qLo);
+	double pLo = quantile(qLo);
+	double pHi = quantile(qHi, (int)pLo);
+	double sumBinFreq = 0, cumulFreq = 0;
+	for (double pNext = floor(pLo) + 1.0; pNext <= ceil(pHi);
+	     pLo = pNext, pNext += 1.0) {
+		int bin = floor(pLo);
+		double freq = (cumulative_[bin + 1] - cumulative_[bin]) *
+			      (std::min(pNext, pHi) - pLo);
+		sumBinFreq += bin * freq;
+		cumulFreq += freq;
+	}
+	/* add 0.5 to give an average for bin mid-points */
+	return sumBinFreq / cumulFreq + 0.5;
+}
diff --git a/src/ipa/rpi/controller/histogram.h b/src/ipa/rpi/controller/histogram.h
new file mode 100644
index 00000000..6b3e3a9e
--- /dev/null
+++ b/src/ipa/rpi/controller/histogram.h
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * histogram.h - histogram calculation interface
+ */
+#pragma once
+
+#include <stdint.h>
+#include <vector>
+#include <cassert>
+
+/*
+ * A simple histogram class, for use in particular to find "quantiles" and
+ * averages between "quantiles".
+ */
+
+namespace RPiController {
+
+class Histogram
+{
+public:
+	Histogram()
+	{
+		cumulative_.push_back(0);
+	}
+
+	template<typename T> Histogram(T *histogram, int num)
+	{
+		assert(num);
+		cumulative_.reserve(num + 1);
+		cumulative_.push_back(0);
+		for (int i = 0; i < num; i++)
+			cumulative_.push_back(cumulative_.back() +
+					      histogram[i]);
+	}
+	uint32_t bins() const { return cumulative_.size() - 1; }
+	uint64_t total() const { return cumulative_[cumulative_.size() - 1]; }
+	/* Cumulative frequency up to a (fractional) point in a bin. */
+	uint64_t cumulativeFreq(double bin) const;
+	/*
+	 * Return the (fractional) bin of the point q (0 <= q <= 1) through the
+	 * histogram. Optionally provide limits to help.
+	 */
+	double quantile(double q, int first = -1, int last = -1) const;
+	/* Return the average histogram bin value between the two quantiles. */
+	double interQuantileMean(double qLo, double qHi) const;
+
+private:
+	std::vector<uint64_t> cumulative_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/lux_status.h b/src/ipa/rpi/controller/lux_status.h
new file mode 100644
index 00000000..5eb9faac
--- /dev/null
+++ b/src/ipa/rpi/controller/lux_status.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * lux_status.h - Lux control algorithm status
+ */
+#pragma once
+
+/*
+ * The "lux" algorithm looks at the (AGC) histogram statistics of the frame and
+ * estimates the current lux level of the scene. It does this by a simple ratio
+ * calculation comparing to a reference image that was taken in known conditions
+ * with known statistics and a properly measured lux level. There is a slight
+ * problem with aperture, in that it may be variable without the system knowing
+ * or being aware of it. In this case an external application may set a
+ * "current_aperture" value if it wishes, which would be used in place of the
+ * (presumably meaningless) value in the image metadata.
+ */
+
+struct LuxStatus {
+	double lux;
+	double aperture;
+};
diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build
new file mode 100644
index 00000000..feb0334e
--- /dev/null
+++ b/src/ipa/rpi/controller/meson.build
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: CC0-1.0
+
+rpi_ipa_controller_sources = files([
+    'algorithm.cpp',
+    'controller.cpp',
+    'device_status.cpp',
+    'histogram.cpp',
+    'pwl.cpp',
+    'rpi/af.cpp',
+    'rpi/agc.cpp',
+    'rpi/alsc.cpp',
+    'rpi/awb.cpp',
+    'rpi/black_level.cpp',
+    'rpi/ccm.cpp',
+    'rpi/contrast.cpp',
+    'rpi/dpc.cpp',
+    'rpi/geq.cpp',
+    'rpi/lux.cpp',
+    'rpi/noise.cpp',
+    'rpi/sdn.cpp',
+    'rpi/sharpen.cpp',
+])
+
+rpi_ipa_controller_deps = [
+    libcamera_private,
+]
+
+rpi_ipa_controller_lib = static_library('rpi_ipa_controller', rpi_ipa_controller_sources,
+                                        dependencies : rpi_ipa_controller_deps)
diff --git a/src/ipa/rpi/controller/metadata.h b/src/ipa/rpi/controller/metadata.h
new file mode 100644
index 00000000..bf8a2393
--- /dev/null
+++ b/src/ipa/rpi/controller/metadata.h
@@ -0,0 +1,126 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019-2021, Raspberry Pi Ltd
+ *
+ * metadata.h - general metadata class
+ */
+#pragma once
+
+/* A simple class for carrying arbitrary metadata, for example about an image. */
+
+#include <any>
+#include <map>
+#include <mutex>
+#include <string>
+
+#include <libcamera/base/thread_annotations.h>
+
+namespace RPiController {
+
+class LIBCAMERA_TSA_CAPABILITY("mutex") Metadata
+{
+public:
+	Metadata() = default;
+
+	Metadata(Metadata const &other)
+	{
+		std::scoped_lock otherLock(other.mutex_);
+		data_ = other.data_;
+	}
+
+	Metadata(Metadata &&other)
+	{
+		std::scoped_lock otherLock(other.mutex_);
+		data_ = std::move(other.data_);
+		other.data_.clear();
+	}
+
+	template<typename T>
+	void set(std::string const &tag, T const &value)
+	{
+		std::scoped_lock lock(mutex_);
+		data_[tag] = value;
+	}
+
+	template<typename T>
+	int get(std::string const &tag, T &value) const
+	{
+		std::scoped_lock lock(mutex_);
+		auto it = data_.find(tag);
+		if (it == data_.end())
+			return -1;
+		value = std::any_cast<T>(it->second);
+		return 0;
+	}
+
+	void clear()
+	{
+		std::scoped_lock lock(mutex_);
+		data_.clear();
+	}
+
+	Metadata &operator=(Metadata const &other)
+	{
+		std::scoped_lock lock(mutex_, other.mutex_);
+		data_ = other.data_;
+		return *this;
+	}
+
+	Metadata &operator=(Metadata &&other)
+	{
+		std::scoped_lock lock(mutex_, other.mutex_);
+		data_ = std::move(other.data_);
+		other.data_.clear();
+		return *this;
+	}
+
+	void merge(Metadata &other)
+	{
+		std::scoped_lock lock(mutex_, other.mutex_);
+		data_.merge(other.data_);
+	}
+
+	void mergeCopy(const Metadata &other)
+	{
+		std::scoped_lock lock(mutex_, other.mutex_);
+		/*
+		 * If the metadata key exists, ignore this item and copy only
+		 * unique key/value pairs.
+		 */
+		data_.insert(other.data_.begin(), other.data_.end());
+	}
+
+	template<typename T>
+	T *getLocked(std::string const &tag)
+	{
+		/*
+		 * This allows in-place access to the Metadata contents,
+		 * for which you should be holding the lock.
+		 */
+		auto it = data_.find(tag);
+		if (it == data_.end())
+			return nullptr;
+		return std::any_cast<T>(&it->second);
+	}
+
+	template<typename T>
+	void setLocked(std::string const &tag, T const &value)
+	{
+		/* Use this only if you're holding the lock yourself. */
+		data_[tag] = value;
+	}
+
+	/*
+	 * Note: use of (lowercase) lock and unlock means you can create scoped
+	 * locks with the standard lock classes.
+	 * e.g. std::lock_guard<RPiController::Metadata> lock(metadata)
+	 */
+	void lock() LIBCAMERA_TSA_ACQUIRE() { mutex_.lock(); }
+	void unlock() LIBCAMERA_TSA_RELEASE() { mutex_.unlock(); }
+
+private:
+	mutable std::mutex mutex_;
+	std::map<std::string, std::any> data_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/noise_status.h b/src/ipa/rpi/controller/noise_status.h
new file mode 100644
index 00000000..da194f71
--- /dev/null
+++ b/src/ipa/rpi/controller/noise_status.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * noise_status.h - Noise control algorithm status
+ */
+#pragma once
+
+/* The "noise" algorithm stores an estimate of the noise profile for this image. */
+
+struct NoiseStatus {
+	double noiseConstant;
+	double noiseSlope;
+};
diff --git a/src/ipa/rpi/controller/pdaf_data.h b/src/ipa/rpi/controller/pdaf_data.h
new file mode 100644
index 00000000..470510f2
--- /dev/null
+++ b/src/ipa/rpi/controller/pdaf_data.h
@@ -0,0 +1,24 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022, Raspberry Pi Ltd
+ *
+ * pdaf_data.h - PDAF Metadata
+ */
+#pragma once
+
+#include <stdint.h>
+
+#include "region_stats.h"
+
+namespace RPiController {
+
+struct PdafData {
+	/* Confidence, in arbitrary units */
+	uint16_t conf;
+	/* Phase error, in s16 Q4 format (S.11.4) */
+	int16_t phase;
+};
+
+using PdafRegions = RegionStats<PdafData>;
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/pwl.cpp b/src/ipa/rpi/controller/pwl.cpp
new file mode 100644
index 00000000..70c2e24b
--- /dev/null
+++ b/src/ipa/rpi/controller/pwl.cpp
@@ -0,0 +1,269 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * pwl.cpp - piecewise linear functions
+ */
+
+#include <cassert>
+#include <cmath>
+#include <stdexcept>
+
+#include "pwl.h"
+
+using namespace RPiController;
+
+int Pwl::read(const libcamera::YamlObject &params)
+{
+	if (!params.size() || params.size() % 2)
+		return -EINVAL;
+
+	const auto &list = params.asList();
+
+	for (auto it = list.begin(); it != list.end(); it++) {
+		auto x = it->get<double>();
+		if (!x)
+			return -EINVAL;
+		if (it != list.begin() && *x <= points_.back().x)
+			return -EINVAL;
+
+		auto y = (++it)->get<double>();
+		if (!y)
+			return -EINVAL;
+
+		points_.push_back(Point(*x, *y));
+	}
+
+	return 0;
+}
+
+void Pwl::append(double x, double y, const double eps)
+{
+	if (points_.empty() || points_.back().x + eps < x)
+		points_.push_back(Point(x, y));
+}
+
+void Pwl::prepend(double x, double y, const double eps)
+{
+	if (points_.empty() || points_.front().x - eps > x)
+		points_.insert(points_.begin(), Point(x, y));
+}
+
+Pwl::Interval Pwl::domain() const
+{
+	return Interval(points_[0].x, points_[points_.size() - 1].x);
+}
+
+Pwl::Interval Pwl::range() const
+{
+	double lo = points_[0].y, hi = lo;
+	for (auto &p : points_)
+		lo = std::min(lo, p.y), hi = std::max(hi, p.y);
+	return Interval(lo, hi);
+}
+
+bool Pwl::empty() const
+{
+	return points_.empty();
+}
+
+double Pwl::eval(double x, int *spanPtr, bool updateSpan) const
+{
+	int span = findSpan(x, spanPtr && *spanPtr != -1 ? *spanPtr : points_.size() / 2 - 1);
+	if (spanPtr && updateSpan)
+		*spanPtr = span;
+	return points_[span].y +
+	       (x - points_[span].x) * (points_[span + 1].y - points_[span].y) /
+		       (points_[span + 1].x - points_[span].x);
+}
+
+int Pwl::findSpan(double x, int span) const
+{
+	/*
+	 * Pwls are generally small, so linear search may well be faster than
+	 * binary, though could review this if large PWls start turning up.
+	 */
+	int lastSpan = points_.size() - 2;
+	/*
+	 * some algorithms may call us with span pointing directly at the last
+	 * control point
+	 */
+	span = std::max(0, std::min(lastSpan, span));
+	while (span < lastSpan && x >= points_[span + 1].x)
+		span++;
+	while (span && x < points_[span].x)
+		span--;
+	return span;
+}
+
+Pwl::PerpType Pwl::invert(Point const &xy, Point &perp, int &span,
+			  const double eps) const
+{
+	assert(span >= -1);
+	bool prevOffEnd = false;
+	for (span = span + 1; span < (int)points_.size() - 1; span++) {
+		Point spanVec = points_[span + 1] - points_[span];
+		double t = ((xy - points_[span]) % spanVec) / spanVec.len2();
+		if (t < -eps) /* off the start of this span */
+		{
+			if (span == 0) {
+				perp = points_[span];
+				return PerpType::Start;
+			} else if (prevOffEnd) {
+				perp = points_[span];
+				return PerpType::Vertex;
+			}
+		} else if (t > 1 + eps) /* off the end of this span */
+		{
+			if (span == (int)points_.size() - 2) {
+				perp = points_[span + 1];
+				return PerpType::End;
+			}
+			prevOffEnd = true;
+		} else /* a true perpendicular */
+		{
+			perp = points_[span] + spanVec * t;
+			return PerpType::Perpendicular;
+		}
+	}
+	return PerpType::None;
+}
+
+Pwl Pwl::inverse(bool *trueInverse, const double eps) const
+{
+	bool appended = false, prepended = false, neither = false;
+	Pwl inverse;
+
+	for (Point const &p : points_) {
+		if (inverse.empty())
+			inverse.append(p.y, p.x, eps);
+		else if (std::abs(inverse.points_.back().x - p.y) <= eps ||
+			 std::abs(inverse.points_.front().x - p.y) <= eps)
+			/* do nothing */;
+		else if (p.y > inverse.points_.back().x) {
+			inverse.append(p.y, p.x, eps);
+			appended = true;
+		} else if (p.y < inverse.points_.front().x) {
+			inverse.prepend(p.y, p.x, eps);
+			prepended = true;
+		} else
+			neither = true;
+	}
+
+	/*
+	 * This is not a proper inverse if we found ourselves putting points
+	 * onto both ends of the inverse, or if there were points that couldn't
+	 * go on either.
+	 */
+	if (trueInverse)
+		*trueInverse = !(neither || (appended && prepended));
+
+	return inverse;
+}
+
+Pwl Pwl::compose(Pwl const &other, const double eps) const
+{
+	double thisX = points_[0].x, thisY = points_[0].y;
+	int thisSpan = 0, otherSpan = other.findSpan(thisY, 0);
+	Pwl result({ { thisX, other.eval(thisY, &otherSpan, false) } });
+	while (thisSpan != (int)points_.size() - 1) {
+		double dx = points_[thisSpan + 1].x - points_[thisSpan].x,
+		       dy = points_[thisSpan + 1].y - points_[thisSpan].y;
+		if (std::abs(dy) > eps &&
+		    otherSpan + 1 < (int)other.points_.size() &&
+		    points_[thisSpan + 1].y >=
+			    other.points_[otherSpan + 1].x + eps) {
+			/*
+			 * next control point in result will be where this
+			 * function's y reaches the next span in other
+			 */
+			thisX = points_[thisSpan].x +
+				(other.points_[otherSpan + 1].x -
+				 points_[thisSpan].y) *
+					dx / dy;
+			thisY = other.points_[++otherSpan].x;
+		} else if (std::abs(dy) > eps && otherSpan > 0 &&
+			   points_[thisSpan + 1].y <=
+				   other.points_[otherSpan - 1].x - eps) {
+			/*
+			 * next control point in result will be where this
+			 * function's y reaches the previous span in other
+			 */
+			thisX = points_[thisSpan].x +
+				(other.points_[otherSpan + 1].x -
+				 points_[thisSpan].y) *
+					dx / dy;
+			thisY = other.points_[--otherSpan].x;
+		} else {
+			/* we stay in the same span in other */
+			thisSpan++;
+			thisX = points_[thisSpan].x,
+			thisY = points_[thisSpan].y;
+		}
+		result.append(thisX, other.eval(thisY, &otherSpan, false),
+			      eps);
+	}
+	return result;
+}
+
+void Pwl::map(std::function<void(double x, double y)> f) const
+{
+	for (auto &pt : points_)
+		f(pt.x, pt.y);
+}
+
+void Pwl::map2(Pwl const &pwl0, Pwl const &pwl1,
+	       std::function<void(double x, double y0, double y1)> f)
+{
+	int span0 = 0, span1 = 0;
+	double x = std::min(pwl0.points_[0].x, pwl1.points_[0].x);
+	f(x, pwl0.eval(x, &span0, false), pwl1.eval(x, &span1, false));
+	while (span0 < (int)pwl0.points_.size() - 1 ||
+	       span1 < (int)pwl1.points_.size() - 1) {
+		if (span0 == (int)pwl0.points_.size() - 1)
+			x = pwl1.points_[++span1].x;
+		else if (span1 == (int)pwl1.points_.size() - 1)
+			x = pwl0.points_[++span0].x;
+		else if (pwl0.points_[span0 + 1].x > pwl1.points_[span1 + 1].x)
+			x = pwl1.points_[++span1].x;
+		else
+			x = pwl0.points_[++span0].x;
+		f(x, pwl0.eval(x, &span0, false), pwl1.eval(x, &span1, false));
+	}
+}
+
+Pwl Pwl::combine(Pwl const &pwl0, Pwl const &pwl1,
+		 std::function<double(double x, double y0, double y1)> f,
+		 const double eps)
+{
+	Pwl result;
+	map2(pwl0, pwl1, [&](double x, double y0, double y1) {
+		result.append(x, f(x, y0, y1), eps);
+	});
+	return result;
+}
+
+void Pwl::matchDomain(Interval const &domain, bool clip, const double eps)
+{
+	int span = 0;
+	prepend(domain.start, eval(clip ? points_[0].x : domain.start, &span),
+		eps);
+	span = points_.size() - 2;
+	append(domain.end, eval(clip ? points_.back().x : domain.end, &span),
+	       eps);
+}
+
+Pwl &Pwl::operator*=(double d)
+{
+	for (auto &pt : points_)
+		pt.y *= d;
+	return *this;
+}
+
+void Pwl::debug(FILE *fp) const
+{
+	fprintf(fp, "Pwl {\n");
+	for (auto &p : points_)
+		fprintf(fp, "\t(%g, %g)\n", p.x, p.y);
+	fprintf(fp, "}\n");
+}
diff --git a/src/ipa/rpi/controller/pwl.h b/src/ipa/rpi/controller/pwl.h
new file mode 100644
index 00000000..aacf6039
--- /dev/null
+++ b/src/ipa/rpi/controller/pwl.h
@@ -0,0 +1,127 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * pwl.h - piecewise linear functions interface
+ */
+#pragma once
+
+#include <functional>
+#include <math.h>
+#include <vector>
+
+#include "libcamera/internal/yaml_parser.h"
+
+namespace RPiController {
+
+class Pwl
+{
+public:
+	struct Interval {
+		Interval(double _start, double _end)
+			: start(_start), end(_end)
+		{
+		}
+		double start, end;
+		bool contains(double value)
+		{
+			return value >= start && value <= end;
+		}
+		double clip(double value)
+		{
+			return value < start ? start
+					     : (value > end ? end : value);
+		}
+		double len() const { return end - start; }
+	};
+	struct Point {
+		Point() : x(0), y(0) {}
+		Point(double _x, double _y)
+			: x(_x), y(_y) {}
+		double x, y;
+		Point operator-(Point const &p) const
+		{
+			return Point(x - p.x, y - p.y);
+		}
+		Point operator+(Point const &p) const
+		{
+			return Point(x + p.x, y + p.y);
+		}
+		double operator%(Point const &p) const
+		{
+			return x * p.x + y * p.y;
+		}
+		Point operator*(double f) const { return Point(x * f, y * f); }
+		Point operator/(double f) const { return Point(x / f, y / f); }
+		double len2() const { return x * x + y * y; }
+		double len() const { return sqrt(len2()); }
+	};
+	Pwl() {}
+	Pwl(std::vector<Point> const &points) : points_(points) {}
+	int read(const libcamera::YamlObject &params);
+	void append(double x, double y, const double eps = 1e-6);
+	void prepend(double x, double y, const double eps = 1e-6);
+	Interval domain() const;
+	Interval range() const;
+	bool empty() const;
+	/*
+	 * Evaluate Pwl, optionally supplying an initial guess for the
+	 * "span". The "span" may be optionally be updated.  If you want to know
+	 * the "span" value but don't have an initial guess you can set it to
+	 * -1.
+	 */
+	double eval(double x, int *spanPtr = nullptr,
+		    bool updateSpan = true) const;
+	/*
+	 * Find perpendicular closest to xy, starting from span+1 so you can
+	 * call it repeatedly to check for multiple closest points (set span to
+	 * -1 on the first call). Also returns "pseudo" perpendiculars; see
+	 * PerpType enum.
+	 */
+	enum class PerpType {
+		None, /* no perpendicular found */
+		Start, /* start of Pwl is closest point */
+		End, /* end of Pwl is closest point */
+		Vertex, /* vertex of Pwl is closest point */
+		Perpendicular /* true perpendicular found */
+	};
+	PerpType invert(Point const &xy, Point &perp, int &span,
+			const double eps = 1e-6) const;
+	/*
+	 * Compute the inverse function. Indicate if it is a proper (true)
+	 * inverse, or only a best effort (e.g. input was non-monotonic).
+	 */
+	Pwl inverse(bool *trueInverse = nullptr, const double eps = 1e-6) const;
+	/* Compose two Pwls together, doing "this" first and "other" after. */
+	Pwl compose(Pwl const &other, const double eps = 1e-6) const;
+	/* Apply function to (x,y) values at every control point. */
+	void map(std::function<void(double x, double y)> f) const;
+	/*
+	 * Apply function to (x, y0, y1) values wherever either Pwl has a
+	 * control point.
+	 */
+	static void map2(Pwl const &pwl0, Pwl const &pwl1,
+			 std::function<void(double x, double y0, double y1)> f);
+	/*
+	 * Combine two Pwls, meaning we create a new Pwl where the y values are
+	 * given by running f wherever either has a knot.
+	 */
+	static Pwl
+	combine(Pwl const &pwl0, Pwl const &pwl1,
+		std::function<double(double x, double y0, double y1)> f,
+		const double eps = 1e-6);
+	/*
+	 * Make "this" match (at least) the given domain. Any extension my be
+	 * clipped or linear.
+	 */
+	void matchDomain(Interval const &domain, bool clip = true,
+			 const double eps = 1e-6);
+	Pwl &operator*=(double d);
+	void debug(FILE *fp = stdout) const;
+
+private:
+	int findSpan(double x, int span) const;
+	std::vector<Point> points_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/region_stats.h b/src/ipa/rpi/controller/region_stats.h
new file mode 100644
index 00000000..a8860dc8
--- /dev/null
+++ b/src/ipa/rpi/controller/region_stats.h
@@ -0,0 +1,123 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022, Raspberry Pi Ltd
+ *
+ * region_stats.h - Raspberry Pi region based statistics container
+ */
+#pragma once
+
+#include <array>
+#include <stdint.h>
+#include <vector>
+
+#include <libcamera/geometry.h>
+
+namespace RPiController {
+
+template<typename T>
+class RegionStats
+{
+public:
+	struct Region {
+		T val;
+		uint32_t counted;
+		uint32_t uncounted;
+	};
+
+	RegionStats()
+		: size_({}), numFloating_(0), default_({})
+	{
+	}
+
+	void init(const libcamera::Size &size, unsigned int numFloating = 0)
+	{
+		size_ = size;
+		numFloating_ = numFloating;
+		regions_.clear();
+		regions_.resize(size_.width * size_.height + numFloating_);
+	}
+
+	void init(unsigned int num)
+	{
+		size_ = libcamera::Size(num, 1);
+		numFloating_ = 0;
+		regions_.clear();
+		regions_.resize(num);
+	}
+
+	unsigned int numRegions() const
+	{
+		return size_.width * size_.height;
+	}
+
+	unsigned int numFloatingRegions() const
+	{
+		return numFloating_;
+	}
+
+	libcamera::Size size() const
+	{
+		return size_;
+	}
+
+	void set(unsigned int index, const Region &region)
+	{
+		if (index >= numRegions())
+			return;
+		set_(index, region);
+	}
+
+	void set(const libcamera::Point &pos, const Region &region)
+	{
+		set(pos.y * size_.width + pos.x, region);
+	}
+
+	void setFloating(unsigned int index, const Region &region)
+	{
+		if (index >= numFloatingRegions())
+			return;
+		set(numRegions() + index, region);
+	}
+
+	const Region &get(unsigned int index) const
+	{
+		if (index >= numRegions())
+			return default_;
+		return get_(index);
+	}
+
+	const Region &get(const libcamera::Point &pos) const
+	{
+		return get(pos.y * size_.width + pos.x);
+	}
+
+	const Region &getFloating(unsigned int index) const
+	{
+		if (index >= numFloatingRegions())
+			return default_;
+		return get_(numRegions() + index);
+	}
+
+	typename std::vector<Region>::iterator begin() { return regions_.begin(); }
+	typename std::vector<Region>::iterator end() { return regions_.end(); }
+	typename std::vector<Region>::const_iterator begin() const { return regions_.begin(); }
+	typename std::vector<Region>::const_iterator end() const { return regions_.end(); }
+
+private:
+	void set_(unsigned int index, const Region &region)
+	{
+		regions_[index] = region;
+	}
+
+	const Region &get_(unsigned int index) const
+	{
+		return regions_[index];
+	}
+
+	libcamera::Size size_;
+	unsigned int numFloating_;
+	std::vector<Region> regions_;
+	Region default_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/af.cpp b/src/ipa/rpi/controller/rpi/af.cpp
new file mode 100644
index 00000000..ed0c8a94
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/af.cpp
@@ -0,0 +1,797 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022-2023, Raspberry Pi Ltd
+ *
+ * af.cpp - Autofocus control algorithm
+ */
+
+#include "af.h"
+
+#include <iomanip>
+#include <math.h>
+#include <stdlib.h>
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/control_ids.h>
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiAf)
+
+#define NAME "rpi.af"
+
+/*
+ * Default values for parameters. All may be overridden in the tuning file.
+ * Many of these values are sensor- or module-dependent; the defaults here
+ * assume IMX708 in a Raspberry Pi V3 camera with the standard lens.
+ *
+ * Here all focus values are in dioptres (1/m). They are converted to hardware
+ * units when written to status.lensSetting or returned from setLensPosition().
+ *
+ * Gain and delay values are relative to the update rate, since much (not all)
+ * of the delay is in the sensor and (for CDAF) ISP, not the lens mechanism;
+ * but note that algorithms are updated at no more than 30 Hz.
+ */
+
+Af::RangeDependentParams::RangeDependentParams()
+	: focusMin(0.0),
+	  focusMax(12.0),
+	  focusDefault(1.0)
+{
+}
+
+Af::SpeedDependentParams::SpeedDependentParams()
+	: stepCoarse(1.0),
+	  stepFine(0.25),
+	  contrastRatio(0.75),
+	  pdafGain(-0.02),
+	  pdafSquelch(0.125),
+	  maxSlew(2.0),
+	  pdafFrames(20),
+	  dropoutFrames(6),
+	  stepFrames(4)
+{
+}
+
+Af::CfgParams::CfgParams()
+	: confEpsilon(8),
+	  confThresh(16),
+	  confClip(512),
+	  skipFrames(5),
+	  map()
+{
+}
+
+template<typename T>
+static void readNumber(T &dest, const libcamera::YamlObject &params, char const *name)
+{
+	auto value = params[name].get<T>();
+	if (value)
+		dest = *value;
+	else
+		LOG(RPiAf, Warning) << "Missing parameter \"" << name << "\"";
+}
+
+void Af::RangeDependentParams::read(const libcamera::YamlObject &params)
+{
+
+	readNumber<double>(focusMin, params, "min");
+	readNumber<double>(focusMax, params, "max");
+	readNumber<double>(focusDefault, params, "default");
+}
+
+void Af::SpeedDependentParams::read(const libcamera::YamlObject &params)
+{
+	readNumber<double>(stepCoarse, params, "step_coarse");
+	readNumber<double>(stepFine, params, "step_fine");
+	readNumber<double>(contrastRatio, params, "contrast_ratio");
+	readNumber<double>(pdafGain, params, "pdaf_gain");
+	readNumber<double>(pdafSquelch, params, "pdaf_squelch");
+	readNumber<double>(maxSlew, params, "max_slew");
+	readNumber<uint32_t>(pdafFrames, params, "pdaf_frames");
+	readNumber<uint32_t>(dropoutFrames, params, "dropout_frames");
+	readNumber<uint32_t>(stepFrames, params, "step_frames");
+}
+
+int Af::CfgParams::read(const libcamera::YamlObject &params)
+{
+	if (params.contains("ranges")) {
+		auto &rr = params["ranges"];
+
+		if (rr.contains("normal"))
+			ranges[AfRangeNormal].read(rr["normal"]);
+		else
+			LOG(RPiAf, Warning) << "Missing range \"normal\"";
+
+		ranges[AfRangeMacro] = ranges[AfRangeNormal];
+		if (rr.contains("macro"))
+			ranges[AfRangeMacro].read(rr["macro"]);
+
+		ranges[AfRangeFull].focusMin = std::min(ranges[AfRangeNormal].focusMin,
+							ranges[AfRangeMacro].focusMin);
+		ranges[AfRangeFull].focusMax = std::max(ranges[AfRangeNormal].focusMax,
+							ranges[AfRangeMacro].focusMax);
+		ranges[AfRangeFull].focusDefault = ranges[AfRangeNormal].focusDefault;
+		if (rr.contains("full"))
+			ranges[AfRangeFull].read(rr["full"]);
+	} else
+		LOG(RPiAf, Warning) << "No ranges defined";
+
+	if (params.contains("speeds")) {
+		auto &ss = params["speeds"];
+
+		if (ss.contains("normal"))
+			speeds[AfSpeedNormal].read(ss["normal"]);
+		else
+			LOG(RPiAf, Warning) << "Missing speed \"normal\"";
+
+		speeds[AfSpeedFast] = speeds[AfSpeedNormal];
+		if (ss.contains("fast"))
+			speeds[AfSpeedFast].read(ss["fast"]);
+	} else
+		LOG(RPiAf, Warning) << "No speeds defined";
+
+	readNumber<uint32_t>(confEpsilon, params, "conf_epsilon");
+	readNumber<uint32_t>(confThresh, params, "conf_thresh");
+	readNumber<uint32_t>(confClip, params, "conf_clip");
+	readNumber<uint32_t>(skipFrames, params, "skip_frames");
+
+	if (params.contains("map"))
+		map.read(params["map"]);
+	else
+		LOG(RPiAf, Warning) << "No map defined";
+
+	return 0;
+}
+
+void Af::CfgParams::initialise()
+{
+	if (map.empty()) {
+		/* Default mapping from dioptres to hardware setting */
+		static constexpr double DefaultMapX0 = 0.0;
+		static constexpr double DefaultMapY0 = 445.0;
+		static constexpr double DefaultMapX1 = 15.0;
+		static constexpr double DefaultMapY1 = 925.0;
+
+		map.append(DefaultMapX0, DefaultMapY0);
+		map.append(DefaultMapX1, DefaultMapY1);
+	}
+}
+
+/* Af Algorithm class */
+
+static constexpr unsigned MaxWindows = 10;
+
+Af::Af(Controller *controller)
+	: AfAlgorithm(controller),
+	  cfg_(),
+	  range_(AfRangeNormal),
+	  speed_(AfSpeedNormal),
+	  mode_(AfAlgorithm::AfModeManual),
+	  pauseFlag_(false),
+	  statsRegion_(0, 0, 0, 0),
+	  windows_(),
+	  useWindows_(false),
+	  phaseWeights_(),
+	  contrastWeights_(),
+	  scanState_(ScanState::Idle),
+	  initted_(false),
+	  ftarget_(-1.0),
+	  fsmooth_(-1.0),
+	  prevContrast_(0.0),
+	  skipCount_(0),
+	  stepCount_(0),
+	  dropCount_(0),
+	  scanMaxContrast_(0.0),
+	  scanMinContrast_(1.0e9),
+	  scanData_(),
+	  reportState_(AfState::Idle)
+{
+	/*
+	 * Reserve space for data, to reduce memory fragmentation. It's too early
+	 * to query the size of the PDAF (from camera) and Contrast (from ISP)
+	 * statistics, but these are plausible upper bounds.
+	 */
+	phaseWeights_.w.reserve(16 * 12);
+	contrastWeights_.w.reserve(getHardwareConfig().focusRegions.width *
+				   getHardwareConfig().focusRegions.height);
+	scanData_.reserve(32);
+}
+
+Af::~Af()
+{
+}
+
+char const *Af::name() const
+{
+	return NAME;
+}
+
+int Af::read(const libcamera::YamlObject &params)
+{
+	return cfg_.read(params);
+}
+
+void Af::initialise()
+{
+	cfg_.initialise();
+}
+
+void Af::switchMode(CameraMode const &cameraMode, [[maybe_unused]] Metadata *metadata)
+{
+	(void)metadata;
+
+	/* Assume that PDAF and Focus stats grids cover the visible area */
+	statsRegion_.x = (int)cameraMode.cropX;
+	statsRegion_.y = (int)cameraMode.cropY;
+	statsRegion_.width = (unsigned)(cameraMode.width * cameraMode.scaleX);
+	statsRegion_.height = (unsigned)(cameraMode.height * cameraMode.scaleY);
+	LOG(RPiAf, Debug) << "switchMode: statsRegion: "
+			  << statsRegion_.x << ','
+			  << statsRegion_.y << ','
+			  << statsRegion_.width << ','
+			  << statsRegion_.height;
+	invalidateWeights();
+
+	if (scanState_ >= ScanState::Coarse && scanState_ < ScanState::Settle) {
+		/*
+		 * If a scan was in progress, re-start it, as CDAF statistics
+		 * may have changed. Though if the application is just about
+		 * to take a still picture, this will not help...
+		 */
+		startProgrammedScan();
+	}
+	skipCount_ = cfg_.skipFrames;
+}
+
+void Af::computeWeights(RegionWeights *wgts, unsigned rows, unsigned cols)
+{
+	wgts->rows = rows;
+	wgts->cols = cols;
+	wgts->sum = 0;
+	wgts->w.resize(rows * cols);
+	std::fill(wgts->w.begin(), wgts->w.end(), 0);
+
+	if (rows > 0 && cols > 0 && useWindows_ &&
+	    statsRegion_.height >= rows && statsRegion_.width >= cols) {
+		/*
+		 * Here we just merge all of the given windows, weighted by area.
+		 * \todo Perhaps a better approach might be to find the phase in each
+		 * window and choose either the closest or the highest-confidence one?
+		 * Ensure weights sum to less than (1<<16). 46080 is a "round number"
+		 * below 65536, for better rounding when window size is a simple
+		 * fraction of image dimensions.
+		 */
+		const unsigned maxCellWeight = 46080u / (MaxWindows * rows * cols);
+		const unsigned cellH = statsRegion_.height / rows;
+		const unsigned cellW = statsRegion_.width / cols;
+		const unsigned cellA = cellH * cellW;
+
+		for (auto &w : windows_) {
+			for (unsigned r = 0; r < rows; ++r) {
+				int y0 = std::max(statsRegion_.y + (int)(cellH * r), w.y);
+				int y1 = std::min(statsRegion_.y + (int)(cellH * (r + 1)),
+						  w.y + (int)(w.height));
+				if (y0 >= y1)
+					continue;
+				y1 -= y0;
+				for (unsigned c = 0; c < cols; ++c) {
+					int x0 = std::max(statsRegion_.x + (int)(cellW * c), w.x);
+					int x1 = std::min(statsRegion_.x + (int)(cellW * (c + 1)),
+							  w.x + (int)(w.width));
+					if (x0 >= x1)
+						continue;
+					unsigned a = y1 * (x1 - x0);
+					a = (maxCellWeight * a + cellA - 1) / cellA;
+					wgts->w[r * cols + c] += a;
+					wgts->sum += a;
+				}
+			}
+		}
+	}
+
+	if (wgts->sum == 0) {
+		/* Default AF window is the middle 1/2 width of the middle 1/3 height */
+		for (unsigned r = rows / 3; r < rows - rows / 3; ++r) {
+			for (unsigned c = cols / 4; c < cols - cols / 4; ++c) {
+				wgts->w[r * cols + c] = 1;
+				wgts->sum += 1;
+			}
+		}
+	}
+}
+
+void Af::invalidateWeights()
+{
+	phaseWeights_.sum = 0;
+	contrastWeights_.sum = 0;
+}
+
+bool Af::getPhase(PdafRegions const &regions, double &phase, double &conf)
+{
+	libcamera::Size size = regions.size();
+	if (size.height != phaseWeights_.rows || size.width != phaseWeights_.cols ||
+	    phaseWeights_.sum == 0) {
+		LOG(RPiAf, Debug) << "Recompute Phase weights " << size.width << 'x' << size.height;
+		computeWeights(&phaseWeights_, size.height, size.width);
+	}
+
+	uint32_t sumWc = 0;
+	int64_t sumWcp = 0;
+	for (unsigned i = 0; i < regions.numRegions(); ++i) {
+		unsigned w = phaseWeights_.w[i];
+		if (w) {
+			const PdafData &data = regions.get(i).val;
+			unsigned c = data.conf;
+			if (c >= cfg_.confThresh) {
+				if (c > cfg_.confClip)
+					c = cfg_.confClip;
+				c -= (cfg_.confThresh >> 2);
+				sumWc += w * c;
+				c -= (cfg_.confThresh >> 2);
+				sumWcp += (int64_t)(w * c) * (int64_t)data.phase;
+			}
+		}
+	}
+
+	if (0 < phaseWeights_.sum && phaseWeights_.sum <= sumWc) {
+		phase = (double)sumWcp / (double)sumWc;
+		conf = (double)sumWc / (double)phaseWeights_.sum;
+		return true;
+	} else {
+		phase = 0.0;
+		conf = 0.0;
+		return false;
+	}
+}
+
+double Af::getContrast(const FocusRegions &focusStats)
+{
+	libcamera::Size size = focusStats.size();
+	if (size.height != contrastWeights_.rows ||
+	    size.width != contrastWeights_.cols || contrastWeights_.sum == 0) {
+		LOG(RPiAf, Debug) << "Recompute Contrast weights "
+				  << size.width << 'x' << size.height;
+		computeWeights(&contrastWeights_, size.height, size.width);
+	}
+
+	uint64_t sumWc = 0;
+	for (unsigned i = 0; i < focusStats.numRegions(); ++i)
+		sumWc += contrastWeights_.w[i] * focusStats.get(i).val;
+
+	return (contrastWeights_.sum > 0) ? ((double)sumWc / (double)contrastWeights_.sum) : 0.0;
+}
+
+void Af::doPDAF(double phase, double conf)
+{
+	/* Apply loop gain */
+	phase *= cfg_.speeds[speed_].pdafGain;
+
+	if (mode_ == AfModeContinuous) {
+		/*
+		 * PDAF in Continuous mode. Scale down lens movement when
+		 * delta is small or confidence is low, to suppress wobble.
+		 */
+		phase *= conf / (conf + cfg_.confEpsilon);
+		if (std::abs(phase) < cfg_.speeds[speed_].pdafSquelch) {
+			double a = phase / cfg_.speeds[speed_].pdafSquelch;
+			phase *= a * a;
+		}
+	} else {
+		/*
+		 * PDAF in triggered-auto mode. Allow early termination when
+		 * phase delta is small; scale down lens movements towards
+		 * the end of the sequence, to ensure a stable image.
+		 */
+		if (stepCount_ >= cfg_.speeds[speed_].stepFrames) {
+			if (std::abs(phase) < cfg_.speeds[speed_].pdafSquelch)
+				stepCount_ = cfg_.speeds[speed_].stepFrames;
+		} else
+			phase *= stepCount_ / cfg_.speeds[speed_].stepFrames;
+	}
+
+	/* Apply slew rate limit. Report failure if out of bounds. */
+	if (phase < -cfg_.speeds[speed_].maxSlew) {
+		phase = -cfg_.speeds[speed_].maxSlew;
+		reportState_ = (ftarget_ <= cfg_.ranges[range_].focusMin) ? AfState::Failed
+									  : AfState::Scanning;
+	} else if (phase > cfg_.speeds[speed_].maxSlew) {
+		phase = cfg_.speeds[speed_].maxSlew;
+		reportState_ = (ftarget_ >= cfg_.ranges[range_].focusMax) ? AfState::Failed
+									  : AfState::Scanning;
+	} else
+		reportState_ = AfState::Focused;
+
+	ftarget_ = fsmooth_ + phase;
+}
+
+bool Af::earlyTerminationByPhase(double phase)
+{
+	if (scanData_.size() > 0 &&
+	    scanData_[scanData_.size() - 1].conf >= cfg_.confEpsilon) {
+		double oldFocus = scanData_[scanData_.size() - 1].focus;
+		double oldPhase = scanData_[scanData_.size() - 1].phase;
+
+		/*
+		 * Check that the gradient is finite and has the expected sign;
+		 * Interpolate/extrapolate the lens position for zero phase.
+		 * Check that the extrapolation is well-conditioned.
+		 */
+		if ((ftarget_ - oldFocus) * (phase - oldPhase) > 0.0) {
+			double param = phase / (phase - oldPhase);
+			if (-3.0 <= param && param <= 3.5) {
+				ftarget_ += param * (oldFocus - ftarget_);
+				LOG(RPiAf, Debug) << "ETBP: param=" << param;
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+double Af::findPeak(unsigned i) const
+{
+	double f = scanData_[i].focus;
+
+	if (i > 0 && i + 1 < scanData_.size()) {
+		double dropLo = scanData_[i].contrast - scanData_[i - 1].contrast;
+		double dropHi = scanData_[i].contrast - scanData_[i + 1].contrast;
+		if (0.0 <= dropLo && dropLo < dropHi) {
+			double param = 0.3125 * (1.0 - dropLo / dropHi) * (1.6 - dropLo / dropHi);
+			f += param * (scanData_[i - 1].focus - f);
+		} else if (0.0 <= dropHi && dropHi < dropLo) {
+			double param = 0.3125 * (1.0 - dropHi / dropLo) * (1.6 - dropHi / dropLo);
+			f += param * (scanData_[i + 1].focus - f);
+		}
+	}
+
+	LOG(RPiAf, Debug) << "FindPeak: " << f;
+	return f;
+}
+
+void Af::doScan(double contrast, double phase, double conf)
+{
+	/* Record lens position, contrast and phase values for the current scan */
+	if (scanData_.empty() || contrast > scanMaxContrast_) {
+		scanMaxContrast_ = contrast;
+		scanMaxIndex_ = scanData_.size();
+	}
+	if (contrast < scanMinContrast_)
+		scanMinContrast_ = contrast;
+	scanData_.emplace_back(ScanRecord{ ftarget_, contrast, phase, conf });
+
+	if (scanState_ == ScanState::Coarse) {
+		if (ftarget_ >= cfg_.ranges[range_].focusMax ||
+		    contrast < cfg_.speeds[speed_].contrastRatio * scanMaxContrast_) {
+			/*
+			 * Finished course scan, or termination based on contrast.
+			 * Jump to just after max contrast and start fine scan.
+			 */
+			ftarget_ = std::min(ftarget_, findPeak(scanMaxIndex_) +
+					2.0 * cfg_.speeds[speed_].stepFine);
+			scanState_ = ScanState::Fine;
+			scanData_.clear();
+		} else
+			ftarget_ += cfg_.speeds[speed_].stepCoarse;
+	} else { /* ScanState::Fine */
+		if (ftarget_ <= cfg_.ranges[range_].focusMin || scanData_.size() >= 5 ||
+		    contrast < cfg_.speeds[speed_].contrastRatio * scanMaxContrast_) {
+			/*
+			 * Finished fine scan, or termination based on contrast.
+			 * Use quadratic peak-finding to find best contrast position.
+			 */
+			ftarget_ = findPeak(scanMaxIndex_);
+			scanState_ = ScanState::Settle;
+		} else
+			ftarget_ -= cfg_.speeds[speed_].stepFine;
+	}
+
+	stepCount_ = (ftarget_ == fsmooth_) ? 0 : cfg_.speeds[speed_].stepFrames;
+}
+
+void Af::doAF(double contrast, double phase, double conf)
+{
+	/* Skip frames at startup and after sensor mode change */
+	if (skipCount_ > 0) {
+		LOG(RPiAf, Debug) << "SKIP";
+		skipCount_--;
+		return;
+	}
+
+	if (scanState_ == ScanState::Pdaf) {
+		/*
+		 * Use PDAF closed-loop control whenever available, in both CAF
+		 * mode and (for a limited number of iterations) when triggered.
+		 * If PDAF fails (due to poor contrast, noise or large defocus),
+		 * fall back to a CDAF-based scan. To avoid "nuisance" scans,
+		 * scan only after a number of frames with low PDAF confidence.
+		 */
+		if (conf > (dropCount_ ? 1.0 : 0.25) * cfg_.confEpsilon) {
+			doPDAF(phase, conf);
+			if (stepCount_ > 0)
+				stepCount_--;
+			else if (mode_ != AfModeContinuous)
+				scanState_ = ScanState::Idle;
+			dropCount_ = 0;
+		} else if (++dropCount_ == cfg_.speeds[speed_].dropoutFrames)
+			startProgrammedScan();
+	} else if (scanState_ >= ScanState::Coarse && fsmooth_ == ftarget_) {
+		/*
+		 * Scanning sequence. This means PDAF has become unavailable.
+		 * Allow a delay between steps for CDAF FoM statistics to be
+		 * updated, and a "settling time" at the end of the sequence.
+		 * [A coarse or fine scan can be abandoned if two PDAF samples
+		 * allow direct interpolation of the zero-phase lens position.]
+		 */
+		if (stepCount_ > 0)
+			stepCount_--;
+		else if (scanState_ == ScanState::Settle) {
+			if (prevContrast_ >= cfg_.speeds[speed_].contrastRatio * scanMaxContrast_ &&
+			    scanMinContrast_ <= cfg_.speeds[speed_].contrastRatio * scanMaxContrast_)
+				reportState_ = AfState::Focused;
+			else
+				reportState_ = AfState::Failed;
+			if (mode_ == AfModeContinuous && !pauseFlag_ &&
+			    cfg_.speeds[speed_].dropoutFrames > 0)
+				scanState_ = ScanState::Pdaf;
+			else
+				scanState_ = ScanState::Idle;
+			scanData_.clear();
+		} else if (conf >= cfg_.confEpsilon && earlyTerminationByPhase(phase)) {
+			scanState_ = ScanState::Settle;
+			stepCount_ = (mode_ == AfModeContinuous) ? 0
+								 : cfg_.speeds[speed_].stepFrames;
+		} else
+			doScan(contrast, phase, conf);
+	}
+}
+
+void Af::updateLensPosition()
+{
+	if (scanState_ >= ScanState::Pdaf) {
+		ftarget_ = std::clamp(ftarget_,
+				      cfg_.ranges[range_].focusMin,
+				      cfg_.ranges[range_].focusMax);
+	}
+
+	if (initted_) {
+		/* from a known lens position: apply slew rate limit */
+		fsmooth_ = std::clamp(ftarget_,
+				      fsmooth_ - cfg_.speeds[speed_].maxSlew,
+				      fsmooth_ + cfg_.speeds[speed_].maxSlew);
+	} else {
+		/* from an unknown position: go straight to target, but add delay */
+		fsmooth_ = ftarget_;
+		initted_ = true;
+		skipCount_ = cfg_.skipFrames;
+	}
+}
+
+void Af::startAF()
+{
+	/* Use PDAF if the tuning file allows it; else CDAF. */
+	if (cfg_.speeds[speed_].dropoutFrames > 0 &&
+	    (mode_ == AfModeContinuous || cfg_.speeds[speed_].pdafFrames > 0)) {
+		if (!initted_) {
+			ftarget_ = cfg_.ranges[range_].focusDefault;
+			updateLensPosition();
+		}
+		stepCount_ = (mode_ == AfModeContinuous) ? 0 : cfg_.speeds[speed_].pdafFrames;
+		scanState_ = ScanState::Pdaf;
+		scanData_.clear();
+		dropCount_ = 0;
+		reportState_ = AfState::Scanning;
+	} else
+		startProgrammedScan();
+}
+
+void Af::startProgrammedScan()
+{
+	ftarget_ = cfg_.ranges[range_].focusMin;
+	updateLensPosition();
+	scanState_ = ScanState::Coarse;
+	scanMaxContrast_ = 0.0;
+	scanMinContrast_ = 1.0e9;
+	scanMaxIndex_ = 0;
+	scanData_.clear();
+	stepCount_ = cfg_.speeds[speed_].stepFrames;
+	reportState_ = AfState::Scanning;
+}
+
+void Af::goIdle()
+{
+	scanState_ = ScanState::Idle;
+	reportState_ = AfState::Idle;
+	scanData_.clear();
+}
+
+/*
+ * PDAF phase data are available in prepare(), but CDAF statistics are not
+ * available until process(). We are gambling on the availability of PDAF.
+ * To expedite feedback control using PDAF, issue the V4L2 lens control from
+ * prepare(). Conversely, during scans, we must allow an extra frame delay
+ * between steps, to retrieve CDAF statistics from the previous process()
+ * so we can terminate the scan early without having to change our minds.
+ */
+
+void Af::prepare(Metadata *imageMetadata)
+{
+	/* Initialize for triggered scan or start of CAF mode */
+	if (scanState_ == ScanState::Trigger)
+		startAF();
+
+	if (initted_) {
+		/* Get PDAF from the embedded metadata, and run AF algorithm core */
+		PdafRegions regions;
+		double phase = 0.0, conf = 0.0;
+		double oldFt = ftarget_;
+		double oldFs = fsmooth_;
+		ScanState oldSs = scanState_;
+		uint32_t oldSt = stepCount_;
+		if (imageMetadata->get("pdaf.regions", regions) == 0)
+			getPhase(regions, phase, conf);
+		doAF(prevContrast_, phase, conf);
+		updateLensPosition();
+		LOG(RPiAf, Debug) << std::fixed << std::setprecision(2)
+				  << static_cast<unsigned int>(reportState_)
+				  << " sst" << static_cast<unsigned int>(oldSs)
+				  << "->" << static_cast<unsigned int>(scanState_)
+				  << " stp" << oldSt << "->" << stepCount_
+				  << " ft" << oldFt << "->" << ftarget_
+				  << " fs" << oldFs << "->" << fsmooth_
+				  << " cont=" << (int)prevContrast_
+				  << " phase=" << (int)phase << " conf=" << (int)conf;
+	}
+
+	/* Report status and produce new lens setting */
+	AfStatus status;
+	if (pauseFlag_)
+		status.pauseState = (scanState_ == ScanState::Idle) ? AfPauseState::Paused
+								    : AfPauseState::Pausing;
+	else
+		status.pauseState = AfPauseState::Running;
+
+	if (mode_ == AfModeAuto && scanState_ != ScanState::Idle)
+		status.state = AfState::Scanning;
+	else
+		status.state = reportState_;
+	status.lensSetting = initted_ ? std::optional<int>(cfg_.map.eval(fsmooth_))
+				      : std::nullopt;
+	imageMetadata->set("af.status", status);
+}
+
+void Af::process(StatisticsPtr &stats, [[maybe_unused]] Metadata *imageMetadata)
+{
+	(void)imageMetadata;
+	prevContrast_ = getContrast(stats->focusRegions);
+}
+
+/* Controls */
+
+void Af::setRange(AfRange r)
+{
+	LOG(RPiAf, Debug) << "setRange: " << (unsigned)r;
+	if (r < AfAlgorithm::AfRangeMax)
+		range_ = r;
+}
+
+void Af::setSpeed(AfSpeed s)
+{
+	LOG(RPiAf, Debug) << "setSpeed: " << (unsigned)s;
+	if (s < AfAlgorithm::AfSpeedMax) {
+		if (scanState_ == ScanState::Pdaf &&
+		    cfg_.speeds[s].pdafFrames > cfg_.speeds[speed_].pdafFrames)
+			stepCount_ += cfg_.speeds[s].pdafFrames - cfg_.speeds[speed_].pdafFrames;
+		speed_ = s;
+	}
+}
+
+void Af::setMetering(bool mode)
+{
+	if (useWindows_ != mode) {
+		useWindows_ = mode;
+		invalidateWeights();
+	}
+}
+
+void Af::setWindows(libcamera::Span<libcamera::Rectangle const> const &wins)
+{
+	windows_.clear();
+	for (auto &w : wins) {
+		LOG(RPiAf, Debug) << "Window: "
+				  << w.x << ", "
+				  << w.y << ", "
+				  << w.width << ", "
+				  << w.height;
+		windows_.push_back(w);
+		if (windows_.size() >= MaxWindows)
+			break;
+	}
+
+	if (useWindows_)
+		invalidateWeights();
+}
+
+bool Af::setLensPosition(double dioptres, int *hwpos)
+{
+	bool changed = false;
+
+	if (mode_ == AfModeManual) {
+		LOG(RPiAf, Debug) << "setLensPosition: " << dioptres;
+		ftarget_ = cfg_.map.domain().clip(dioptres);
+		changed = !(initted_ && fsmooth_ == ftarget_);
+		updateLensPosition();
+	}
+
+	if (hwpos)
+		*hwpos = cfg_.map.eval(fsmooth_);
+
+	return changed;
+}
+
+std::optional<double> Af::getLensPosition() const
+{
+	/*
+	 * \todo We ought to perform some precise timing here to determine
+	 * the current lens position.
+	 */
+	return initted_ ? std::optional<double>(fsmooth_) : std::nullopt;
+}
+
+void Af::cancelScan()
+{
+	LOG(RPiAf, Debug) << "cancelScan";
+	if (mode_ == AfModeAuto)
+		goIdle();
+}
+
+void Af::triggerScan()
+{
+	LOG(RPiAf, Debug) << "triggerScan";
+	if (mode_ == AfModeAuto && scanState_ == ScanState::Idle)
+		scanState_ = ScanState::Trigger;
+}
+
+void Af::setMode(AfAlgorithm::AfMode mode)
+{
+	LOG(RPiAf, Debug) << "setMode: " << (unsigned)mode;
+	if (mode_ != mode) {
+		mode_ = mode;
+		pauseFlag_ = false;
+		if (mode == AfModeContinuous)
+			scanState_ = ScanState::Trigger;
+		else if (mode != AfModeAuto || scanState_ < ScanState::Coarse)
+			goIdle();
+	}
+}
+
+AfAlgorithm::AfMode Af::getMode() const
+{
+	return mode_;
+}
+
+void Af::pause(AfAlgorithm::AfPause pause)
+{
+	LOG(RPiAf, Debug) << "pause: " << (unsigned)pause;
+	if (mode_ == AfModeContinuous) {
+		if (pause == AfPauseResume && pauseFlag_) {
+			pauseFlag_ = false;
+			if (scanState_ < ScanState::Coarse)
+				scanState_ = ScanState::Trigger;
+		} else if (pause != AfPauseResume && !pauseFlag_) {
+			pauseFlag_ = true;
+			if (pause == AfPauseImmediate || scanState_ < ScanState::Coarse)
+				goIdle();
+		}
+	}
+}
+
+// Register algorithm with the system.
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Af(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/af.h b/src/ipa/rpi/controller/rpi/af.h
new file mode 100644
index 00000000..6d2bae67
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/af.h
@@ -0,0 +1,165 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022-2023, Raspberry Pi Ltd
+ *
+ * af.h - Autofocus control algorithm
+ */
+#pragma once
+
+#include "../af_algorithm.h"
+#include "../af_status.h"
+#include "../pdaf_data.h"
+#include "../pwl.h"
+
+/*
+ * This algorithm implements a hybrid of CDAF and PDAF, favouring PDAF.
+ *
+ * Whenever PDAF is available, it is used in a continuous feedback loop.
+ * When triggered in auto mode, we simply enable AF for a limited number
+ * of frames (it may terminate early if the delta becomes small enough).
+ *
+ * When PDAF confidence is low (due e.g. to low contrast or extreme defocus)
+ * or PDAF data are absent, fall back to CDAF with a programmed scan pattern.
+ * A coarse and fine scan are performed, using ISP's CDAF focus FoM to
+ * estimate the lens position with peak contrast. This is slower due to
+ * extra latency in the ISP, and requires a settling time between steps.
+ *
+ * Some hysteresis is applied to the switch between PDAF and CDAF, to avoid
+ * "nuisance" scans. During each interval where PDAF is not working, only
+ * ONE scan will be performed; CAF cannot track objects using CDAF alone.
+ *
+ */
+
+namespace RPiController {
+
+class Af : public AfAlgorithm
+{
+public:
+	Af(Controller *controller = NULL);
+	~Af();
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void initialise() override;
+
+	/* IPA calls */
+	void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;
+	void prepare(Metadata *imageMetadata) override;
+	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
+
+	/* controls */
+	void setRange(AfRange range) override;
+	void setSpeed(AfSpeed speed) override;
+	void setMetering(bool use_windows) override;
+	void setWindows(libcamera::Span<libcamera::Rectangle const> const &wins) override;
+	void setMode(AfMode mode) override;
+	AfMode getMode() const override;
+	bool setLensPosition(double dioptres, int32_t *hwpos) override;
+	std::optional<double> getLensPosition() const override;
+	void triggerScan() override;
+	void cancelScan() override;
+	void pause(AfPause pause) override;
+
+private:
+	enum class ScanState {
+		Idle = 0,
+		Trigger,
+		Pdaf,
+		Coarse,
+		Fine,
+		Settle
+	};
+
+	struct RangeDependentParams {
+		double focusMin;       		/* lower (far) limit in dipotres */
+		double focusMax;	       	/* upper (near) limit in dioptres */
+		double focusDefault;		/* default setting ("hyperfocal") */
+
+		RangeDependentParams();
+		void read(const libcamera::YamlObject &params);
+	};
+
+	struct SpeedDependentParams {
+		double stepCoarse;		/* used for scans */
+		double stepFine;		/* used for scans */
+		double contrastRatio;		/* used for scan termination and reporting */
+		double pdafGain;		/* coefficient for PDAF feedback loop */
+		double pdafSquelch;		/* PDAF stability parameter (device-specific) */
+		double maxSlew;			/* limit for lens movement per frame */
+		uint32_t pdafFrames;		/* number of iterations when triggered */
+		uint32_t dropoutFrames;		/* number of non-PDAF frames to switch to CDAF */
+		uint32_t stepFrames;		/* frames to skip in between steps of a scan */
+
+		SpeedDependentParams();
+		void read(const libcamera::YamlObject &params);
+	};
+
+	struct CfgParams {
+		RangeDependentParams ranges[AfRangeMax];
+		SpeedDependentParams speeds[AfSpeedMax];
+		uint32_t confEpsilon;	       	/* PDAF hysteresis threshold (sensor-specific) */
+		uint32_t confThresh;	       	/* PDAF confidence cell min (sensor-specific) */
+		uint32_t confClip;	       	/* PDAF confidence cell max (sensor-specific) */
+		uint32_t skipFrames;	       	/* frames to skip at start or modeswitch */
+		Pwl map;		       	/* converts dioptres -> lens driver position */
+
+		CfgParams();
+		int read(const libcamera::YamlObject &params);
+		void initialise();
+	};
+
+	struct ScanRecord {
+		double focus;
+		double contrast;
+		double phase;
+		double conf;
+	};
+
+	struct RegionWeights {
+		unsigned rows;
+		unsigned cols;
+		uint32_t sum;
+		std::vector<uint16_t> w;
+
+		RegionWeights()
+			: rows(0), cols(0), sum(0), w() {}
+	};
+
+	void computeWeights(RegionWeights *wgts, unsigned rows, unsigned cols);
+	void invalidateWeights();
+	bool getPhase(PdafRegions const &regions, double &phase, double &conf);
+	double getContrast(const FocusRegions &focusStats);
+	void doPDAF(double phase, double conf);
+	bool earlyTerminationByPhase(double phase);
+	double findPeak(unsigned index) const;
+	void doScan(double contrast, double phase, double conf);
+	void doAF(double contrast, double phase, double conf);
+	void updateLensPosition();
+	void startAF();
+	void startProgrammedScan();
+	void goIdle();
+
+	/* Configuration and settings */
+	CfgParams cfg_;
+	AfRange range_;
+	AfSpeed speed_;
+	AfMode mode_;
+	bool pauseFlag_;
+	libcamera::Rectangle statsRegion_;
+	std::vector<libcamera::Rectangle> windows_;
+	bool useWindows_;
+	RegionWeights phaseWeights_;
+	RegionWeights contrastWeights_;
+
+	/* Working state. */
+	ScanState scanState_;
+	bool initted_;
+	double ftarget_, fsmooth_;
+	double prevContrast_;
+	unsigned skipCount_, stepCount_, dropCount_;
+	unsigned scanMaxIndex_;
+	double scanMaxContrast_, scanMinContrast_;
+	std::vector<ScanRecord> scanData_;
+	AfState reportState_;
+};
+
+} // namespace RPiController
diff --git a/src/ipa/rpi/controller/rpi/agc.cpp b/src/ipa/rpi/controller/rpi/agc.cpp
new file mode 100644
index 00000000..e6fb7b8d
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/agc.cpp
@@ -0,0 +1,922 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * agc.cpp - AGC/AEC control algorithm
+ */
+
+#include <algorithm>
+#include <map>
+#include <tuple>
+
+#include <libcamera/base/log.h>
+
+#include "../awb_status.h"
+#include "../device_status.h"
+#include "../histogram.h"
+#include "../lux_status.h"
+#include "../metadata.h"
+
+#include "agc.h"
+
+using namespace RPiController;
+using namespace libcamera;
+using libcamera::utils::Duration;
+using namespace std::literals::chrono_literals;
+
+LOG_DEFINE_CATEGORY(RPiAgc)
+
+#define NAME "rpi.agc"
+
+int AgcMeteringMode::read(const libcamera::YamlObject &params)
+{
+	const YamlObject &yamlWeights = params["weights"];
+
+	for (const auto &p : yamlWeights.asList()) {
+		auto value = p.get<double>();
+		if (!value)
+			return -EINVAL;
+		weights.push_back(*value);
+	}
+
+	return 0;
+}
+
+static std::tuple<int, std::string>
+readMeteringModes(std::map<std::string, AgcMeteringMode> &metering_modes,
+		  const libcamera::YamlObject &params)
+{
+	std::string first;
+	int ret;
+
+	for (const auto &[key, value] : params.asDict()) {
+		AgcMeteringMode meteringMode;
+		ret = meteringMode.read(value);
+		if (ret)
+			return { ret, {} };
+
+		metering_modes[key] = std::move(meteringMode);
+		if (first.empty())
+			first = key;
+	}
+
+	return { 0, first };
+}
+
+int AgcExposureMode::read(const libcamera::YamlObject &params)
+{
+	auto value = params["shutter"].getList<double>();
+	if (!value)
+		return -EINVAL;
+	std::transform(value->begin(), value->end(), std::back_inserter(shutter),
+		       [](double v) { return v * 1us; });
+
+	value = params["gain"].getList<double>();
+	if (!value)
+		return -EINVAL;
+	gain = std::move(*value);
+
+	if (shutter.size() < 2 || gain.size() < 2) {
+		LOG(RPiAgc, Error)
+			<< "AgcExposureMode: must have at least two entries in exposure profile";
+		return -EINVAL;
+	}
+
+	if (shutter.size() != gain.size()) {
+		LOG(RPiAgc, Error)
+			<< "AgcExposureMode: expect same number of exposure and gain entries in exposure profile";
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+static std::tuple<int, std::string>
+readExposureModes(std::map<std::string, AgcExposureMode> &exposureModes,
+		  const libcamera::YamlObject &params)
+{
+	std::string first;
+	int ret;
+
+	for (const auto &[key, value] : params.asDict()) {
+		AgcExposureMode exposureMode;
+		ret = exposureMode.read(value);
+		if (ret)
+			return { ret, {} };
+
+		exposureModes[key] = std::move(exposureMode);
+		if (first.empty())
+			first = key;
+	}
+
+	return { 0, first };
+}
+
+int AgcConstraint::read(const libcamera::YamlObject &params)
+{
+	std::string boundString = params["bound"].get<std::string>("");
+	transform(boundString.begin(), boundString.end(),
+		  boundString.begin(), ::toupper);
+	if (boundString != "UPPER" && boundString != "LOWER") {
+		LOG(RPiAgc, Error) << "AGC constraint type should be UPPER or LOWER";
+		return -EINVAL;
+	}
+	bound = boundString == "UPPER" ? Bound::UPPER : Bound::LOWER;
+
+	auto value = params["q_lo"].get<double>();
+	if (!value)
+		return -EINVAL;
+	qLo = *value;
+
+	value = params["q_hi"].get<double>();
+	if (!value)
+		return -EINVAL;
+	qHi = *value;
+
+	return yTarget.read(params["y_target"]);
+}
+
+static std::tuple<int, AgcConstraintMode>
+readConstraintMode(const libcamera::YamlObject &params)
+{
+	AgcConstraintMode mode;
+	int ret;
+
+	for (const auto &p : params.asList()) {
+		AgcConstraint constraint;
+		ret = constraint.read(p);
+		if (ret)
+			return { ret, {} };
+
+		mode.push_back(std::move(constraint));
+	}
+
+	return { 0, mode };
+}
+
+static std::tuple<int, std::string>
+readConstraintModes(std::map<std::string, AgcConstraintMode> &constraintModes,
+		    const libcamera::YamlObject &params)
+{
+	std::string first;
+	int ret;
+
+	for (const auto &[key, value] : params.asDict()) {
+		std::tie(ret, constraintModes[key]) = readConstraintMode(value);
+		if (ret)
+			return { ret, {} };
+
+		if (first.empty())
+			first = key;
+	}
+
+	return { 0, first };
+}
+
+int AgcConfig::read(const libcamera::YamlObject &params)
+{
+	LOG(RPiAgc, Debug) << "AgcConfig";
+	int ret;
+
+	std::tie(ret, defaultMeteringMode) =
+		readMeteringModes(meteringModes, params["metering_modes"]);
+	if (ret)
+		return ret;
+	std::tie(ret, defaultExposureMode) =
+		readExposureModes(exposureModes, params["exposure_modes"]);
+	if (ret)
+		return ret;
+	std::tie(ret, defaultConstraintMode) =
+		readConstraintModes(constraintModes, params["constraint_modes"]);
+	if (ret)
+		return ret;
+
+	ret = yTarget.read(params["y_target"]);
+	if (ret)
+		return ret;
+
+	speed = params["speed"].get<double>(0.2);
+	startupFrames = params["startup_frames"].get<uint16_t>(10);
+	convergenceFrames = params["convergence_frames"].get<unsigned int>(6);
+	fastReduceThreshold = params["fast_reduce_threshold"].get<double>(0.4);
+	baseEv = params["base_ev"].get<double>(1.0);
+
+	/* Start with quite a low value as ramping up is easier than ramping down. */
+	defaultExposureTime = params["default_exposure_time"].get<double>(1000) * 1us;
+	defaultAnalogueGain = params["default_analogue_gain"].get<double>(1.0);
+
+	return 0;
+}
+
+Agc::ExposureValues::ExposureValues()
+	: shutter(0s), analogueGain(0),
+	  totalExposure(0s), totalExposureNoDG(0s)
+{
+}
+
+Agc::Agc(Controller *controller)
+	: AgcAlgorithm(controller), meteringMode_(nullptr),
+	  exposureMode_(nullptr), constraintMode_(nullptr),
+	  frameCount_(0), lockCount_(0),
+	  lastTargetExposure_(0s), ev_(1.0), flickerPeriod_(0s),
+	  maxShutter_(0s), fixedShutter_(0s), fixedAnalogueGain_(0.0)
+{
+	memset(&awb_, 0, sizeof(awb_));
+	/*
+	 * Setting status_.totalExposureValue_ to zero initially tells us
+	 * it's not been calculated yet (i.e. Process hasn't yet run).
+	 */
+	memset(&status_, 0, sizeof(status_));
+	status_.ev = ev_;
+}
+
+char const *Agc::name() const
+{
+	return NAME;
+}
+
+int Agc::read(const libcamera::YamlObject &params)
+{
+	LOG(RPiAgc, Debug) << "Agc";
+
+	int ret = config_.read(params);
+	if (ret)
+		return ret;
+
+	const Size &size = getHardwareConfig().agcZoneWeights;
+	for (auto const &modes : config_.meteringModes) {
+		if (modes.second.weights.size() != size.width * size.height) {
+			LOG(RPiAgc, Error) << "AgcMeteringMode: Incorrect number of weights";
+			return -EINVAL;
+		}
+	}
+
+	/*
+	 * Set the config's defaults (which are the first ones it read) as our
+	 * current modes, until someone changes them.  (they're all known to
+	 * exist at this point)
+	 */
+	meteringModeName_ = config_.defaultMeteringMode;
+	meteringMode_ = &config_.meteringModes[meteringModeName_];
+	exposureModeName_ = config_.defaultExposureMode;
+	exposureMode_ = &config_.exposureModes[exposureModeName_];
+	constraintModeName_ = config_.defaultConstraintMode;
+	constraintMode_ = &config_.constraintModes[constraintModeName_];
+	/* Set up the "last shutter/gain" values, in case AGC starts "disabled". */
+	status_.shutterTime = config_.defaultExposureTime;
+	status_.analogueGain = config_.defaultAnalogueGain;
+	return 0;
+}
+
+void Agc::disableAuto()
+{
+	fixedShutter_ = status_.shutterTime;
+	fixedAnalogueGain_ = status_.analogueGain;
+}
+
+void Agc::enableAuto()
+{
+	fixedShutter_ = 0s;
+	fixedAnalogueGain_ = 0;
+}
+
+unsigned int Agc::getConvergenceFrames() const
+{
+	/*
+	 * If shutter and gain have been explicitly set, there is no
+	 * convergence to happen, so no need to drop any frames - return zero.
+	 */
+	if (fixedShutter_ && fixedAnalogueGain_)
+		return 0;
+	else
+		return config_.convergenceFrames;
+}
+
+void Agc::setEv(double ev)
+{
+	ev_ = ev;
+}
+
+void Agc::setFlickerPeriod(Duration flickerPeriod)
+{
+	flickerPeriod_ = flickerPeriod;
+}
+
+void Agc::setMaxShutter(Duration maxShutter)
+{
+	maxShutter_ = maxShutter;
+}
+
+void Agc::setFixedShutter(Duration fixedShutter)
+{
+	fixedShutter_ = fixedShutter;
+	/* Set this in case someone calls disableAuto() straight after. */
+	status_.shutterTime = limitShutter(fixedShutter_);
+}
+
+void Agc::setFixedAnalogueGain(double fixedAnalogueGain)
+{
+	fixedAnalogueGain_ = fixedAnalogueGain;
+	/* Set this in case someone calls disableAuto() straight after. */
+	status_.analogueGain = limitGain(fixedAnalogueGain);
+}
+
+void Agc::setMeteringMode(std::string const &meteringModeName)
+{
+	meteringModeName_ = meteringModeName;
+}
+
+void Agc::setExposureMode(std::string const &exposureModeName)
+{
+	exposureModeName_ = exposureModeName;
+}
+
+void Agc::setConstraintMode(std::string const &constraintModeName)
+{
+	constraintModeName_ = constraintModeName;
+}
+
+void Agc::switchMode(CameraMode const &cameraMode,
+		     Metadata *metadata)
+{
+	/* AGC expects the mode sensitivity always to be non-zero. */
+	ASSERT(cameraMode.sensitivity);
+
+	housekeepConfig();
+
+	/*
+	 * Store the mode in the local state. We must cache the sensitivity of
+	 * of the previous mode for the calculations below.
+	 */
+	double lastSensitivity = mode_.sensitivity;
+	mode_ = cameraMode;
+
+	Duration fixedShutter = limitShutter(fixedShutter_);
+	if (fixedShutter && fixedAnalogueGain_) {
+		/* We're going to reset the algorithm here with these fixed values. */
+
+		fetchAwbStatus(metadata);
+		double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });
+		ASSERT(minColourGain != 0.0);
+
+		/* This is the equivalent of computeTargetExposure and applyDigitalGain. */
+		target_.totalExposureNoDG = fixedShutter_ * fixedAnalogueGain_;
+		target_.totalExposure = target_.totalExposureNoDG / minColourGain;
+
+		/* Equivalent of filterExposure. This resets any "history". */
+		filtered_ = target_;
+
+		/* Equivalent of divideUpExposure. */
+		filtered_.shutter = fixedShutter;
+		filtered_.analogueGain = fixedAnalogueGain_;
+	} else if (status_.totalExposureValue) {
+		/*
+		 * On a mode switch, various things could happen:
+		 * - the exposure profile might change
+		 * - a fixed exposure or gain might be set
+		 * - the new mode's sensitivity might be different
+		 * We cope with the last of these by scaling the target values. After
+		 * that we just need to re-divide the exposure/gain according to the
+		 * current exposure profile, which takes care of everything else.
+		 */
+
+		double ratio = lastSensitivity / cameraMode.sensitivity;
+		target_.totalExposureNoDG *= ratio;
+		target_.totalExposure *= ratio;
+		filtered_.totalExposureNoDG *= ratio;
+		filtered_.totalExposure *= ratio;
+
+		divideUpExposure();
+	} else {
+		/*
+		 * We come through here on startup, when at least one of the shutter
+		 * or gain has not been fixed. We must still write those values out so
+		 * that they will be applied immediately. We supply some arbitrary defaults
+		 * for any that weren't set.
+		 */
+
+		/* Equivalent of divideUpExposure. */
+		filtered_.shutter = fixedShutter ? fixedShutter : config_.defaultExposureTime;
+		filtered_.analogueGain = fixedAnalogueGain_ ? fixedAnalogueGain_ : config_.defaultAnalogueGain;
+	}
+
+	writeAndFinish(metadata, false);
+}
+
+void Agc::prepare(Metadata *imageMetadata)
+{
+	Duration totalExposureValue = status_.totalExposureValue;
+	AgcStatus delayedStatus;
+
+	if (!imageMetadata->get("agc.delayed_status", delayedStatus))
+		totalExposureValue = delayedStatus.totalExposureValue;
+
+	status_.digitalGain = 1.0;
+	fetchAwbStatus(imageMetadata); /* always fetch it so that Process knows it's been done */
+
+	if (status_.totalExposureValue) {
+		/* Process has run, so we have meaningful values. */
+		DeviceStatus deviceStatus;
+		if (imageMetadata->get("device.status", deviceStatus) == 0) {
+			Duration actualExposure = deviceStatus.shutterSpeed *
+						  deviceStatus.analogueGain;
+			if (actualExposure) {
+				status_.digitalGain = totalExposureValue / actualExposure;
+				LOG(RPiAgc, Debug) << "Want total exposure " << totalExposureValue;
+				/*
+				 * Never ask for a gain < 1.0, and also impose
+				 * some upper limit. Make it customisable?
+				 */
+				status_.digitalGain = std::max(1.0, std::min(status_.digitalGain, 4.0));
+				LOG(RPiAgc, Debug) << "Actual exposure " << actualExposure;
+				LOG(RPiAgc, Debug) << "Use digitalGain " << status_.digitalGain;
+				LOG(RPiAgc, Debug) << "Effective exposure "
+						   << actualExposure * status_.digitalGain;
+				/* Decide whether AEC/AGC has converged. */
+				updateLockStatus(deviceStatus);
+			}
+		} else
+			LOG(RPiAgc, Warning) << name() << ": no device metadata";
+		imageMetadata->set("agc.status", status_);
+	}
+}
+
+void Agc::process(StatisticsPtr &stats, Metadata *imageMetadata)
+{
+	frameCount_++;
+	/*
+	 * First a little bit of housekeeping, fetching up-to-date settings and
+	 * configuration, that kind of thing.
+	 */
+	housekeepConfig();
+	/* Get the current exposure values for the frame that's just arrived. */
+	fetchCurrentExposure(imageMetadata);
+	/* Compute the total gain we require relative to the current exposure. */
+	double gain, targetY;
+	computeGain(stats, imageMetadata, gain, targetY);
+	/* Now compute the target (final) exposure which we think we want. */
+	computeTargetExposure(gain);
+	/*
+	 * Some of the exposure has to be applied as digital gain, so work out
+	 * what that is. This function also tells us whether it's decided to
+	 * "desaturate" the image more quickly.
+	 */
+	bool desaturate = applyDigitalGain(gain, targetY);
+	/* The results have to be filtered so as not to change too rapidly. */
+	filterExposure(desaturate);
+	/*
+	 * The last thing is to divide up the exposure value into a shutter time
+	 * and analogue gain, according to the current exposure mode.
+	 */
+	divideUpExposure();
+	/* Finally advertise what we've done. */
+	writeAndFinish(imageMetadata, desaturate);
+}
+
+void Agc::updateLockStatus(DeviceStatus const &deviceStatus)
+{
+	const double errorFactor = 0.10; /* make these customisable? */
+	const int maxLockCount = 5;
+	/* Reset "lock count" when we exceed this multiple of errorFactor */
+	const double resetMargin = 1.5;
+
+	/* Add 200us to the exposure time error to allow for line quantisation. */
+	Duration exposureError = lastDeviceStatus_.shutterSpeed * errorFactor + 200us;
+	double gainError = lastDeviceStatus_.analogueGain * errorFactor;
+	Duration targetError = lastTargetExposure_ * errorFactor;
+
+	/*
+	 * Note that we don't know the exposure/gain limits of the sensor, so
+	 * the values we keep requesting may be unachievable. For this reason
+	 * we only insist that we're close to values in the past few frames.
+	 */
+	if (deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed - exposureError &&
+	    deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed + exposureError &&
+	    deviceStatus.analogueGain > lastDeviceStatus_.analogueGain - gainError &&
+	    deviceStatus.analogueGain < lastDeviceStatus_.analogueGain + gainError &&
+	    status_.targetExposureValue > lastTargetExposure_ - targetError &&
+	    status_.targetExposureValue < lastTargetExposure_ + targetError)
+		lockCount_ = std::min(lockCount_ + 1, maxLockCount);
+	else if (deviceStatus.shutterSpeed < lastDeviceStatus_.shutterSpeed - resetMargin * exposureError ||
+		 deviceStatus.shutterSpeed > lastDeviceStatus_.shutterSpeed + resetMargin * exposureError ||
+		 deviceStatus.analogueGain < lastDeviceStatus_.analogueGain - resetMargin * gainError ||
+		 deviceStatus.analogueGain > lastDeviceStatus_.analogueGain + resetMargin * gainError ||
+		 status_.targetExposureValue < lastTargetExposure_ - resetMargin * targetError ||
+		 status_.targetExposureValue > lastTargetExposure_ + resetMargin * targetError)
+		lockCount_ = 0;
+
+	lastDeviceStatus_ = deviceStatus;
+	lastTargetExposure_ = status_.targetExposureValue;
+
+	LOG(RPiAgc, Debug) << "Lock count updated to " << lockCount_;
+	status_.locked = lockCount_ == maxLockCount;
+}
+
+static void copyString(std::string const &s, char *d, size_t size)
+{
+	size_t length = s.copy(d, size - 1);
+	d[length] = '\0';
+}
+
+void Agc::housekeepConfig()
+{
+	/* First fetch all the up-to-date settings, so no one else has to do it. */
+	status_.ev = ev_;
+	status_.fixedShutter = limitShutter(fixedShutter_);
+	status_.fixedAnalogueGain = fixedAnalogueGain_;
+	status_.flickerPeriod = flickerPeriod_;
+	LOG(RPiAgc, Debug) << "ev " << status_.ev << " fixedShutter "
+			   << status_.fixedShutter << " fixedAnalogueGain "
+			   << status_.fixedAnalogueGain;
+	/*
+	 * Make sure the "mode" pointers point to the up-to-date things, if
+	 * they've changed.
+	 */
+	if (strcmp(meteringModeName_.c_str(), status_.meteringMode)) {
+		auto it = config_.meteringModes.find(meteringModeName_);
+		if (it == config_.meteringModes.end())
+			LOG(RPiAgc, Fatal) << "No metering mode " << meteringModeName_;
+		meteringMode_ = &it->second;
+		copyString(meteringModeName_, status_.meteringMode,
+			   sizeof(status_.meteringMode));
+	}
+	if (strcmp(exposureModeName_.c_str(), status_.exposureMode)) {
+		auto it = config_.exposureModes.find(exposureModeName_);
+		if (it == config_.exposureModes.end())
+			LOG(RPiAgc, Fatal) << "No exposure profile " << exposureModeName_;
+		exposureMode_ = &it->second;
+		copyString(exposureModeName_, status_.exposureMode,
+			   sizeof(status_.exposureMode));
+	}
+	if (strcmp(constraintModeName_.c_str(), status_.constraintMode)) {
+		auto it =
+			config_.constraintModes.find(constraintModeName_);
+		if (it == config_.constraintModes.end())
+			LOG(RPiAgc, Fatal) << "No constraint list " << constraintModeName_;
+		constraintMode_ = &it->second;
+		copyString(constraintModeName_, status_.constraintMode,
+			   sizeof(status_.constraintMode));
+	}
+	LOG(RPiAgc, Debug) << "exposureMode "
+			   << exposureModeName_ << " constraintMode "
+			   << constraintModeName_ << " meteringMode "
+			   << meteringModeName_;
+}
+
+void Agc::fetchCurrentExposure(Metadata *imageMetadata)
+{
+	std::unique_lock<Metadata> lock(*imageMetadata);
+	DeviceStatus *deviceStatus =
+		imageMetadata->getLocked<DeviceStatus>("device.status");
+	if (!deviceStatus)
+		LOG(RPiAgc, Fatal) << "No device metadata";
+	current_.shutter = deviceStatus->shutterSpeed;
+	current_.analogueGain = deviceStatus->analogueGain;
+	AgcStatus *agcStatus =
+		imageMetadata->getLocked<AgcStatus>("agc.status");
+	current_.totalExposure = agcStatus ? agcStatus->totalExposureValue : 0s;
+	current_.totalExposureNoDG = current_.shutter * current_.analogueGain;
+}
+
+void Agc::fetchAwbStatus(Metadata *imageMetadata)
+{
+	awb_.gainR = 1.0; /* in case not found in metadata */
+	awb_.gainG = 1.0;
+	awb_.gainB = 1.0;
+	if (imageMetadata->get("awb.status", awb_) != 0)
+		LOG(RPiAgc, Debug) << "No AWB status found";
+}
+
+static double computeInitialY(StatisticsPtr &stats, AwbStatus const &awb,
+			      std::vector<double> &weights, double gain)
+{
+	constexpr uint64_t maxVal = 1 << Statistics::NormalisationFactorPow2;
+
+	ASSERT(weights.size() == stats->agcRegions.numRegions());
+
+	/*
+	 * Note how the calculation below means that equal weights give you
+	 * "average" metering (i.e. all pixels equally important).
+	 */
+	double rSum = 0, gSum = 0, bSum = 0, pixelSum = 0;
+	for (unsigned int i = 0; i < stats->agcRegions.numRegions(); i++) {
+		auto &region = stats->agcRegions.get(i);
+		double rAcc = std::min<double>(region.val.rSum * gain, (maxVal - 1) * region.counted);
+		double gAcc = std::min<double>(region.val.gSum * gain, (maxVal - 1) * region.counted);
+		double bAcc = std::min<double>(region.val.bSum * gain, (maxVal - 1) * region.counted);
+		rSum += rAcc * weights[i];
+		gSum += gAcc * weights[i];
+		bSum += bAcc * weights[i];
+		pixelSum += region.counted * weights[i];
+	}
+	if (pixelSum == 0.0) {
+		LOG(RPiAgc, Warning) << "computeInitialY: pixelSum is zero";
+		return 0;
+	}
+	double ySum = rSum * awb.gainR * .299 +
+		      gSum * awb.gainG * .587 +
+		      bSum * awb.gainB * .114;
+	return ySum / pixelSum / maxVal;
+}
+
+/*
+ * We handle extra gain through EV by adjusting our Y targets. However, you
+ * simply can't monitor histograms once they get very close to (or beyond!)
+ * saturation, so we clamp the Y targets to this value. It does mean that EV
+ * increases don't necessarily do quite what you might expect in certain
+ * (contrived) cases.
+ */
+
+static constexpr double EvGainYTargetLimit = 0.9;
+
+static double constraintComputeGain(AgcConstraint &c, const Histogram &h, double lux,
+				    double evGain, double &targetY)
+{
+	targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));
+	targetY = std::min(EvGainYTargetLimit, targetY * evGain);
+	double iqm = h.interQuantileMean(c.qLo, c.qHi);
+	return (targetY * h.bins()) / iqm;
+}
+
+void Agc::computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,
+		      double &gain, double &targetY)
+{
+	struct LuxStatus lux = {};
+	lux.lux = 400; /* default lux level to 400 in case no metadata found */
+	if (imageMetadata->get("lux.status", lux) != 0)
+		LOG(RPiAgc, Warning) << "No lux level found";
+	const Histogram &h = statistics->yHist;
+	double evGain = status_.ev * config_.baseEv;
+	/*
+	 * The initial gain and target_Y come from some of the regions. After
+	 * that we consider the histogram constraints.
+	 */
+	targetY = config_.yTarget.eval(config_.yTarget.domain().clip(lux.lux));
+	targetY = std::min(EvGainYTargetLimit, targetY * evGain);
+
+	/*
+	 * Do this calculation a few times as brightness increase can be
+	 * non-linear when there are saturated regions.
+	 */
+	gain = 1.0;
+	for (int i = 0; i < 8; i++) {
+		double initialY = computeInitialY(statistics, awb_, meteringMode_->weights, gain);
+		double extraGain = std::min(10.0, targetY / (initialY + .001));
+		gain *= extraGain;
+		LOG(RPiAgc, Debug) << "Initial Y " << initialY << " target " << targetY
+				   << " gives gain " << gain;
+		if (extraGain < 1.01) /* close enough */
+			break;
+	}
+
+	for (auto &c : *constraintMode_) {
+		double newTargetY;
+		double newGain = constraintComputeGain(c, h, lux.lux, evGain, newTargetY);
+		LOG(RPiAgc, Debug) << "Constraint has target_Y "
+				   << newTargetY << " giving gain " << newGain;
+		if (c.bound == AgcConstraint::Bound::LOWER && newGain > gain) {
+			LOG(RPiAgc, Debug) << "Lower bound constraint adopted";
+			gain = newGain;
+			targetY = newTargetY;
+		} else if (c.bound == AgcConstraint::Bound::UPPER && newGain < gain) {
+			LOG(RPiAgc, Debug) << "Upper bound constraint adopted";
+			gain = newGain;
+			targetY = newTargetY;
+		}
+	}
+	LOG(RPiAgc, Debug) << "Final gain " << gain << " (target_Y " << targetY << " ev "
+			   << status_.ev << " base_ev " << config_.baseEv
+			   << ")";
+}
+
+void Agc::computeTargetExposure(double gain)
+{
+	if (status_.fixedShutter && status_.fixedAnalogueGain) {
+		/*
+		 * When ag and shutter are both fixed, we need to drive the
+		 * total exposure so that we end up with a digital gain of at least
+		 * 1/minColourGain. Otherwise we'd desaturate channels causing
+		 * white to go cyan or magenta.
+		 */
+		double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });
+		ASSERT(minColourGain != 0.0);
+		target_.totalExposure =
+			status_.fixedShutter * status_.fixedAnalogueGain / minColourGain;
+	} else {
+		/*
+		 * The statistics reflect the image without digital gain, so the final
+		 * total exposure we're aiming for is:
+		 */
+		target_.totalExposure = current_.totalExposureNoDG * gain;
+		/* The final target exposure is also limited to what the exposure mode allows. */
+		Duration maxShutter = status_.fixedShutter
+					      ? status_.fixedShutter
+					      : exposureMode_->shutter.back();
+		maxShutter = limitShutter(maxShutter);
+		Duration maxTotalExposure =
+			maxShutter *
+			(status_.fixedAnalogueGain != 0.0
+				 ? status_.fixedAnalogueGain
+				 : exposureMode_->gain.back());
+		target_.totalExposure = std::min(target_.totalExposure, maxTotalExposure);
+	}
+	LOG(RPiAgc, Debug) << "Target totalExposure " << target_.totalExposure;
+}
+
+bool Agc::applyDigitalGain(double gain, double targetY)
+{
+	double minColourGain = std::min({ awb_.gainR, awb_.gainG, awb_.gainB, 1.0 });
+	ASSERT(minColourGain != 0.0);
+	double dg = 1.0 / minColourGain;
+	/*
+	 * I think this pipeline subtracts black level and rescales before we
+	 * get the stats, so no need to worry about it.
+	 */
+	LOG(RPiAgc, Debug) << "after AWB, target dg " << dg << " gain " << gain
+			   << " target_Y " << targetY;
+	/*
+	 * Finally, if we're trying to reduce exposure but the target_Y is
+	 * "close" to 1.0, then the gain computed for that constraint will be
+	 * only slightly less than one, because the measured Y can never be
+	 * larger than 1.0. When this happens, demand a large digital gain so
+	 * that the exposure can be reduced, de-saturating the image much more
+	 * quickly (and we then approach the correct value more quickly from
+	 * below).
+	 */
+	bool desaturate = targetY > config_.fastReduceThreshold &&
+			  gain < sqrt(targetY);
+	if (desaturate)
+		dg /= config_.fastReduceThreshold;
+	LOG(RPiAgc, Debug) << "Digital gain " << dg << " desaturate? " << desaturate;
+	target_.totalExposureNoDG = target_.totalExposure / dg;
+	LOG(RPiAgc, Debug) << "Target totalExposureNoDG " << target_.totalExposureNoDG;
+	return desaturate;
+}
+
+void Agc::filterExposure(bool desaturate)
+{
+	double speed = config_.speed;
+	/*
+	 * AGC adapts instantly if both shutter and gain are directly specified
+	 * or we're in the startup phase.
+	 */
+	if ((status_.fixedShutter && status_.fixedAnalogueGain) ||
+	    frameCount_ <= config_.startupFrames)
+		speed = 1.0;
+	if (!filtered_.totalExposure) {
+		filtered_.totalExposure = target_.totalExposure;
+		filtered_.totalExposureNoDG = target_.totalExposureNoDG;
+	} else {
+		/*
+		 * If close to the result go faster, to save making so many
+		 * micro-adjustments on the way. (Make this customisable?)
+		 */
+		if (filtered_.totalExposure < 1.2 * target_.totalExposure &&
+		    filtered_.totalExposure > 0.8 * target_.totalExposure)
+			speed = sqrt(speed);
+		filtered_.totalExposure = speed * target_.totalExposure +
+					  filtered_.totalExposure * (1.0 - speed);
+		/*
+		 * When desaturing, take a big jump down in totalExposureNoDG,
+		 * which we'll hide with digital gain.
+		 */
+		if (desaturate)
+			filtered_.totalExposureNoDG =
+				target_.totalExposureNoDG;
+		else
+			filtered_.totalExposureNoDG =
+				speed * target_.totalExposureNoDG +
+				filtered_.totalExposureNoDG * (1.0 - speed);
+	}
+	/*
+	 * We can't let the totalExposureNoDG exposure deviate too far below the
+	 * total exposure, as there might not be enough digital gain available
+	 * in the ISP to hide it (which will cause nasty oscillation).
+	 */
+	if (filtered_.totalExposureNoDG <
+	    filtered_.totalExposure * config_.fastReduceThreshold)
+		filtered_.totalExposureNoDG = filtered_.totalExposure * config_.fastReduceThreshold;
+	LOG(RPiAgc, Debug) << "After filtering, totalExposure " << filtered_.totalExposure
+			   << " no dg " << filtered_.totalExposureNoDG;
+}
+
+void Agc::divideUpExposure()
+{
+	/*
+	 * Sending the fixed shutter/gain cases through the same code may seem
+	 * unnecessary, but it will make more sense when extend this to cover
+	 * variable aperture.
+	 */
+	Duration exposureValue = filtered_.totalExposureNoDG;
+	Duration shutterTime;
+	double analogueGain;
+	shutterTime = status_.fixedShutter ? status_.fixedShutter
+					   : exposureMode_->shutter[0];
+	shutterTime = limitShutter(shutterTime);
+	analogueGain = status_.fixedAnalogueGain != 0.0 ? status_.fixedAnalogueGain
+							: exposureMode_->gain[0];
+	analogueGain = limitGain(analogueGain);
+	if (shutterTime * analogueGain < exposureValue) {
+		for (unsigned int stage = 1;
+		     stage < exposureMode_->gain.size(); stage++) {
+			if (!status_.fixedShutter) {
+				Duration stageShutter =
+					limitShutter(exposureMode_->shutter[stage]);
+				if (stageShutter * analogueGain >= exposureValue) {
+					shutterTime = exposureValue / analogueGain;
+					break;
+				}
+				shutterTime = stageShutter;
+			}
+			if (status_.fixedAnalogueGain == 0.0) {
+				if (exposureMode_->gain[stage] * shutterTime >= exposureValue) {
+					analogueGain = exposureValue / shutterTime;
+					break;
+				}
+				analogueGain = exposureMode_->gain[stage];
+				analogueGain = limitGain(analogueGain);
+			}
+		}
+	}
+	LOG(RPiAgc, Debug) << "Divided up shutter and gain are " << shutterTime << " and "
+			   << analogueGain;
+	/*
+	 * Finally adjust shutter time for flicker avoidance (require both
+	 * shutter and gain not to be fixed).
+	 */
+	if (!status_.fixedShutter && !status_.fixedAnalogueGain &&
+	    status_.flickerPeriod) {
+		int flickerPeriods = shutterTime / status_.flickerPeriod;
+		if (flickerPeriods) {
+			Duration newShutterTime = flickerPeriods * status_.flickerPeriod;
+			analogueGain *= shutterTime / newShutterTime;
+			/*
+			 * We should still not allow the ag to go over the
+			 * largest value in the exposure mode. Note that this
+			 * may force more of the total exposure into the digital
+			 * gain as a side-effect.
+			 */
+			analogueGain = std::min(analogueGain, exposureMode_->gain.back());
+			analogueGain = limitGain(analogueGain);
+			shutterTime = newShutterTime;
+		}
+		LOG(RPiAgc, Debug) << "After flicker avoidance, shutter "
+				   << shutterTime << " gain " << analogueGain;
+	}
+	filtered_.shutter = shutterTime;
+	filtered_.analogueGain = analogueGain;
+}
+
+void Agc::writeAndFinish(Metadata *imageMetadata, bool desaturate)
+{
+	status_.totalExposureValue = filtered_.totalExposure;
+	status_.targetExposureValue = desaturate ? 0s : target_.totalExposureNoDG;
+	status_.shutterTime = filtered_.shutter;
+	status_.analogueGain = filtered_.analogueGain;
+	/*
+	 * Write to metadata as well, in case anyone wants to update the camera
+	 * immediately.
+	 */
+	imageMetadata->set("agc.status", status_);
+	LOG(RPiAgc, Debug) << "Output written, total exposure requested is "
+			   << filtered_.totalExposure;
+	LOG(RPiAgc, Debug) << "Camera exposure update: shutter time " << filtered_.shutter
+			   << " analogue gain " << filtered_.analogueGain;
+}
+
+Duration Agc::limitShutter(Duration shutter)
+{
+	/*
+	 * shutter == 0 is a special case for fixed shutter values, and must pass
+	 * through unchanged
+	 */
+	if (!shutter)
+		return shutter;
+
+	shutter = std::clamp(shutter, mode_.minShutter, maxShutter_);
+	return shutter;
+}
+
+double Agc::limitGain(double gain) const
+{
+	/*
+	 * Only limit the lower bounds of the gain value to what the sensor limits.
+	 * The upper bound on analogue gain will be made up with additional digital
+	 * gain applied by the ISP.
+	 *
+	 * gain == 0.0 is a special case for fixed shutter values, and must pass
+	 * through unchanged
+	 */
+	if (!gain)
+		return gain;
+
+	gain = std::max(gain, mode_.minAnalogueGain);
+	return gain;
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Agc(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/agc.h b/src/ipa/rpi/controller/rpi/agc.h
new file mode 100644
index 00000000..4e5f272f
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/agc.h
@@ -0,0 +1,133 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * agc.h - AGC/AEC control algorithm
+ */
+#pragma once
+
+#include <vector>
+#include <mutex>
+
+#include <libcamera/base/utils.h>
+
+#include "../agc_algorithm.h"
+#include "../agc_status.h"
+#include "../pwl.h"
+
+/* This is our implementation of AGC. */
+
+namespace RPiController {
+
+struct AgcMeteringMode {
+	std::vector<double> weights;
+	int read(const libcamera::YamlObject &params);
+};
+
+struct AgcExposureMode {
+	std::vector<libcamera::utils::Duration> shutter;
+	std::vector<double> gain;
+	int read(const libcamera::YamlObject &params);
+};
+
+struct AgcConstraint {
+	enum class Bound { LOWER = 0, UPPER = 1 };
+	Bound bound;
+	double qLo;
+	double qHi;
+	Pwl yTarget;
+	int read(const libcamera::YamlObject &params);
+};
+
+typedef std::vector<AgcConstraint> AgcConstraintMode;
+
+struct AgcConfig {
+	int read(const libcamera::YamlObject &params);
+	std::map<std::string, AgcMeteringMode> meteringModes;
+	std::map<std::string, AgcExposureMode> exposureModes;
+	std::map<std::string, AgcConstraintMode> constraintModes;
+	Pwl yTarget;
+	double speed;
+	uint16_t startupFrames;
+	unsigned int convergenceFrames;
+	double maxChange;
+	double minChange;
+	double fastReduceThreshold;
+	double speedUpThreshold;
+	std::string defaultMeteringMode;
+	std::string defaultExposureMode;
+	std::string defaultConstraintMode;
+	double baseEv;
+	libcamera::utils::Duration defaultExposureTime;
+	double defaultAnalogueGain;
+};
+
+class Agc : public AgcAlgorithm
+{
+public:
+	Agc(Controller *controller);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	unsigned int getConvergenceFrames() const override;
+	void setEv(double ev) override;
+	void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) override;
+	void setMaxShutter(libcamera::utils::Duration maxShutter) override;
+	void setFixedShutter(libcamera::utils::Duration fixedShutter) override;
+	void setFixedAnalogueGain(double fixedAnalogueGain) override;
+	void setMeteringMode(std::string const &meteringModeName) override;
+	void setExposureMode(std::string const &exposureModeName) override;
+	void setConstraintMode(std::string const &contraintModeName) override;
+	void enableAuto() override;
+	void disableAuto() override;
+	void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;
+	void prepare(Metadata *imageMetadata) override;
+	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
+
+private:
+	void updateLockStatus(DeviceStatus const &deviceStatus);
+	AgcConfig config_;
+	void housekeepConfig();
+	void fetchCurrentExposure(Metadata *imageMetadata);
+	void fetchAwbStatus(Metadata *imageMetadata);
+	void computeGain(StatisticsPtr &statistics, Metadata *imageMetadata,
+			 double &gain, double &targetY);
+	void computeTargetExposure(double gain);
+	bool applyDigitalGain(double gain, double targetY);
+	void filterExposure(bool desaturate);
+	void divideUpExposure();
+	void writeAndFinish(Metadata *imageMetadata, bool desaturate);
+	libcamera::utils::Duration limitShutter(libcamera::utils::Duration shutter);
+	double limitGain(double gain) const;
+	AgcMeteringMode *meteringMode_;
+	AgcExposureMode *exposureMode_;
+	AgcConstraintMode *constraintMode_;
+	CameraMode mode_;
+	uint64_t frameCount_;
+	AwbStatus awb_;
+	struct ExposureValues {
+		ExposureValues();
+
+		libcamera::utils::Duration shutter;
+		double analogueGain;
+		libcamera::utils::Duration totalExposure;
+		libcamera::utils::Duration totalExposureNoDG; /* without digital gain */
+	};
+	ExposureValues current_;  /* values for the current frame */
+	ExposureValues target_;   /* calculate the values we want here */
+	ExposureValues filtered_; /* these values are filtered towards target */
+	AgcStatus status_;
+	int lockCount_;
+	DeviceStatus lastDeviceStatus_;
+	libcamera::utils::Duration lastTargetExposure_;
+	/* Below here the "settings" that applications can change. */
+	std::string meteringModeName_;
+	std::string exposureModeName_;
+	std::string constraintModeName_;
+	double ev_;
+	libcamera::utils::Duration flickerPeriod_;
+	libcamera::utils::Duration maxShutter_;
+	libcamera::utils::Duration fixedShutter_;
+	double fixedAnalogueGain_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/alsc.cpp b/src/ipa/rpi/controller/rpi/alsc.cpp
new file mode 100644
index 00000000..3a2e8fe0
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/alsc.cpp
@@ -0,0 +1,865 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * alsc.cpp - ALSC (auto lens shading correction) control algorithm
+ */
+
+#include <algorithm>
+#include <functional>
+#include <math.h>
+#include <numeric>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/span.h>
+
+#include "../awb_status.h"
+#include "alsc.h"
+
+/* Raspberry Pi ALSC (Auto Lens Shading Correction) algorithm. */
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiAlsc)
+
+#define NAME "rpi.alsc"
+
+static const double InsufficientData = -1.0;
+
+Alsc::Alsc(Controller *controller)
+	: Algorithm(controller)
+{
+	asyncAbort_ = asyncStart_ = asyncStarted_ = asyncFinished_ = false;
+	asyncThread_ = std::thread(std::bind(&Alsc::asyncFunc, this));
+}
+
+Alsc::~Alsc()
+{
+	{
+		std::lock_guard<std::mutex> lock(mutex_);
+		asyncAbort_ = true;
+	}
+	asyncSignal_.notify_one();
+	asyncThread_.join();
+}
+
+char const *Alsc::name() const
+{
+	return NAME;
+}
+
+static int generateLut(Array2D<double> &lut, const libcamera::YamlObject &params)
+{
+	/* These must be signed ints for the co-ordinate calculations below. */
+	int X = lut.dimensions().width, Y = lut.dimensions().height;
+	double cstrength = params["corner_strength"].get<double>(2.0);
+	if (cstrength <= 1.0) {
+		LOG(RPiAlsc, Error) << "corner_strength must be > 1.0";
+		return -EINVAL;
+	}
+
+	double asymmetry = params["asymmetry"].get<double>(1.0);
+	if (asymmetry < 0) {
+		LOG(RPiAlsc, Error) << "asymmetry must be >= 0";
+		return -EINVAL;
+	}
+
+	double f1 = cstrength - 1, f2 = 1 + sqrt(cstrength);
+	double R2 = X * Y / 4 * (1 + asymmetry * asymmetry);
+	int num = 0;
+	for (int y = 0; y < Y; y++) {
+		for (int x = 0; x < X; x++) {
+			double dy = y - Y / 2 + 0.5,
+			       dx = (x - X / 2 + 0.5) * asymmetry;
+			double r2 = (dx * dx + dy * dy) / R2;
+			lut[num++] =
+				(f1 * r2 + f2) * (f1 * r2 + f2) /
+				(f2 * f2); /* this reproduces the cos^4 rule */
+		}
+	}
+	return 0;
+}
+
+static int readLut(Array2D<double> &lut, const libcamera::YamlObject &params)
+{
+	if (params.size() != lut.size()) {
+		LOG(RPiAlsc, Error) << "Invalid number of entries in LSC table";
+		return -EINVAL;
+	}
+
+	int num = 0;
+	for (const auto &p : params.asList()) {
+		auto value = p.get<double>();
+		if (!value)
+			return -EINVAL;
+		lut[num++] = *value;
+	}
+
+	return 0;
+}
+
+static int readCalibrations(std::vector<AlscCalibration> &calibrations,
+			    const libcamera::YamlObject &params,
+			    std::string const &name, const Size &size)
+{
+	if (params.contains(name)) {
+		double lastCt = 0;
+		for (const auto &p : params[name].asList()) {
+			auto value = p["ct"].get<double>();
+			if (!value)
+				return -EINVAL;
+			double ct = *value;
+			if (ct <= lastCt) {
+				LOG(RPiAlsc, Error)
+					<< "Entries in " << name << " must be in increasing ct order";
+				return -EINVAL;
+			}
+			AlscCalibration calibration;
+			calibration.ct = lastCt = ct;
+
+			const libcamera::YamlObject &table = p["table"];
+			if (table.size() != size.width * size.height) {
+				LOG(RPiAlsc, Error)
+					<< "Incorrect number of values for ct "
+					<< ct << " in " << name;
+				return -EINVAL;
+			}
+
+			int num = 0;
+			calibration.table.resize(size);
+			for (const auto &elem : table.asList()) {
+				value = elem.get<double>();
+				if (!value)
+					return -EINVAL;
+				calibration.table[num++] = *value;
+			}
+
+			calibrations.push_back(std::move(calibration));
+			LOG(RPiAlsc, Debug)
+				<< "Read " << name << " calibration for ct " << ct;
+		}
+	}
+	return 0;
+}
+
+int Alsc::read(const libcamera::YamlObject &params)
+{
+	config_.tableSize = getHardwareConfig().awbRegions;
+	config_.framePeriod = params["frame_period"].get<uint16_t>(12);
+	config_.startupFrames = params["startup_frames"].get<uint16_t>(10);
+	config_.speed = params["speed"].get<double>(0.05);
+	double sigma = params["sigma"].get<double>(0.01);
+	config_.sigmaCr = params["sigma_Cr"].get<double>(sigma);
+	config_.sigmaCb = params["sigma_Cb"].get<double>(sigma);
+	config_.minCount = params["min_count"].get<double>(10.0);
+	config_.minG = params["min_G"].get<uint16_t>(50);
+	config_.omega = params["omega"].get<double>(1.3);
+	config_.nIter = params["n_iter"].get<uint32_t>(config_.tableSize.width + config_.tableSize.height);
+	config_.luminanceStrength =
+		params["luminance_strength"].get<double>(1.0);
+
+	config_.luminanceLut.resize(config_.tableSize, 1.0);
+	int ret = 0;
+
+	if (params.contains("corner_strength"))
+		ret = generateLut(config_.luminanceLut, params);
+	else if (params.contains("luminance_lut"))
+		ret = readLut(config_.luminanceLut, params["luminance_lut"]);
+	else
+		LOG(RPiAlsc, Warning)
+			<< "no luminance table - assume unity everywhere";
+	if (ret)
+		return ret;
+
+	ret = readCalibrations(config_.calibrationsCr, params, "calibrations_Cr",
+			       config_.tableSize);
+	if (ret)
+		return ret;
+	ret = readCalibrations(config_.calibrationsCb, params, "calibrations_Cb",
+			       config_.tableSize);
+	if (ret)
+		return ret;
+
+	config_.defaultCt = params["default_ct"].get<double>(4500.0);
+	config_.threshold = params["threshold"].get<double>(1e-3);
+	config_.lambdaBound = params["lambda_bound"].get<double>(0.05);
+
+	return 0;
+}
+
+static double getCt(Metadata *metadata, double defaultCt);
+static void getCalTable(double ct, std::vector<AlscCalibration> const &calibrations,
+			Array2D<double> &calTable);
+static void resampleCalTable(const Array2D<double> &calTableIn, CameraMode const &cameraMode,
+			     Array2D<double> &calTableOut);
+static void compensateLambdasForCal(const Array2D<double> &calTable,
+				    const Array2D<double> &oldLambdas,
+				    Array2D<double> &newLambdas);
+static void addLuminanceToTables(std::array<Array2D<double>, 3> &results,
+				 const Array2D<double> &lambdaR, double lambdaG,
+				 const Array2D<double> &lambdaB,
+				 const Array2D<double> &luminanceLut,
+				 double luminanceStrength);
+
+void Alsc::initialise()
+{
+	frameCount2_ = frameCount_ = framePhase_ = 0;
+	firstTime_ = true;
+	ct_ = config_.defaultCt;
+
+	const size_t XY = config_.tableSize.width * config_.tableSize.height;
+
+	for (auto &r : syncResults_)
+		r.resize(config_.tableSize);
+	for (auto &r : prevSyncResults_)
+		r.resize(config_.tableSize);
+	for (auto &r : asyncResults_)
+		r.resize(config_.tableSize);
+
+	luminanceTable_.resize(config_.tableSize);
+	asyncLambdaR_.resize(config_.tableSize);
+	asyncLambdaB_.resize(config_.tableSize);
+	/* The lambdas are initialised in the SwitchMode. */
+	lambdaR_.resize(config_.tableSize);
+	lambdaB_.resize(config_.tableSize);
+
+	/* Temporaries for the computations, but sensible to allocate this up-front! */
+	for (auto &c : tmpC_)
+		c.resize(config_.tableSize);
+	for (auto &m : tmpM_)
+		m.resize(XY);
+}
+
+void Alsc::waitForAysncThread()
+{
+	if (asyncStarted_) {
+		asyncStarted_ = false;
+		std::unique_lock<std::mutex> lock(mutex_);
+		syncSignal_.wait(lock, [&] {
+			return asyncFinished_;
+		});
+		asyncFinished_ = false;
+	}
+}
+
+static bool compareModes(CameraMode const &cm0, CameraMode const &cm1)
+{
+	/*
+	 * Return true if the modes crop from the sensor significantly differently,
+	 * or if the user transform has changed.
+	 */
+	if (cm0.transform != cm1.transform)
+		return true;
+	int leftDiff = abs(cm0.cropX - cm1.cropX);
+	int topDiff = abs(cm0.cropY - cm1.cropY);
+	int rightDiff = fabs(cm0.cropX + cm0.scaleX * cm0.width -
+			     cm1.cropX - cm1.scaleX * cm1.width);
+	int bottomDiff = fabs(cm0.cropY + cm0.scaleY * cm0.height -
+			      cm1.cropY - cm1.scaleY * cm1.height);
+	/*
+	 * These thresholds are a rather arbitrary amount chosen to trigger
+	 * when carrying on with the previously calculated tables might be
+	 * worse than regenerating them (but without the adaptive algorithm).
+	 */
+	int thresholdX = cm0.sensorWidth >> 4;
+	int thresholdY = cm0.sensorHeight >> 4;
+	return leftDiff > thresholdX || rightDiff > thresholdX ||
+	       topDiff > thresholdY || bottomDiff > thresholdY;
+}
+
+void Alsc::switchMode(CameraMode const &cameraMode,
+		      [[maybe_unused]] Metadata *metadata)
+{
+	/*
+	 * We're going to start over with the tables if there's any "significant"
+	 * change.
+	 */
+	bool resetTables = firstTime_ || compareModes(cameraMode_, cameraMode);
+
+	/* Believe the colour temperature from the AWB, if there is one. */
+	ct_ = getCt(metadata, ct_);
+
+	/* Ensure the other thread isn't running while we do this. */
+	waitForAysncThread();
+
+	cameraMode_ = cameraMode;
+
+	/*
+	 * We must resample the luminance table like we do the others, but it's
+	 * fixed so we can simply do it up front here.
+	 */
+	resampleCalTable(config_.luminanceLut, cameraMode_, luminanceTable_);
+
+	if (resetTables) {
+		/*
+		 * Upon every "table reset", arrange for something sensible to be
+		 * generated. Construct the tables for the previous recorded colour
+		 * temperature. In order to start over from scratch we initialise
+		 * the lambdas, but the rest of this code then echoes the code in
+		 * doAlsc, without the adaptive algorithm.
+		 */
+		std::fill(lambdaR_.begin(), lambdaR_.end(), 1.0);
+		std::fill(lambdaB_.begin(), lambdaB_.end(), 1.0);
+		Array2D<double> &calTableR = tmpC_[0], &calTableB = tmpC_[1], &calTableTmp = tmpC_[2];
+		getCalTable(ct_, config_.calibrationsCr, calTableTmp);
+		resampleCalTable(calTableTmp, cameraMode_, calTableR);
+		getCalTable(ct_, config_.calibrationsCb, calTableTmp);
+		resampleCalTable(calTableTmp, cameraMode_, calTableB);
+		compensateLambdasForCal(calTableR, lambdaR_, asyncLambdaR_);
+		compensateLambdasForCal(calTableB, lambdaB_, asyncLambdaB_);
+		addLuminanceToTables(syncResults_, asyncLambdaR_, 1.0, asyncLambdaB_,
+				     luminanceTable_, config_.luminanceStrength);
+		prevSyncResults_ = syncResults_;
+		framePhase_ = config_.framePeriod; /* run the algo again asap */
+		firstTime_ = false;
+	}
+}
+
+void Alsc::fetchAsyncResults()
+{
+	LOG(RPiAlsc, Debug) << "Fetch ALSC results";
+	asyncFinished_ = false;
+	asyncStarted_ = false;
+	syncResults_ = asyncResults_;
+}
+
+double getCt(Metadata *metadata, double defaultCt)
+{
+	AwbStatus awbStatus;
+	awbStatus.temperatureK = defaultCt; /* in case nothing found */
+	if (metadata->get("awb.status", awbStatus) != 0)
+		LOG(RPiAlsc, Debug) << "no AWB results found, using "
+				    << awbStatus.temperatureK;
+	else
+		LOG(RPiAlsc, Debug) << "AWB results found, using "
+				    << awbStatus.temperatureK;
+	return awbStatus.temperatureK;
+}
+
+static void copyStats(RgbyRegions &regions, StatisticsPtr &stats,
+		      AlscStatus const &status)
+{
+	if (!regions.numRegions())
+		regions.init(stats->awbRegions.size());
+
+	const std::vector<double> &rTable = status.r;
+	const std::vector<double> &gTable = status.g;
+	const std::vector<double> &bTable = status.b;
+	for (unsigned int i = 0; i < stats->awbRegions.numRegions(); i++) {
+		auto r = stats->awbRegions.get(i);
+		r.val.rSum = static_cast<uint64_t>(r.val.rSum / rTable[i]);
+		r.val.gSum = static_cast<uint64_t>(r.val.gSum / gTable[i]);
+		r.val.bSum = static_cast<uint64_t>(r.val.bSum / bTable[i]);
+		regions.set(i, r);
+	}
+}
+
+void Alsc::restartAsync(StatisticsPtr &stats, Metadata *imageMetadata)
+{
+	LOG(RPiAlsc, Debug) << "Starting ALSC calculation";
+	/*
+	 * Get the current colour temperature. It's all we need from the
+	 * metadata. Default to the last CT value (which could be the default).
+	 */
+	ct_ = getCt(imageMetadata, ct_);
+	/*
+	 * We have to copy the statistics here, dividing out our best guess of
+	 * the LSC table that the pipeline applied to them.
+	 */
+	AlscStatus alscStatus;
+	if (imageMetadata->get("alsc.status", alscStatus) != 0) {
+		LOG(RPiAlsc, Warning)
+			<< "No ALSC status found for applied gains!";
+		alscStatus.r.resize(config_.tableSize.width * config_.tableSize.height, 1.0);
+		alscStatus.g.resize(config_.tableSize.width * config_.tableSize.height, 1.0);
+		alscStatus.b.resize(config_.tableSize.width * config_.tableSize.height, 1.0);
+	}
+	copyStats(statistics_, stats, alscStatus);
+	framePhase_ = 0;
+	asyncStarted_ = true;
+	{
+		std::lock_guard<std::mutex> lock(mutex_);
+		asyncStart_ = true;
+	}
+	asyncSignal_.notify_one();
+}
+
+void Alsc::prepare(Metadata *imageMetadata)
+{
+	/*
+	 * Count frames since we started, and since we last poked the async
+	 * thread.
+	 */
+	if (frameCount_ < (int)config_.startupFrames)
+		frameCount_++;
+	double speed = frameCount_ < (int)config_.startupFrames
+			       ? 1.0
+			       : config_.speed;
+	LOG(RPiAlsc, Debug)
+		<< "frame count " << frameCount_ << " speed " << speed;
+	{
+		std::unique_lock<std::mutex> lock(mutex_);
+		if (asyncStarted_ && asyncFinished_)
+			fetchAsyncResults();
+	}
+	/* Apply IIR filter to results and program into the pipeline. */
+	for (unsigned int j = 0; j < syncResults_.size(); j++) {
+		for (unsigned int i = 0; i < syncResults_[j].size(); i++)
+			prevSyncResults_[j][i] = speed * syncResults_[j][i] + (1.0 - speed) * prevSyncResults_[j][i];
+	}
+	/* Put output values into status metadata. */
+	AlscStatus status;
+	status.r = prevSyncResults_[0].data();
+	status.g = prevSyncResults_[1].data();
+	status.b = prevSyncResults_[2].data();
+	imageMetadata->set("alsc.status", status);
+}
+
+void Alsc::process(StatisticsPtr &stats, Metadata *imageMetadata)
+{
+	/*
+	 * Count frames since we started, and since we last poked the async
+	 * thread.
+	 */
+	if (framePhase_ < (int)config_.framePeriod)
+		framePhase_++;
+	if (frameCount2_ < (int)config_.startupFrames)
+		frameCount2_++;
+	LOG(RPiAlsc, Debug) << "frame_phase " << framePhase_;
+	if (framePhase_ >= (int)config_.framePeriod ||
+	    frameCount2_ < (int)config_.startupFrames) {
+		if (asyncStarted_ == false)
+			restartAsync(stats, imageMetadata);
+	}
+}
+
+void Alsc::asyncFunc()
+{
+	while (true) {
+		{
+			std::unique_lock<std::mutex> lock(mutex_);
+			asyncSignal_.wait(lock, [&] {
+				return asyncStart_ || asyncAbort_;
+			});
+			asyncStart_ = false;
+			if (asyncAbort_)
+				break;
+		}
+		doAlsc();
+		{
+			std::lock_guard<std::mutex> lock(mutex_);
+			asyncFinished_ = true;
+		}
+		syncSignal_.notify_one();
+	}
+}
+
+void getCalTable(double ct, std::vector<AlscCalibration> const &calibrations,
+		 Array2D<double> &calTable)
+{
+	if (calibrations.empty()) {
+		std::fill(calTable.begin(), calTable.end(), 1.0);
+		LOG(RPiAlsc, Debug) << "no calibrations found";
+	} else if (ct <= calibrations.front().ct) {
+		calTable = calibrations.front().table;
+		LOG(RPiAlsc, Debug) << "using calibration for "
+				    << calibrations.front().ct;
+	} else if (ct >= calibrations.back().ct) {
+		calTable = calibrations.back().table;
+		LOG(RPiAlsc, Debug) << "using calibration for "
+				    << calibrations.back().ct;
+	} else {
+		int idx = 0;
+		while (ct > calibrations[idx + 1].ct)
+			idx++;
+		double ct0 = calibrations[idx].ct, ct1 = calibrations[idx + 1].ct;
+		LOG(RPiAlsc, Debug)
+			<< "ct is " << ct << ", interpolating between "
+			<< ct0 << " and " << ct1;
+		for (unsigned int i = 0; i < calTable.size(); i++)
+			calTable[i] =
+				(calibrations[idx].table[i] * (ct1 - ct) +
+				 calibrations[idx + 1].table[i] * (ct - ct0)) /
+				(ct1 - ct0);
+	}
+}
+
+void resampleCalTable(const Array2D<double> &calTableIn,
+		      CameraMode const &cameraMode,
+		      Array2D<double> &calTableOut)
+{
+	int X = calTableIn.dimensions().width;
+	int Y = calTableIn.dimensions().height;
+
+	/*
+	 * Precalculate and cache the x sampling locations and phases to save
+	 * recomputing them on every row.
+	 */
+	int xLo[X], xHi[X];
+	double xf[X];
+	double scaleX = cameraMode.sensorWidth /
+			(cameraMode.width * cameraMode.scaleX);
+	double xOff = cameraMode.cropX / (double)cameraMode.sensorWidth;
+	double x = .5 / scaleX + xOff * X - .5;
+	double xInc = 1 / scaleX;
+	for (int i = 0; i < X; i++, x += xInc) {
+		xLo[i] = floor(x);
+		xf[i] = x - xLo[i];
+		xHi[i] = std::min(xLo[i] + 1, X - 1);
+		xLo[i] = std::max(xLo[i], 0);
+		if (!!(cameraMode.transform & libcamera::Transform::HFlip)) {
+			xLo[i] = X - 1 - xLo[i];
+			xHi[i] = X - 1 - xHi[i];
+		}
+	}
+	/* Now march over the output table generating the new values. */
+	double scaleY = cameraMode.sensorHeight /
+			(cameraMode.height * cameraMode.scaleY);
+	double yOff = cameraMode.cropY / (double)cameraMode.sensorHeight;
+	double y = .5 / scaleY + yOff * Y - .5;
+	double yInc = 1 / scaleY;
+	for (int j = 0; j < Y; j++, y += yInc) {
+		int yLo = floor(y);
+		double yf = y - yLo;
+		int yHi = std::min(yLo + 1, Y - 1);
+		yLo = std::max(yLo, 0);
+		if (!!(cameraMode.transform & libcamera::Transform::VFlip)) {
+			yLo = Y - 1 - yLo;
+			yHi = Y - 1 - yHi;
+		}
+		double const *rowAbove = calTableIn.ptr() + X * yLo;
+		double const *rowBelow = calTableIn.ptr() + X * yHi;
+		double *out = calTableOut.ptr() + X * j;
+		for (int i = 0; i < X; i++) {
+			double above = rowAbove[xLo[i]] * (1 - xf[i]) +
+				       rowAbove[xHi[i]] * xf[i];
+			double below = rowBelow[xLo[i]] * (1 - xf[i]) +
+				       rowBelow[xHi[i]] * xf[i];
+			*(out++) = above * (1 - yf) + below * yf;
+		}
+	}
+}
+
+/* Calculate chrominance statistics (R/G and B/G) for each region. */
+static void calculateCrCb(const RgbyRegions &awbRegion, Array2D<double> &cr,
+			  Array2D<double> &cb, uint32_t minCount, uint16_t minG)
+{
+	for (unsigned int i = 0; i < cr.size(); i++) {
+		auto s = awbRegion.get(i);
+
+		if (s.counted <= minCount || s.val.gSum / s.counted <= minG) {
+			cr[i] = cb[i] = InsufficientData;
+			continue;
+		}
+
+		cr[i] = s.val.rSum / (double)s.val.gSum;
+		cb[i] = s.val.bSum / (double)s.val.gSum;
+	}
+}
+
+static void applyCalTable(const Array2D<double> &calTable, Array2D<double> &C)
+{
+	for (unsigned int i = 0; i < C.size(); i++)
+		if (C[i] != InsufficientData)
+			C[i] *= calTable[i];
+}
+
+void compensateLambdasForCal(const Array2D<double> &calTable,
+			     const Array2D<double> &oldLambdas,
+			     Array2D<double> &newLambdas)
+{
+	double minNewLambda = std::numeric_limits<double>::max();
+	for (unsigned int i = 0; i < newLambdas.size(); i++) {
+		newLambdas[i] = oldLambdas[i] * calTable[i];
+		minNewLambda = std::min(minNewLambda, newLambdas[i]);
+	}
+	for (unsigned int i = 0; i < newLambdas.size(); i++)
+		newLambdas[i] /= minNewLambda;
+}
+
+[[maybe_unused]] static void printCalTable(const Array2D<double> &C)
+{
+	const Size &size = C.dimensions();
+	printf("table: [\n");
+	for (unsigned int j = 0; j < size.height; j++) {
+		for (unsigned int i = 0; i < size.width; i++) {
+			printf("%5.3f", 1.0 / C[j * size.width + i]);
+			if (i != size.width - 1 || j != size.height - 1)
+				printf(",");
+		}
+		printf("\n");
+	}
+	printf("]\n");
+}
+
+/*
+ * Compute weight out of 1.0 which reflects how similar we wish to make the
+ * colours of these two regions.
+ */
+static double computeWeight(double Ci, double Cj, double sigma)
+{
+	if (Ci == InsufficientData || Cj == InsufficientData)
+		return 0;
+	double diff = (Ci - Cj) / sigma;
+	return exp(-diff * diff / 2);
+}
+
+/* Compute all weights. */
+static void computeW(const Array2D<double> &C, double sigma,
+		     SparseArray<double> &W)
+{
+	size_t XY = C.size();
+	size_t X = C.dimensions().width;
+
+	for (unsigned int i = 0; i < XY; i++) {
+		/* Start with neighbour above and go clockwise. */
+		W[i][0] = i >= X ? computeWeight(C[i], C[i - X], sigma) : 0;
+		W[i][1] = i % X < X - 1 ? computeWeight(C[i], C[i + 1], sigma) : 0;
+		W[i][2] = i < XY - X ? computeWeight(C[i], C[i + X], sigma) : 0;
+		W[i][3] = i % X ? computeWeight(C[i], C[i - 1], sigma) : 0;
+	}
+}
+
+/* Compute M, the large but sparse matrix such that M * lambdas = 0. */
+static void constructM(const Array2D<double> &C,
+		       const SparseArray<double> &W,
+		       SparseArray<double> &M)
+{
+	size_t XY = C.size();
+	size_t X = C.dimensions().width;
+
+	double epsilon = 0.001;
+	for (unsigned int i = 0; i < XY; i++) {
+		/*
+		 * Note how, if C[i] == INSUFFICIENT_DATA, the weights will all
+		 * be zero so the equation is still set up correctly.
+		 */
+		int m = !!(i >= X) + !!(i % X < X - 1) + !!(i < XY - X) +
+			!!(i % X); /* total number of neighbours */
+		/* we'll divide the diagonal out straight away */
+		double diagonal = (epsilon + W[i][0] + W[i][1] + W[i][2] + W[i][3]) * C[i];
+		M[i][0] = i >= X ? (W[i][0] * C[i - X] + epsilon / m * C[i]) / diagonal : 0;
+		M[i][1] = i % X < X - 1 ? (W[i][1] * C[i + 1] + epsilon / m * C[i]) / diagonal : 0;
+		M[i][2] = i < XY - X ? (W[i][2] * C[i + X] + epsilon / m * C[i]) / diagonal : 0;
+		M[i][3] = i % X ? (W[i][3] * C[i - 1] + epsilon / m * C[i]) / diagonal : 0;
+	}
+}
+
+/*
+ * In the compute_lambda_ functions, note that the matrix coefficients for the
+ * left/right neighbours are zero down the left/right edges, so we don't need
+ * need to test the i value to exclude them.
+ */
+static double computeLambdaBottom(int i, const SparseArray<double> &M,
+				  Array2D<double> &lambda)
+{
+	return M[i][1] * lambda[i + 1] + M[i][2] * lambda[i + lambda.dimensions().width] +
+	       M[i][3] * lambda[i - 1];
+}
+static double computeLambdaBottomStart(int i, const SparseArray<double> &M,
+				       Array2D<double> &lambda)
+{
+	return M[i][1] * lambda[i + 1] + M[i][2] * lambda[i + lambda.dimensions().width];
+}
+static double computeLambdaInterior(int i, const SparseArray<double> &M,
+				    Array2D<double> &lambda)
+{
+	return M[i][0] * lambda[i - lambda.dimensions().width] + M[i][1] * lambda[i + 1] +
+	       M[i][2] * lambda[i + lambda.dimensions().width] + M[i][3] * lambda[i - 1];
+}
+static double computeLambdaTop(int i, const SparseArray<double> &M,
+			       Array2D<double> &lambda)
+{
+	return M[i][0] * lambda[i - lambda.dimensions().width] + M[i][1] * lambda[i + 1] +
+	       M[i][3] * lambda[i - 1];
+}
+static double computeLambdaTopEnd(int i, const SparseArray<double> &M,
+				  Array2D<double> &lambda)
+{
+	return M[i][0] * lambda[i - lambda.dimensions().width] + M[i][3] * lambda[i - 1];
+}
+
+/* Gauss-Seidel iteration with over-relaxation. */
+static double gaussSeidel2Sor(const SparseArray<double> &M, double omega,
+			      Array2D<double> &lambda, double lambdaBound)
+{
+	int XY = lambda.size();
+	int X = lambda.dimensions().width;
+	const double min = 1 - lambdaBound, max = 1 + lambdaBound;
+	Array2D<double> oldLambda = lambda;
+	int i;
+	lambda[0] = computeLambdaBottomStart(0, M, lambda);
+	lambda[0] = std::clamp(lambda[0], min, max);
+	for (i = 1; i < X; i++) {
+		lambda[i] = computeLambdaBottom(i, M, lambda);
+		lambda[i] = std::clamp(lambda[i], min, max);
+	}
+	for (; i < XY - X; i++) {
+		lambda[i] = computeLambdaInterior(i, M, lambda);
+		lambda[i] = std::clamp(lambda[i], min, max);
+	}
+	for (; i < XY - 1; i++) {
+		lambda[i] = computeLambdaTop(i, M, lambda);
+		lambda[i] = std::clamp(lambda[i], min, max);
+	}
+	lambda[i] = computeLambdaTopEnd(i, M, lambda);
+	lambda[i] = std::clamp(lambda[i], min, max);
+	/*
+	 * Also solve the system from bottom to top, to help spread the updates
+	 * better.
+	 */
+	lambda[i] = computeLambdaTopEnd(i, M, lambda);
+	lambda[i] = std::clamp(lambda[i], min, max);
+	for (i = XY - 2; i >= XY - X; i--) {
+		lambda[i] = computeLambdaTop(i, M, lambda);
+		lambda[i] = std::clamp(lambda[i], min, max);
+	}
+	for (; i >= X; i--) {
+		lambda[i] = computeLambdaInterior(i, M, lambda);
+		lambda[i] = std::clamp(lambda[i], min, max);
+	}
+	for (; i >= 1; i--) {
+		lambda[i] = computeLambdaBottom(i, M, lambda);
+		lambda[i] = std::clamp(lambda[i], min, max);
+	}
+	lambda[0] = computeLambdaBottomStart(0, M, lambda);
+	lambda[0] = std::clamp(lambda[0], min, max);
+	double maxDiff = 0;
+	for (i = 0; i < XY; i++) {
+		lambda[i] = oldLambda[i] + (lambda[i] - oldLambda[i]) * omega;
+		if (fabs(lambda[i] - oldLambda[i]) > fabs(maxDiff))
+			maxDiff = lambda[i] - oldLambda[i];
+	}
+	return maxDiff;
+}
+
+/* Normalise the values so that the smallest value is 1. */
+static void normalise(Array2D<double> &results)
+{
+	double minval = *std::min_element(results.begin(), results.end());
+	std::for_each(results.begin(), results.end(),
+		      [minval](double val) { return val / minval; });
+}
+
+/* Rescale the values so that the average value is 1. */
+static void reaverage(Array2D<double> &data)
+{
+	double sum = std::accumulate(data.begin(), data.end(), 0.0);
+	double ratio = 1 / (sum / data.size());
+	std::for_each(data.begin(), data.end(),
+		      [ratio](double val) { return val * ratio; });
+}
+
+static void runMatrixIterations(const Array2D<double> &C,
+				Array2D<double> &lambda,
+				const SparseArray<double> &W,
+				SparseArray<double> &M, double omega,
+				unsigned int nIter, double threshold, double lambdaBound)
+{
+	constructM(C, W, M);
+	double lastMaxDiff = std::numeric_limits<double>::max();
+	for (unsigned int i = 0; i < nIter; i++) {
+		double maxDiff = fabs(gaussSeidel2Sor(M, omega, lambda, lambdaBound));
+		if (maxDiff < threshold) {
+			LOG(RPiAlsc, Debug)
+				<< "Stop after " << i + 1 << " iterations";
+			break;
+		}
+		/*
+		 * this happens very occasionally (so make a note), though
+		 * doesn't seem to matter
+		 */
+		if (maxDiff > lastMaxDiff)
+			LOG(RPiAlsc, Debug)
+				<< "Iteration " << i << ": maxDiff gone up "
+				<< lastMaxDiff << " to " << maxDiff;
+		lastMaxDiff = maxDiff;
+	}
+	/* We're going to normalise the lambdas so the total average is 1. */
+	reaverage(lambda);
+}
+
+static void addLuminanceRb(Array2D<double> &result, const Array2D<double> &lambda,
+			   const Array2D<double> &luminanceLut,
+			   double luminanceStrength)
+{
+	for (unsigned int i = 0; i < result.size(); i++)
+		result[i] = lambda[i] * ((luminanceLut[i] - 1) * luminanceStrength + 1);
+}
+
+static void addLuminanceG(Array2D<double> &result, double lambda,
+			  const Array2D<double> &luminanceLut,
+			  double luminanceStrength)
+{
+	for (unsigned int i = 0; i < result.size(); i++)
+		result[i] = lambda * ((luminanceLut[i] - 1) * luminanceStrength + 1);
+}
+
+void addLuminanceToTables(std::array<Array2D<double>, 3> &results,
+			  const Array2D<double> &lambdaR,
+			  double lambdaG, const Array2D<double> &lambdaB,
+			  const Array2D<double> &luminanceLut,
+			  double luminanceStrength)
+{
+	addLuminanceRb(results[0], lambdaR, luminanceLut, luminanceStrength);
+	addLuminanceG(results[1], lambdaG, luminanceLut, luminanceStrength);
+	addLuminanceRb(results[2], lambdaB, luminanceLut, luminanceStrength);
+	for (auto &r : results)
+		normalise(r);
+}
+
+void Alsc::doAlsc()
+{
+	Array2D<double> &cr = tmpC_[0], &cb = tmpC_[1], &calTableR = tmpC_[2],
+			&calTableB = tmpC_[3], &calTableTmp = tmpC_[4];
+	SparseArray<double> &wr = tmpM_[0], &wb = tmpM_[1], &M = tmpM_[2];
+
+	/*
+	 * Calculate our R/B ("Cr"/"Cb") colour statistics, and assess which are
+	 * usable.
+	 */
+	calculateCrCb(statistics_, cr, cb, config_.minCount, config_.minG);
+	/*
+	 * Fetch the new calibrations (if any) for this CT. Resample them in
+	 * case the camera mode is not full-frame.
+	 */
+	getCalTable(ct_, config_.calibrationsCr, calTableTmp);
+	resampleCalTable(calTableTmp, cameraMode_, calTableR);
+	getCalTable(ct_, config_.calibrationsCb, calTableTmp);
+	resampleCalTable(calTableTmp, cameraMode_, calTableB);
+	/*
+	 * You could print out the cal tables for this image here, if you're
+	 * tuning the algorithm...
+	 * Apply any calibration to the statistics, so the adaptive algorithm
+	 * makes only the extra adjustments.
+	 */
+	applyCalTable(calTableR, cr);
+	applyCalTable(calTableB, cb);
+	/* Compute weights between zones. */
+	computeW(cr, config_.sigmaCr, wr);
+	computeW(cb, config_.sigmaCb, wb);
+	/* Run Gauss-Seidel iterations over the resulting matrix, for R and B. */
+	runMatrixIterations(cr, lambdaR_, wr, M, config_.omega, config_.nIter,
+			    config_.threshold, config_.lambdaBound);
+	runMatrixIterations(cb, lambdaB_, wb, M, config_.omega, config_.nIter,
+			    config_.threshold, config_.lambdaBound);
+	/*
+	 * Fold the calibrated gains into our final lambda values. (Note that on
+	 * the next run, we re-start with the lambda values that don't have the
+	 * calibration gains included.)
+	 */
+	compensateLambdasForCal(calTableR, lambdaR_, asyncLambdaR_);
+	compensateLambdasForCal(calTableB, lambdaB_, asyncLambdaB_);
+	/* Fold in the luminance table at the appropriate strength. */
+	addLuminanceToTables(asyncResults_, asyncLambdaR_, 1.0,
+			     asyncLambdaB_, luminanceTable_,
+			     config_.luminanceStrength);
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Alsc(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/alsc.h b/src/ipa/rpi/controller/rpi/alsc.h
new file mode 100644
index 00000000..0b6d9478
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/alsc.h
@@ -0,0 +1,174 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * alsc.h - ALSC (auto lens shading correction) control algorithm
+ */
+#pragma once
+
+#include <array>
+#include <mutex>
+#include <condition_variable>
+#include <thread>
+#include <vector>
+
+#include <libcamera/geometry.h>
+
+#include "../algorithm.h"
+#include "../alsc_status.h"
+#include "../statistics.h"
+
+namespace RPiController {
+
+/* Algorithm to generate automagic LSC (Lens Shading Correction) tables. */
+
+/*
+ * The Array2D class is a very thin wrapper round std::vector so that it can
+ * be used in exactly the same way in the code but carries its correct width
+ * and height ("dimensions") with it.
+ */
+
+template<typename T>
+class Array2D
+{
+public:
+	using Size = libcamera::Size;
+
+	const Size &dimensions() const { return dimensions_; }
+
+	size_t size() const { return data_.size(); }
+
+	const std::vector<T> &data() const { return data_; }
+
+	void resize(const Size &dims)
+	{
+		dimensions_ = dims;
+		data_.resize(dims.width * dims.height);
+	}
+
+	void resize(const Size &dims, const T &value)
+	{
+		resize(dims);
+		std::fill(data_.begin(), data_.end(), value);
+	}
+
+	T &operator[](int index) { return data_[index]; }
+
+	const T &operator[](int index) const { return data_[index]; }
+
+	T *ptr() { return data_.data(); }
+
+	const T *ptr() const { return data_.data(); }
+
+	auto begin() { return data_.begin(); }
+	auto end() { return data_.end(); }
+
+private:
+	Size dimensions_;
+	std::vector<T> data_;
+};
+
+/*
+ * We'll use the term SparseArray for the large sparse matrices that are
+ * XY tall but have only 4 non-zero elements on each row.
+ */
+
+template<typename T>
+using SparseArray = std::vector<std::array<T, 4>>;
+
+struct AlscCalibration {
+	double ct;
+	Array2D<double> table;
+};
+
+struct AlscConfig {
+	/* Only repeat the ALSC calculation every "this many" frames */
+	uint16_t framePeriod;
+	/* number of initial frames for which speed taken as 1.0 (maximum) */
+	uint16_t startupFrames;
+	/* IIR filter speed applied to algorithm results */
+	double speed;
+	double sigmaCr;
+	double sigmaCb;
+	double minCount;
+	uint16_t minG;
+	double omega;
+	uint32_t nIter;
+	Array2D<double> luminanceLut;
+	double luminanceStrength;
+	std::vector<AlscCalibration> calibrationsCr;
+	std::vector<AlscCalibration> calibrationsCb;
+	double defaultCt; /* colour temperature if no metadata found */
+	double threshold; /* iteration termination threshold */
+	double lambdaBound; /* upper/lower bound for lambda from a value of 1 */
+	libcamera::Size tableSize;
+};
+
+class Alsc : public Algorithm
+{
+public:
+	Alsc(Controller *controller = NULL);
+	~Alsc();
+	char const *name() const override;
+	void initialise() override;
+	void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;
+	int read(const libcamera::YamlObject &params) override;
+	void prepare(Metadata *imageMetadata) override;
+	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
+
+private:
+	/* configuration is read-only, and available to both threads */
+	AlscConfig config_;
+	bool firstTime_;
+	CameraMode cameraMode_;
+	Array2D<double> luminanceTable_;
+	std::thread asyncThread_;
+	void asyncFunc(); /* asynchronous thread function */
+	std::mutex mutex_;
+	/* condvar for async thread to wait on */
+	std::condition_variable asyncSignal_;
+	/* condvar for synchronous thread to wait on */
+	std::condition_variable syncSignal_;
+	/* for sync thread to check  if async thread finished (requires mutex) */
+	bool asyncFinished_;
+	/* for async thread to check if it's been told to run (requires mutex) */
+	bool asyncStart_;
+	/* for async thread to check if it's been told to quit (requires mutex) */
+	bool asyncAbort_;
+
+	/*
+	 * The following are only for the synchronous thread to use:
+	 * for sync thread to note its has asked async thread to run
+	 */
+	bool asyncStarted_;
+	/* counts up to framePeriod before restarting the async thread */
+	int framePhase_;
+	/* counts up to startupFrames */
+	int frameCount_;
+	/* counts up to startupFrames for Process function */
+	int frameCount2_;
+	std::array<Array2D<double>, 3> syncResults_;
+	std::array<Array2D<double>, 3> prevSyncResults_;
+	void waitForAysncThread();
+	/*
+	 * The following are for the asynchronous thread to use, though the main
+	 * thread can set/reset them if the async thread is known to be idle:
+	 */
+	void restartAsync(StatisticsPtr &stats, Metadata *imageMetadata);
+	/* copy out the results from the async thread so that it can be restarted */
+	void fetchAsyncResults();
+	double ct_;
+	RgbyRegions statistics_;
+	std::array<Array2D<double>, 3> asyncResults_;
+	Array2D<double> asyncLambdaR_;
+	Array2D<double> asyncLambdaB_;
+	void doAlsc();
+	Array2D<double> lambdaR_;
+	Array2D<double> lambdaB_;
+
+	/* Temporaries for the computations */
+	std::array<Array2D<double>, 5> tmpC_;
+	std::array<SparseArray<double>, 3> tmpM_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/awb.cpp b/src/ipa/rpi/controller/rpi/awb.cpp
new file mode 100644
index 00000000..ef3435d6
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/awb.cpp
@@ -0,0 +1,734 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * awb.cpp - AWB control algorithm
+ */
+
+#include <assert.h>
+#include <functional>
+
+#include <libcamera/base/log.h>
+
+#include "../lux_status.h"
+
+#include "awb.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiAwb)
+
+#define NAME "rpi.awb"
+
+/*
+ * todo - the locking in this algorithm needs some tidying up as has been done
+ * elsewhere (ALSC and AGC).
+ */
+
+int AwbMode::read(const libcamera::YamlObject &params)
+{
+	auto value = params["lo"].get<double>();
+	if (!value)
+		return -EINVAL;
+	ctLo = *value;
+
+	value = params["hi"].get<double>();
+	if (!value)
+		return -EINVAL;
+	ctHi = *value;
+
+	return 0;
+}
+
+int AwbPrior::read(const libcamera::YamlObject &params)
+{
+	auto value = params["lux"].get<double>();
+	if (!value)
+		return -EINVAL;
+	lux = *value;
+
+	return prior.read(params["prior"]);
+}
+
+static int readCtCurve(Pwl &ctR, Pwl &ctB, const libcamera::YamlObject &params)
+{
+	if (params.size() % 3) {
+		LOG(RPiAwb, Error) << "AwbConfig: incomplete CT curve entry";
+		return -EINVAL;
+	}
+
+	if (params.size() < 6) {
+		LOG(RPiAwb, Error) << "AwbConfig: insufficient points in CT curve";
+		return -EINVAL;
+	}
+
+	const auto &list = params.asList();
+
+	for (auto it = list.begin(); it != list.end(); it++) {
+		auto value = it->get<double>();
+		if (!value)
+			return -EINVAL;
+		double ct = *value;
+
+		assert(it == list.begin() || ct != ctR.domain().end);
+
+		value = (++it)->get<double>();
+		if (!value)
+			return -EINVAL;
+		ctR.append(ct, *value);
+
+		value = (++it)->get<double>();
+		if (!value)
+			return -EINVAL;
+		ctB.append(ct, *value);
+	}
+
+	return 0;
+}
+
+int AwbConfig::read(const libcamera::YamlObject &params)
+{
+	int ret;
+
+	bayes = params["bayes"].get<int>(1);
+	framePeriod = params["frame_period"].get<uint16_t>(10);
+	startupFrames = params["startup_frames"].get<uint16_t>(10);
+	convergenceFrames = params["convergence_frames"].get<unsigned int>(3);
+	speed = params["speed"].get<double>(0.05);
+
+	if (params.contains("ct_curve")) {
+		ret = readCtCurve(ctR, ctB, params["ct_curve"]);
+		if (ret)
+			return ret;
+		/* We will want the inverse functions of these too. */
+		ctRInverse = ctR.inverse();
+		ctBInverse = ctB.inverse();
+	}
+
+	if (params.contains("priors")) {
+		for (const auto &p : params["priors"].asList()) {
+			AwbPrior prior;
+			ret = prior.read(p);
+			if (ret)
+				return ret;
+			if (!priors.empty() && prior.lux <= priors.back().lux) {
+				LOG(RPiAwb, Error) << "AwbConfig: Prior must be ordered in increasing lux value";
+				return -EINVAL;
+			}
+			priors.push_back(prior);
+		}
+		if (priors.empty()) {
+			LOG(RPiAwb, Error) << "AwbConfig: no AWB priors configured";
+			return ret;
+		}
+	}
+	if (params.contains("modes")) {
+		for (const auto &[key, value] : params["modes"].asDict()) {
+			ret = modes[key].read(value);
+			if (ret)
+				return ret;
+			if (defaultMode == nullptr)
+				defaultMode = &modes[key];
+		}
+		if (defaultMode == nullptr) {
+			LOG(RPiAwb, Error) << "AwbConfig: no AWB modes configured";
+			return -EINVAL;
+		}
+	}
+
+	minPixels = params["min_pixels"].get<double>(16.0);
+	minG = params["min_G"].get<uint16_t>(32);
+	minRegions = params["min_regions"].get<uint32_t>(10);
+	deltaLimit = params["delta_limit"].get<double>(0.2);
+	coarseStep = params["coarse_step"].get<double>(0.2);
+	transversePos = params["transverse_pos"].get<double>(0.01);
+	transverseNeg = params["transverse_neg"].get<double>(0.01);
+	if (transversePos <= 0 || transverseNeg <= 0) {
+		LOG(RPiAwb, Error) << "AwbConfig: transverse_pos/neg must be > 0";
+		return -EINVAL;
+	}
+
+	sensitivityR = params["sensitivity_r"].get<double>(1.0);
+	sensitivityB = params["sensitivity_b"].get<double>(1.0);
+
+	if (bayes) {
+		if (ctR.empty() || ctB.empty() || priors.empty() ||
+		    defaultMode == nullptr) {
+			LOG(RPiAwb, Warning)
+				<< "Bayesian AWB mis-configured - switch to Grey method";
+			bayes = false;
+		}
+	}
+	fast = params[fast].get<int>(bayes); /* default to fast for Bayesian, otherwise slow */
+	whitepointR = params["whitepoint_r"].get<double>(0.0);
+	whitepointB = params["whitepoint_b"].get<double>(0.0);
+	if (bayes == false)
+		sensitivityR = sensitivityB = 1.0; /* nor do sensitivities make any sense */
+	return 0;
+}
+
+Awb::Awb(Controller *controller)
+	: AwbAlgorithm(controller)
+{
+	asyncAbort_ = asyncStart_ = asyncStarted_ = asyncFinished_ = false;
+	mode_ = nullptr;
+	manualR_ = manualB_ = 0.0;
+	asyncThread_ = std::thread(std::bind(&Awb::asyncFunc, this));
+}
+
+Awb::~Awb()
+{
+	{
+		std::lock_guard<std::mutex> lock(mutex_);
+		asyncAbort_ = true;
+	}
+	asyncSignal_.notify_one();
+	asyncThread_.join();
+}
+
+char const *Awb::name() const
+{
+	return NAME;
+}
+
+int Awb::read(const libcamera::YamlObject &params)
+{
+	return config_.read(params);
+}
+
+void Awb::initialise()
+{
+	frameCount_ = framePhase_ = 0;
+	/*
+	 * Put something sane into the status that we are filtering towards,
+	 * just in case the first few frames don't have anything meaningful in
+	 * them.
+	 */
+	if (!config_.ctR.empty() && !config_.ctB.empty()) {
+		syncResults_.temperatureK = config_.ctR.domain().clip(4000);
+		syncResults_.gainR = 1.0 / config_.ctR.eval(syncResults_.temperatureK);
+		syncResults_.gainG = 1.0;
+		syncResults_.gainB = 1.0 / config_.ctB.eval(syncResults_.temperatureK);
+	} else {
+		/* random values just to stop the world blowing up */
+		syncResults_.temperatureK = 4500;
+		syncResults_.gainR = syncResults_.gainG = syncResults_.gainB = 1.0;
+	}
+	prevSyncResults_ = syncResults_;
+	asyncResults_ = syncResults_;
+}
+
+void Awb::disableAuto()
+{
+	/* Freeze the most recent values, and treat them as manual gains */
+	manualR_ = syncResults_.gainR = prevSyncResults_.gainR;
+	manualB_ = syncResults_.gainB = prevSyncResults_.gainB;
+	syncResults_.gainG = prevSyncResults_.gainG;
+	syncResults_.temperatureK = prevSyncResults_.temperatureK;
+}
+
+void Awb::enableAuto()
+{
+	manualR_ = 0.0;
+	manualB_ = 0.0;
+}
+
+unsigned int Awb::getConvergenceFrames() const
+{
+	/*
+	 * If not in auto mode, there is no convergence
+	 * to happen, so no need to drop any frames - return zero.
+	 */
+	if (!isAutoEnabled())
+		return 0;
+	else
+		return config_.convergenceFrames;
+}
+
+void Awb::setMode(std::string const &modeName)
+{
+	modeName_ = modeName;
+}
+
+void Awb::setManualGains(double manualR, double manualB)
+{
+	/* If any of these are 0.0, we swich back to auto. */
+	manualR_ = manualR;
+	manualB_ = manualB;
+	/*
+	 * If not in auto mode, set these values into the syncResults which
+	 * means that Prepare() will adopt them immediately.
+	 */
+	if (!isAutoEnabled()) {
+		syncResults_.gainR = prevSyncResults_.gainR = manualR_;
+		syncResults_.gainG = prevSyncResults_.gainG = 1.0;
+		syncResults_.gainB = prevSyncResults_.gainB = manualB_;
+		if (config_.bayes) {
+			/* Also estimate the best corresponding colour temperature from the curves. */
+			double ctR = config_.ctRInverse.eval(config_.ctRInverse.domain().clip(1 / manualR_));
+			double ctB = config_.ctBInverse.eval(config_.ctBInverse.domain().clip(1 / manualB_));
+			prevSyncResults_.temperatureK = (ctR + ctB) / 2;
+			syncResults_.temperatureK = prevSyncResults_.temperatureK;
+		}
+	}
+}
+
+void Awb::switchMode([[maybe_unused]] CameraMode const &cameraMode,
+		     Metadata *metadata)
+{
+	/* Let other algorithms know the current white balance values. */
+	metadata->set("awb.status", prevSyncResults_);
+}
+
+bool Awb::isAutoEnabled() const
+{
+	return manualR_ == 0.0 || manualB_ == 0.0;
+}
+
+void Awb::fetchAsyncResults()
+{
+	LOG(RPiAwb, Debug) << "Fetch AWB results";
+	asyncFinished_ = false;
+	asyncStarted_ = false;
+	/*
+	 * It's possible manual gains could be set even while the async
+	 * thread was running, so only copy the results if still in auto mode.
+	 */
+	if (isAutoEnabled())
+		syncResults_ = asyncResults_;
+}
+
+void Awb::restartAsync(StatisticsPtr &stats, double lux)
+{
+	LOG(RPiAwb, Debug) << "Starting AWB calculation";
+	/* this makes a new reference which belongs to the asynchronous thread */
+	statistics_ = stats;
+	/* store the mode as it could technically change */
+	auto m = config_.modes.find(modeName_);
+	mode_ = m != config_.modes.end()
+			? &m->second
+			: (mode_ == nullptr ? config_.defaultMode : mode_);
+	lux_ = lux;
+	framePhase_ = 0;
+	asyncStarted_ = true;
+	size_t len = modeName_.copy(asyncResults_.mode,
+				    sizeof(asyncResults_.mode) - 1);
+	asyncResults_.mode[len] = '\0';
+	{
+		std::lock_guard<std::mutex> lock(mutex_);
+		asyncStart_ = true;
+	}
+	asyncSignal_.notify_one();
+}
+
+void Awb::prepare(Metadata *imageMetadata)
+{
+	if (frameCount_ < (int)config_.startupFrames)
+		frameCount_++;
+	double speed = frameCount_ < (int)config_.startupFrames
+			       ? 1.0
+			       : config_.speed;
+	LOG(RPiAwb, Debug)
+		<< "frame_count " << frameCount_ << " speed " << speed;
+	{
+		std::unique_lock<std::mutex> lock(mutex_);
+		if (asyncStarted_ && asyncFinished_)
+			fetchAsyncResults();
+	}
+	/* Finally apply IIR filter to results and put into metadata. */
+	memcpy(prevSyncResults_.mode, syncResults_.mode,
+	       sizeof(prevSyncResults_.mode));
+	prevSyncResults_.temperatureK = speed * syncResults_.temperatureK +
+					(1.0 - speed) * prevSyncResults_.temperatureK;
+	prevSyncResults_.gainR = speed * syncResults_.gainR +
+				 (1.0 - speed) * prevSyncResults_.gainR;
+	prevSyncResults_.gainG = speed * syncResults_.gainG +
+				 (1.0 - speed) * prevSyncResults_.gainG;
+	prevSyncResults_.gainB = speed * syncResults_.gainB +
+				 (1.0 - speed) * prevSyncResults_.gainB;
+	imageMetadata->set("awb.status", prevSyncResults_);
+	LOG(RPiAwb, Debug)
+		<< "Using AWB gains r " << prevSyncResults_.gainR << " g "
+		<< prevSyncResults_.gainG << " b "
+		<< prevSyncResults_.gainB;
+}
+
+void Awb::process(StatisticsPtr &stats, Metadata *imageMetadata)
+{
+	/* Count frames since we last poked the async thread. */
+	if (framePhase_ < (int)config_.framePeriod)
+		framePhase_++;
+	LOG(RPiAwb, Debug) << "frame_phase " << framePhase_;
+	/* We do not restart the async thread if we're not in auto mode. */
+	if (isAutoEnabled() &&
+	    (framePhase_ >= (int)config_.framePeriod ||
+	     frameCount_ < (int)config_.startupFrames)) {
+		/* Update any settings and any image metadata that we need. */
+		struct LuxStatus luxStatus = {};
+		luxStatus.lux = 400; /* in case no metadata */
+		if (imageMetadata->get("lux.status", luxStatus) != 0)
+			LOG(RPiAwb, Debug) << "No lux metadata found";
+		LOG(RPiAwb, Debug) << "Awb lux value is " << luxStatus.lux;
+
+		if (asyncStarted_ == false)
+			restartAsync(stats, luxStatus.lux);
+	}
+}
+
+void Awb::asyncFunc()
+{
+	while (true) {
+		{
+			std::unique_lock<std::mutex> lock(mutex_);
+			asyncSignal_.wait(lock, [&] {
+				return asyncStart_ || asyncAbort_;
+			});
+			asyncStart_ = false;
+			if (asyncAbort_)
+				break;
+		}
+		doAwb();
+		{
+			std::lock_guard<std::mutex> lock(mutex_);
+			asyncFinished_ = true;
+		}
+		syncSignal_.notify_one();
+	}
+}
+
+static void generateStats(std::vector<Awb::RGB> &zones,
+			  RgbyRegions &stats, double minPixels,
+			  double minG)
+{
+	for (auto const &region : stats) {
+		Awb::RGB zone;
+		if (region.counted >= minPixels) {
+			zone.G = region.val.gSum / region.counted;
+			if (zone.G >= minG) {
+				zone.R = region.val.rSum / region.counted;
+				zone.B = region.val.bSum / region.counted;
+				zones.push_back(zone);
+			}
+		}
+	}
+}
+
+void Awb::prepareStats()
+{
+	zones_.clear();
+	/*
+	 * LSC has already been applied to the stats in this pipeline, so stop
+	 * any LSC compensation.  We also ignore config_.fast in this version.
+	 */
+	generateStats(zones_, statistics_->awbRegions, config_.minPixels,
+		      config_.minG);
+	/*
+	 * apply sensitivities, so values appear to come from our "canonical"
+	 * sensor.
+	 */
+	for (auto &zone : zones_) {
+		zone.R *= config_.sensitivityR;
+		zone.B *= config_.sensitivityB;
+	}
+}
+
+double Awb::computeDelta2Sum(double gainR, double gainB)
+{
+	/*
+	 * Compute the sum of the squared colour error (non-greyness) as it
+	 * appears in the log likelihood equation.
+	 */
+	double delta2Sum = 0;
+	for (auto &z : zones_) {
+		double deltaR = gainR * z.R - 1 - config_.whitepointR;
+		double deltaB = gainB * z.B - 1 - config_.whitepointB;
+		double delta2 = deltaR * deltaR + deltaB * deltaB;
+		/* LOG(RPiAwb, Debug) << "deltaR " << deltaR << " deltaB " << deltaB << " delta2 " << delta2; */
+		delta2 = std::min(delta2, config_.deltaLimit);
+		delta2Sum += delta2;
+	}
+	return delta2Sum;
+}
+
+Pwl Awb::interpolatePrior()
+{
+	/*
+	 * Interpolate the prior log likelihood function for our current lux
+	 * value.
+	 */
+	if (lux_ <= config_.priors.front().lux)
+		return config_.priors.front().prior;
+	else if (lux_ >= config_.priors.back().lux)
+		return config_.priors.back().prior;
+	else {
+		int idx = 0;
+		/* find which two we lie between */
+		while (config_.priors[idx + 1].lux < lux_)
+			idx++;
+		double lux0 = config_.priors[idx].lux,
+		       lux1 = config_.priors[idx + 1].lux;
+		return Pwl::combine(config_.priors[idx].prior,
+				    config_.priors[idx + 1].prior,
+				    [&](double /*x*/, double y0, double y1) {
+					    return y0 + (y1 - y0) *
+							(lux_ - lux0) / (lux1 - lux0);
+				    });
+	}
+}
+
+static double interpolateQuadatric(Pwl::Point const &a, Pwl::Point const &b,
+				   Pwl::Point const &c)
+{
+	/*
+	 * Given 3 points on a curve, find the extremum of the function in that
+	 * interval by fitting a quadratic.
+	 */
+	const double eps = 1e-3;
+	Pwl::Point ca = c - a, ba = b - a;
+	double denominator = 2 * (ba.y * ca.x - ca.y * ba.x);
+	if (abs(denominator) > eps) {
+		double numerator = ba.y * ca.x * ca.x - ca.y * ba.x * ba.x;
+		double result = numerator / denominator + a.x;
+		return std::max(a.x, std::min(c.x, result));
+	}
+	/* has degenerated to straight line segment */
+	return a.y < c.y - eps ? a.x : (c.y < a.y - eps ? c.x : b.x);
+}
+
+double Awb::coarseSearch(Pwl const &prior)
+{
+	points_.clear(); /* assume doesn't deallocate memory */
+	size_t bestPoint = 0;
+	double t = mode_->ctLo;
+	int spanR = 0, spanB = 0;
+	/* Step down the CT curve evaluating log likelihood. */
+	while (true) {
+		double r = config_.ctR.eval(t, &spanR);
+		double b = config_.ctB.eval(t, &spanB);
+		double gainR = 1 / r, gainB = 1 / b;
+		double delta2Sum = computeDelta2Sum(gainR, gainB);
+		double priorLogLikelihood = prior.eval(prior.domain().clip(t));
+		double finalLogLikelihood = delta2Sum - priorLogLikelihood;
+		LOG(RPiAwb, Debug)
+			<< "t: " << t << " gain R " << gainR << " gain B "
+			<< gainB << " delta2_sum " << delta2Sum
+			<< " prior " << priorLogLikelihood << " final "
+			<< finalLogLikelihood;
+		points_.push_back(Pwl::Point(t, finalLogLikelihood));
+		if (points_.back().y < points_[bestPoint].y)
+			bestPoint = points_.size() - 1;
+		if (t == mode_->ctHi)
+			break;
+		/* for even steps along the r/b curve scale them by the current t */
+		t = std::min(t + t / 10 * config_.coarseStep, mode_->ctHi);
+	}
+	t = points_[bestPoint].x;
+	LOG(RPiAwb, Debug) << "Coarse search found CT " << t;
+	/*
+	 * We have the best point of the search, but refine it with a quadratic
+	 * interpolation around its neighbours.
+	 */
+	if (points_.size() > 2) {
+		unsigned long bp = std::min(bestPoint, points_.size() - 2);
+		bestPoint = std::max(1UL, bp);
+		t = interpolateQuadatric(points_[bestPoint - 1],
+					 points_[bestPoint],
+					 points_[bestPoint + 1]);
+		LOG(RPiAwb, Debug)
+			<< "After quadratic refinement, coarse search has CT "
+			<< t;
+	}
+	return t;
+}
+
+void Awb::fineSearch(double &t, double &r, double &b, Pwl const &prior)
+{
+	int spanR = -1, spanB = -1;
+	config_.ctR.eval(t, &spanR);
+	config_.ctB.eval(t, &spanB);
+	double step = t / 10 * config_.coarseStep * 0.1;
+	int nsteps = 5;
+	double rDiff = config_.ctR.eval(t + nsteps * step, &spanR) -
+		       config_.ctR.eval(t - nsteps * step, &spanR);
+	double bDiff = config_.ctB.eval(t + nsteps * step, &spanB) -
+		       config_.ctB.eval(t - nsteps * step, &spanB);
+	Pwl::Point transverse(bDiff, -rDiff);
+	if (transverse.len2() < 1e-6)
+		return;
+	/*
+	 * unit vector orthogonal to the b vs. r function (pointing outwards
+	 * with r and b increasing)
+	 */
+	transverse = transverse / transverse.len();
+	double bestLogLikelihood = 0, bestT = 0, bestR = 0, bestB = 0;
+	double transverseRange = config_.transverseNeg + config_.transversePos;
+	const int maxNumDeltas = 12;
+	/* a transverse step approximately every 0.01 r/b units */
+	int numDeltas = floor(transverseRange * 100 + 0.5) + 1;
+	numDeltas = numDeltas < 3 ? 3 : (numDeltas > maxNumDeltas ? maxNumDeltas : numDeltas);
+	/*
+	 * Step down CT curve. March a bit further if the transverse range is
+	 * large.
+	 */
+	nsteps += numDeltas;
+	for (int i = -nsteps; i <= nsteps; i++) {
+		double tTest = t + i * step;
+		double priorLogLikelihood =
+			prior.eval(prior.domain().clip(tTest));
+		double rCurve = config_.ctR.eval(tTest, &spanR);
+		double bCurve = config_.ctB.eval(tTest, &spanB);
+		/* x will be distance off the curve, y the log likelihood there */
+		Pwl::Point points[maxNumDeltas];
+		int bestPoint = 0;
+		/* Take some measurements transversely *off* the CT curve. */
+		for (int j = 0; j < numDeltas; j++) {
+			points[j].x = -config_.transverseNeg +
+				      (transverseRange * j) / (numDeltas - 1);
+			Pwl::Point rbTest = Pwl::Point(rCurve, bCurve) +
+					    transverse * points[j].x;
+			double rTest = rbTest.x, bTest = rbTest.y;
+			double gainR = 1 / rTest, gainB = 1 / bTest;
+			double delta2Sum = computeDelta2Sum(gainR, gainB);
+			points[j].y = delta2Sum - priorLogLikelihood;
+			LOG(RPiAwb, Debug)
+				<< "At t " << tTest << " r " << rTest << " b "
+				<< bTest << ": " << points[j].y;
+			if (points[j].y < points[bestPoint].y)
+				bestPoint = j;
+		}
+		/*
+		 * We have NUM_DELTAS points transversely across the CT curve,
+		 * now let's do a quadratic interpolation for the best result.
+		 */
+		bestPoint = std::max(1, std::min(bestPoint, numDeltas - 2));
+		Pwl::Point rbTest = Pwl::Point(rCurve, bCurve) +
+					transverse * interpolateQuadatric(points[bestPoint - 1],
+									points[bestPoint],
+									points[bestPoint + 1]);
+		double rTest = rbTest.x, bTest = rbTest.y;
+		double gainR = 1 / rTest, gainB = 1 / bTest;
+		double delta2Sum = computeDelta2Sum(gainR, gainB);
+		double finalLogLikelihood = delta2Sum - priorLogLikelihood;
+		LOG(RPiAwb, Debug)
+			<< "Finally "
+			<< tTest << " r " << rTest << " b " << bTest << ": "
+			<< finalLogLikelihood
+			<< (finalLogLikelihood < bestLogLikelihood ? " BEST" : "");
+		if (bestT == 0 || finalLogLikelihood < bestLogLikelihood)
+			bestLogLikelihood = finalLogLikelihood,
+			bestT = tTest, bestR = rTest, bestB = bTest;
+	}
+	t = bestT, r = bestR, b = bestB;
+	LOG(RPiAwb, Debug)
+		<< "Fine search found t " << t << " r " << r << " b " << b;
+}
+
+void Awb::awbBayes()
+{
+	/*
+	 * May as well divide out G to save computeDelta2Sum from doing it over
+	 * and over.
+	 */
+	for (auto &z : zones_)
+		z.R = z.R / (z.G + 1), z.B = z.B / (z.G + 1);
+	/*
+	 * Get the current prior, and scale according to how many zones are
+	 * valid... not entirely sure about this.
+	 */
+	Pwl prior = interpolatePrior();
+	prior *= zones_.size() / (double)(statistics_->awbRegions.numRegions());
+	prior.map([](double x, double y) {
+		LOG(RPiAwb, Debug) << "(" << x << "," << y << ")";
+	});
+	double t = coarseSearch(prior);
+	double r = config_.ctR.eval(t);
+	double b = config_.ctB.eval(t);
+	LOG(RPiAwb, Debug)
+		<< "After coarse search: r " << r << " b " << b << " (gains r "
+		<< 1 / r << " b " << 1 / b << ")";
+	/*
+	 * Not entirely sure how to handle the fine search yet. Mostly the
+	 * estimated CT is already good enough, but the fine search allows us to
+	 * wander transverely off the CT curve. Under some illuminants, where
+	 * there may be more or less green light, this may prove beneficial,
+	 * though I probably need more real datasets before deciding exactly how
+	 * this should be controlled and tuned.
+	 */
+	fineSearch(t, r, b, prior);
+	LOG(RPiAwb, Debug)
+		<< "After fine search: r " << r << " b " << b << " (gains r "
+		<< 1 / r << " b " << 1 / b << ")";
+	/*
+	 * Write results out for the main thread to pick up. Remember to adjust
+	 * the gains from the ones that the "canonical sensor" would require to
+	 * the ones needed by *this* sensor.
+	 */
+	asyncResults_.temperatureK = t;
+	asyncResults_.gainR = 1.0 / r * config_.sensitivityR;
+	asyncResults_.gainG = 1.0;
+	asyncResults_.gainB = 1.0 / b * config_.sensitivityB;
+}
+
+void Awb::awbGrey()
+{
+	LOG(RPiAwb, Debug) << "Grey world AWB";
+	/*
+	 * Make a separate list of the derivatives for each of red and blue, so
+	 * that we can sort them to exclude the extreme gains.  We could
+	 * consider some variations, such as normalising all the zones first, or
+	 * doing an L2 average etc.
+	 */
+	std::vector<RGB> &derivsR(zones_);
+	std::vector<RGB> derivsB(derivsR);
+	std::sort(derivsR.begin(), derivsR.end(),
+		  [](RGB const &a, RGB const &b) {
+			  return a.G * b.R < b.G * a.R;
+		  });
+	std::sort(derivsB.begin(), derivsB.end(),
+		  [](RGB const &a, RGB const &b) {
+			  return a.G * b.B < b.G * a.B;
+		  });
+	/* Average the middle half of the values. */
+	int discard = derivsR.size() / 4;
+	RGB sumR(0, 0, 0), sumB(0, 0, 0);
+	for (auto ri = derivsR.begin() + discard,
+		  bi = derivsB.begin() + discard;
+	     ri != derivsR.end() - discard; ri++, bi++)
+		sumR += *ri, sumB += *bi;
+	double gainR = sumR.G / (sumR.R + 1),
+	       gainB = sumB.G / (sumB.B + 1);
+	asyncResults_.temperatureK = 4500; /* don't know what it is */
+	asyncResults_.gainR = gainR;
+	asyncResults_.gainG = 1.0;
+	asyncResults_.gainB = gainB;
+}
+
+void Awb::doAwb()
+{
+	prepareStats();
+	LOG(RPiAwb, Debug) << "Valid zones: " << zones_.size();
+	if (zones_.size() > config_.minRegions) {
+		if (config_.bayes)
+			awbBayes();
+		else
+			awbGrey();
+		LOG(RPiAwb, Debug)
+			<< "CT found is "
+			<< asyncResults_.temperatureK
+			<< " with gains r " << asyncResults_.gainR
+			<< " and b " << asyncResults_.gainB;
+	}
+	/*
+	 * we're done with these; we may as well relinquish our hold on the
+	 * pointer.
+	 */
+	statistics_.reset();
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Awb(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/awb.h b/src/ipa/rpi/controller/rpi/awb.h
new file mode 100644
index 00000000..e7d49cd8
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/awb.h
@@ -0,0 +1,191 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * awb.h - AWB control algorithm
+ */
+#pragma once
+
+#include <mutex>
+#include <condition_variable>
+#include <thread>
+
+#include "../awb_algorithm.h"
+#include "../pwl.h"
+#include "../awb_status.h"
+#include "../statistics.h"
+
+namespace RPiController {
+
+/* Control algorithm to perform AWB calculations. */
+
+struct AwbMode {
+	int read(const libcamera::YamlObject &params);
+	double ctLo; /* low CT value for search */
+	double ctHi; /* high CT value for search */
+};
+
+struct AwbPrior {
+	int read(const libcamera::YamlObject &params);
+	double lux; /* lux level */
+	Pwl prior; /* maps CT to prior log likelihood for this lux level */
+};
+
+struct AwbConfig {
+	AwbConfig() : defaultMode(nullptr) {}
+	int read(const libcamera::YamlObject &params);
+	/* Only repeat the AWB calculation every "this many" frames */
+	uint16_t framePeriod;
+	/* number of initial frames for which speed taken as 1.0 (maximum) */
+	uint16_t startupFrames;
+	unsigned int convergenceFrames; /* approx number of frames to converge */
+	double speed; /* IIR filter speed applied to algorithm results */
+	bool fast; /* "fast" mode uses a 16x16 rather than 32x32 grid */
+	Pwl ctR; /* function maps CT to r (= R/G) */
+	Pwl ctB; /* function maps CT to b (= B/G) */
+	Pwl ctRInverse; /* inverse of ctR */
+	Pwl ctBInverse; /* inverse of ctB */
+	/* table of illuminant priors at different lux levels */
+	std::vector<AwbPrior> priors;
+	/* AWB "modes" (determines the search range) */
+	std::map<std::string, AwbMode> modes;
+	AwbMode *defaultMode; /* mode used if no mode selected */
+	/*
+	 * minimum proportion of pixels counted within AWB region for it to be
+	 * "useful"
+	 */
+	double minPixels;
+	/* minimum G value of those pixels, to be regarded a "useful" */
+	uint16_t minG;
+	/*
+	 * number of AWB regions that must be "useful" in order to do the AWB
+	 * calculation
+	 */
+	uint32_t minRegions;
+	/* clamp on colour error term (so as not to penalise non-grey excessively) */
+	double deltaLimit;
+	/* step size control in coarse search */
+	double coarseStep;
+	/* how far to wander off CT curve towards "more purple" */
+	double transversePos;
+	/* how far to wander off CT curve towards "more green" */
+	double transverseNeg;
+	/*
+	 * red sensitivity ratio (set to canonical sensor's R/G divided by this
+	 * sensor's R/G)
+	 */
+	double sensitivityR;
+	/*
+	 * blue sensitivity ratio (set to canonical sensor's B/G divided by this
+	 * sensor's B/G)
+	 */
+	double sensitivityB;
+	/* The whitepoint (which we normally "aim" for) can be moved. */
+	double whitepointR;
+	double whitepointB;
+	bool bayes; /* use Bayesian algorithm */
+};
+
+class Awb : public AwbAlgorithm
+{
+public:
+	Awb(Controller *controller = NULL);
+	~Awb();
+	char const *name() const override;
+	void initialise() override;
+	int read(const libcamera::YamlObject &params) override;
+	unsigned int getConvergenceFrames() const override;
+	void setMode(std::string const &name) override;
+	void setManualGains(double manualR, double manualB) override;
+	void enableAuto() override;
+	void disableAuto() override;
+	void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;
+	void prepare(Metadata *imageMetadata) override;
+	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
+	struct RGB {
+		RGB(double r = 0, double g = 0, double b = 0)
+			: R(r), G(g), B(b)
+		{
+		}
+		double R, G, B;
+		RGB &operator+=(RGB const &other)
+		{
+			R += other.R, G += other.G, B += other.B;
+			return *this;
+		}
+	};
+
+private:
+	bool isAutoEnabled() const;
+	/* configuration is read-only, and available to both threads */
+	AwbConfig config_;
+	std::thread asyncThread_;
+	void asyncFunc(); /* asynchronous thread function */
+	std::mutex mutex_;
+	/* condvar for async thread to wait on */
+	std::condition_variable asyncSignal_;
+	/* condvar for synchronous thread to wait on */
+	std::condition_variable syncSignal_;
+	/* for sync thread to check  if async thread finished (requires mutex) */
+	bool asyncFinished_;
+	/* for async thread to check if it's been told to run (requires mutex) */
+	bool asyncStart_;
+	/* for async thread to check if it's been told to quit (requires mutex) */
+	bool asyncAbort_;
+
+	/*
+	 * The following are only for the synchronous thread to use:
+	 * for sync thread to note its has asked async thread to run
+	 */
+	bool asyncStarted_;
+	/* counts up to framePeriod before restarting the async thread */
+	int framePhase_;
+	int frameCount_; /* counts up to startup_frames */
+	AwbStatus syncResults_;
+	AwbStatus prevSyncResults_;
+	std::string modeName_;
+	/*
+	 * The following are for the asynchronous thread to use, though the main
+	 * thread can set/reset them if the async thread is known to be idle:
+	 */
+	void restartAsync(StatisticsPtr &stats, double lux);
+	/* copy out the results from the async thread so that it can be restarted */
+	void fetchAsyncResults();
+	StatisticsPtr statistics_;
+	AwbMode *mode_;
+	double lux_;
+	AwbStatus asyncResults_;
+	void doAwb();
+	void awbBayes();
+	void awbGrey();
+	void prepareStats();
+	double computeDelta2Sum(double gainR, double gainB);
+	Pwl interpolatePrior();
+	double coarseSearch(Pwl const &prior);
+	void fineSearch(double &t, double &r, double &b, Pwl const &prior);
+	std::vector<RGB> zones_;
+	std::vector<Pwl::Point> points_;
+	/* manual r setting */
+	double manualR_;
+	/* manual b setting */
+	double manualB_;
+};
+
+static inline Awb::RGB operator+(Awb::RGB const &a, Awb::RGB const &b)
+{
+	return Awb::RGB(a.R + b.R, a.G + b.G, a.B + b.B);
+}
+static inline Awb::RGB operator-(Awb::RGB const &a, Awb::RGB const &b)
+{
+	return Awb::RGB(a.R - b.R, a.G - b.G, a.B - b.B);
+}
+static inline Awb::RGB operator*(double d, Awb::RGB const &rgb)
+{
+	return Awb::RGB(d * rgb.R, d * rgb.G, d * rgb.B);
+}
+static inline Awb::RGB operator*(Awb::RGB const &rgb, double d)
+{
+	return d * rgb;
+}
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/black_level.cpp b/src/ipa/rpi/controller/rpi/black_level.cpp
new file mode 100644
index 00000000..85baec3f
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/black_level.cpp
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * black_level.cpp - black level control algorithm
+ */
+
+#include <math.h>
+#include <stdint.h>
+
+#include <libcamera/base/log.h>
+
+#include "../black_level_status.h"
+
+#include "black_level.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiBlackLevel)
+
+#define NAME "rpi.black_level"
+
+BlackLevel::BlackLevel(Controller *controller)
+	: Algorithm(controller)
+{
+}
+
+char const *BlackLevel::name() const
+{
+	return NAME;
+}
+
+int BlackLevel::read(const libcamera::YamlObject &params)
+{
+	/* 64 in 10 bits scaled to 16 bits */
+	uint16_t blackLevel = params["black_level"].get<uint16_t>(4096);
+	blackLevelR_ = params["black_level_r"].get<uint16_t>(blackLevel);
+	blackLevelG_ = params["black_level_g"].get<uint16_t>(blackLevel);
+	blackLevelB_ = params["black_level_b"].get<uint16_t>(blackLevel);
+	LOG(RPiBlackLevel, Debug)
+		<< " Read black levels red " << blackLevelR_
+		<< " green " << blackLevelG_
+		<< " blue " << blackLevelB_;
+	return 0;
+}
+
+void BlackLevel::prepare(Metadata *imageMetadata)
+{
+	/*
+	 * Possibly we should think about doing this in a switchMode or
+	 * something?
+	 */
+	struct BlackLevelStatus status;
+	status.blackLevelR = blackLevelR_;
+	status.blackLevelG = blackLevelG_;
+	status.blackLevelB = blackLevelB_;
+	imageMetadata->set("black_level.status", status);
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return new BlackLevel(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/black_level.h b/src/ipa/rpi/controller/rpi/black_level.h
new file mode 100644
index 00000000..2403f7f7
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/black_level.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * black_level.h - black level control algorithm
+ */
+#pragma once
+
+#include "../algorithm.h"
+#include "../black_level_status.h"
+
+/* This is our implementation of the "black level algorithm". */
+
+namespace RPiController {
+
+class BlackLevel : public Algorithm
+{
+public:
+	BlackLevel(Controller *controller);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void prepare(Metadata *imageMetadata) override;
+
+private:
+	double blackLevelR_;
+	double blackLevelG_;
+	double blackLevelB_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/ccm.cpp b/src/ipa/rpi/controller/rpi/ccm.cpp
new file mode 100644
index 00000000..2e2e6664
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/ccm.cpp
@@ -0,0 +1,199 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * ccm.cpp - CCM (colour correction matrix) control algorithm
+ */
+
+#include <libcamera/base/log.h>
+
+#include "../awb_status.h"
+#include "../ccm_status.h"
+#include "../lux_status.h"
+#include "../metadata.h"
+
+#include "ccm.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiCcm)
+
+/*
+ * This algorithm selects a CCM (Colour Correction Matrix) according to the
+ * colour temperature estimated by AWB (interpolating between known matricies as
+ * necessary). Additionally the amount of colour saturation can be controlled
+ * both according to the current estimated lux level and according to a
+ * saturation setting that is exposed to applications.
+ */
+
+#define NAME "rpi.ccm"
+
+Matrix::Matrix()
+{
+	memset(m, 0, sizeof(m));
+}
+Matrix::Matrix(double m0, double m1, double m2, double m3, double m4, double m5,
+	       double m6, double m7, double m8)
+{
+	m[0][0] = m0, m[0][1] = m1, m[0][2] = m2, m[1][0] = m3, m[1][1] = m4,
+	m[1][2] = m5, m[2][0] = m6, m[2][1] = m7, m[2][2] = m8;
+}
+int Matrix::read(const libcamera::YamlObject &params)
+{
+	double *ptr = (double *)m;
+
+	if (params.size() != 9) {
+		LOG(RPiCcm, Error) << "Wrong number of values in CCM";
+		return -EINVAL;
+	}
+
+	for (const auto &param : params.asList()) {
+		auto value = param.get<double>();
+		if (!value)
+			return -EINVAL;
+		*ptr++ = *value;
+	}
+
+	return 0;
+}
+
+Ccm::Ccm(Controller *controller)
+	: CcmAlgorithm(controller), saturation_(1.0) {}
+
+char const *Ccm::name() const
+{
+	return NAME;
+}
+
+int Ccm::read(const libcamera::YamlObject &params)
+{
+	int ret;
+
+	if (params.contains("saturation")) {
+		ret = config_.saturation.read(params["saturation"]);
+		if (ret)
+			return ret;
+	}
+
+	for (auto &p : params["ccms"].asList()) {
+		auto value = p["ct"].get<double>();
+		if (!value)
+			return -EINVAL;
+
+		CtCcm ctCcm;
+		ctCcm.ct = *value;
+		ret = ctCcm.ccm.read(p["ccm"]);
+		if (ret)
+			return ret;
+
+		if (!config_.ccms.empty() && ctCcm.ct <= config_.ccms.back().ct) {
+			LOG(RPiCcm, Error)
+				<< "CCM not in increasing colour temperature order";
+			return -EINVAL;
+		}
+
+		config_.ccms.push_back(std::move(ctCcm));
+	}
+
+	if (config_.ccms.empty()) {
+		LOG(RPiCcm, Error) << "No CCMs specified";
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+void Ccm::setSaturation(double saturation)
+{
+	saturation_ = saturation;
+}
+
+void Ccm::initialise()
+{
+}
+
+template<typename T>
+static bool getLocked(Metadata *metadata, std::string const &tag, T &value)
+{
+	T *ptr = metadata->getLocked<T>(tag);
+	if (ptr == nullptr)
+		return false;
+	value = *ptr;
+	return true;
+}
+
+Matrix calculateCcm(std::vector<CtCcm> const &ccms, double ct)
+{
+	if (ct <= ccms.front().ct)
+		return ccms.front().ccm;
+	else if (ct >= ccms.back().ct)
+		return ccms.back().ccm;
+	else {
+		int i = 0;
+		for (; ct > ccms[i].ct; i++)
+			;
+		double lambda =
+			(ct - ccms[i - 1].ct) / (ccms[i].ct - ccms[i - 1].ct);
+		return lambda * ccms[i].ccm + (1.0 - lambda) * ccms[i - 1].ccm;
+	}
+}
+
+Matrix applySaturation(Matrix const &ccm, double saturation)
+{
+	Matrix RGB2Y(0.299, 0.587, 0.114, -0.169, -0.331, 0.500, 0.500, -0.419,
+		     -0.081);
+	Matrix Y2RGB(1.000, 0.000, 1.402, 1.000, -0.345, -0.714, 1.000, 1.771,
+		     0.000);
+	Matrix S(1, 0, 0, 0, saturation, 0, 0, 0, saturation);
+	return Y2RGB * S * RGB2Y * ccm;
+}
+
+void Ccm::prepare(Metadata *imageMetadata)
+{
+	bool awbOk = false, luxOk = false;
+	struct AwbStatus awb = {};
+	awb.temperatureK = 4000; /* in case no metadata */
+	struct LuxStatus lux = {};
+	lux.lux = 400; /* in case no metadata */
+	{
+		/* grab mutex just once to get everything */
+		std::lock_guard<Metadata> lock(*imageMetadata);
+		awbOk = getLocked(imageMetadata, "awb.status", awb);
+		luxOk = getLocked(imageMetadata, "lux.status", lux);
+	}
+	if (!awbOk)
+		LOG(RPiCcm, Warning) << "no colour temperature found";
+	if (!luxOk)
+		LOG(RPiCcm, Warning) << "no lux value found";
+	Matrix ccm = calculateCcm(config_.ccms, awb.temperatureK);
+	double saturation = saturation_;
+	struct CcmStatus ccmStatus;
+	ccmStatus.saturation = saturation;
+	if (!config_.saturation.empty())
+		saturation *= config_.saturation.eval(
+			config_.saturation.domain().clip(lux.lux));
+	ccm = applySaturation(ccm, saturation);
+	for (int j = 0; j < 3; j++)
+		for (int i = 0; i < 3; i++)
+			ccmStatus.matrix[j * 3 + i] =
+				std::max(-8.0, std::min(7.9999, ccm.m[j][i]));
+	LOG(RPiCcm, Debug)
+		<< "colour temperature " << awb.temperatureK << "K";
+	LOG(RPiCcm, Debug)
+		<< "CCM: " << ccmStatus.matrix[0] << " " << ccmStatus.matrix[1]
+		<< " " << ccmStatus.matrix[2] << "     "
+		<< ccmStatus.matrix[3] << " " << ccmStatus.matrix[4]
+		<< " " << ccmStatus.matrix[5] << "     "
+		<< ccmStatus.matrix[6] << " " << ccmStatus.matrix[7]
+		<< " " << ccmStatus.matrix[8];
+	imageMetadata->set("ccm.status", ccmStatus);
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Ccm(controller);
+	;
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/ccm.h b/src/ipa/rpi/controller/rpi/ccm.h
new file mode 100644
index 00000000..286d0b33
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/ccm.h
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * ccm.h - CCM (colour correction matrix) control algorithm
+ */
+#pragma once
+
+#include <vector>
+
+#include "../ccm_algorithm.h"
+#include "../pwl.h"
+
+namespace RPiController {
+
+/* Algorithm to calculate colour matrix. Should be placed after AWB. */
+
+struct Matrix {
+	Matrix(double m0, double m1, double m2, double m3, double m4, double m5,
+	       double m6, double m7, double m8);
+	Matrix();
+	double m[3][3];
+	int read(const libcamera::YamlObject &params);
+};
+static inline Matrix operator*(double d, Matrix const &m)
+{
+	return Matrix(m.m[0][0] * d, m.m[0][1] * d, m.m[0][2] * d,
+		      m.m[1][0] * d, m.m[1][1] * d, m.m[1][2] * d,
+		      m.m[2][0] * d, m.m[2][1] * d, m.m[2][2] * d);
+}
+static inline Matrix operator*(Matrix const &m1, Matrix const &m2)
+{
+	Matrix m;
+	for (int i = 0; i < 3; i++)
+		for (int j = 0; j < 3; j++)
+			m.m[i][j] = m1.m[i][0] * m2.m[0][j] +
+				    m1.m[i][1] * m2.m[1][j] +
+				    m1.m[i][2] * m2.m[2][j];
+	return m;
+}
+static inline Matrix operator+(Matrix const &m1, Matrix const &m2)
+{
+	Matrix m;
+	for (int i = 0; i < 3; i++)
+		for (int j = 0; j < 3; j++)
+			m.m[i][j] = m1.m[i][j] + m2.m[i][j];
+	return m;
+}
+
+struct CtCcm {
+	double ct;
+	Matrix ccm;
+};
+
+struct CcmConfig {
+	std::vector<CtCcm> ccms;
+	Pwl saturation;
+};
+
+class Ccm : public CcmAlgorithm
+{
+public:
+	Ccm(Controller *controller = NULL);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void setSaturation(double saturation) override;
+	void initialise() override;
+	void prepare(Metadata *imageMetadata) override;
+
+private:
+	CcmConfig config_;
+	double saturation_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/contrast.cpp b/src/ipa/rpi/controller/rpi/contrast.cpp
new file mode 100644
index 00000000..bee1eadd
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/contrast.cpp
@@ -0,0 +1,181 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * contrast.cpp - contrast (gamma) control algorithm
+ */
+#include <stdint.h>
+
+#include <libcamera/base/log.h>
+
+#include "../contrast_status.h"
+#include "../histogram.h"
+
+#include "contrast.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiContrast)
+
+/*
+ * This is a very simple control algorithm which simply retrieves the results of
+ * AGC and AWB via their "status" metadata, and applies digital gain to the
+ * colour channels in accordance with those instructions. We take care never to
+ * apply less than unity gains, as that would cause fully saturated pixels to go
+ * off-white.
+ */
+
+#define NAME "rpi.contrast"
+
+Contrast::Contrast(Controller *controller)
+	: ContrastAlgorithm(controller), brightness_(0.0), contrast_(1.0)
+{
+}
+
+char const *Contrast::name() const
+{
+	return NAME;
+}
+
+int Contrast::read(const libcamera::YamlObject &params)
+{
+	// enable adaptive enhancement by default
+	config_.ceEnable = params["ce_enable"].get<int>(1);
+	// the point near the bottom of the histogram to move
+	config_.loHistogram = params["lo_histogram"].get<double>(0.01);
+	// where in the range to try and move it to
+	config_.loLevel = params["lo_level"].get<double>(0.015);
+	// but don't move by more than this
+	config_.loMax = params["lo_max"].get<double>(500);
+	// equivalent values for the top of the histogram...
+	config_.hiHistogram = params["hi_histogram"].get<double>(0.95);
+	config_.hiLevel = params["hi_level"].get<double>(0.95);
+	config_.hiMax = params["hi_max"].get<double>(2000);
+	return config_.gammaCurve.read(params["gamma_curve"]);
+}
+
+void Contrast::setBrightness(double brightness)
+{
+	brightness_ = brightness;
+}
+
+void Contrast::setContrast(double contrast)
+{
+	contrast_ = contrast;
+}
+
+void Contrast::initialise()
+{
+	/*
+	 * Fill in some default values as Prepare will run before Process gets
+	 * called.
+	 */
+	status_.brightness = brightness_;
+	status_.contrast = contrast_;
+	status_.gammaCurve = config_.gammaCurve;
+}
+
+void Contrast::prepare(Metadata *imageMetadata)
+{
+	imageMetadata->set("contrast.status", status_);
+}
+
+Pwl computeStretchCurve(Histogram const &histogram,
+			ContrastConfig const &config)
+{
+	Pwl enhance;
+	enhance.append(0, 0);
+	/*
+	 * If the start of the histogram is rather empty, try to pull it down a
+	 * bit.
+	 */
+	double histLo = histogram.quantile(config.loHistogram) *
+			(65536 / histogram.bins());
+	double levelLo = config.loLevel * 65536;
+	LOG(RPiContrast, Debug)
+		<< "Move histogram point " << histLo << " to " << levelLo;
+	histLo = std::max(levelLo,
+			  std::min(65535.0, std::min(histLo, levelLo + config.loMax)));
+	LOG(RPiContrast, Debug)
+		<< "Final values " << histLo << " -> " << levelLo;
+	enhance.append(histLo, levelLo);
+	/*
+	 * Keep the mid-point (median) in the same place, though, to limit the
+	 * apparent amount of global brightness shift.
+	 */
+	double mid = histogram.quantile(0.5) * (65536 / histogram.bins());
+	enhance.append(mid, mid);
+
+	/*
+	 * If the top to the histogram is empty, try to pull the pixel values
+	 * there up.
+	 */
+	double histHi = histogram.quantile(config.hiHistogram) *
+			(65536 / histogram.bins());
+	double levelHi = config.hiLevel * 65536;
+	LOG(RPiContrast, Debug)
+		<< "Move histogram point " << histHi << " to " << levelHi;
+	histHi = std::min(levelHi,
+			  std::max(0.0, std::max(histHi, levelHi - config.hiMax)));
+	LOG(RPiContrast, Debug)
+		<< "Final values " << histHi << " -> " << levelHi;
+	enhance.append(histHi, levelHi);
+	enhance.append(65535, 65535);
+	return enhance;
+}
+
+Pwl applyManualContrast(Pwl const &gammaCurve, double brightness,
+			double contrast)
+{
+	Pwl newGammaCurve;
+	LOG(RPiContrast, Debug)
+		<< "Manual brightness " << brightness << " contrast " << contrast;
+	gammaCurve.map([&](double x, double y) {
+		newGammaCurve.append(
+			x, std::max(0.0, std::min(65535.0,
+						  (y - 32768) * contrast +
+							  32768 + brightness)));
+	});
+	return newGammaCurve;
+}
+
+void Contrast::process(StatisticsPtr &stats,
+		       [[maybe_unused]] Metadata *imageMetadata)
+{
+	Histogram &histogram = stats->yHist;
+	/*
+	 * We look at the histogram and adjust the gamma curve in the following
+	 * ways: 1. Adjust the gamma curve so as to pull the start of the
+	 * histogram down, and possibly push the end up.
+	 */
+	Pwl gammaCurve = config_.gammaCurve;
+	if (config_.ceEnable) {
+		if (config_.loMax != 0 || config_.hiMax != 0)
+			gammaCurve = computeStretchCurve(histogram, config_).compose(gammaCurve);
+		/*
+		 * We could apply other adjustments (e.g. partial equalisation)
+		 * based on the histogram...?
+		 */
+	}
+	/*
+	 * 2. Finally apply any manually selected brightness/contrast
+	 * adjustment.
+	 */
+	if (brightness_ != 0 || contrast_ != 1.0)
+		gammaCurve = applyManualContrast(gammaCurve, brightness_, contrast_);
+	/*
+	 * And fill in the status for output. Use more points towards the bottom
+	 * of the curve.
+	 */
+	status_.brightness = brightness_;
+	status_.contrast = contrast_;
+	status_.gammaCurve = std::move(gammaCurve);
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Contrast(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/contrast.h b/src/ipa/rpi/controller/rpi/contrast.h
new file mode 100644
index 00000000..9c81277a
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/contrast.h
@@ -0,0 +1,51 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * contrast.h - contrast (gamma) control algorithm
+ */
+#pragma once
+
+#include <mutex>
+
+#include "../contrast_algorithm.h"
+#include "../pwl.h"
+
+namespace RPiController {
+
+/*
+ * Back End algorithm to appaly correct digital gain. Should be placed after
+ * Back End AWB.
+ */
+
+struct ContrastConfig {
+	bool ceEnable;
+	double loHistogram;
+	double loLevel;
+	double loMax;
+	double hiHistogram;
+	double hiLevel;
+	double hiMax;
+	Pwl gammaCurve;
+};
+
+class Contrast : public ContrastAlgorithm
+{
+public:
+	Contrast(Controller *controller = NULL);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void setBrightness(double brightness) override;
+	void setContrast(double contrast) override;
+	void initialise() override;
+	void prepare(Metadata *imageMetadata) override;
+	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
+
+private:
+	ContrastConfig config_;
+	double brightness_;
+	double contrast_;
+	ContrastStatus status_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/dpc.cpp b/src/ipa/rpi/controller/rpi/dpc.cpp
new file mode 100644
index 00000000..be3871df
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/dpc.cpp
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * dpc.cpp - DPC (defective pixel correction) control algorithm
+ */
+
+#include <libcamera/base/log.h>
+
+#include "dpc.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiDpc)
+
+/*
+ * We use the lux status so that we can apply stronger settings in darkness (if
+ * necessary).
+ */
+
+#define NAME "rpi.dpc"
+
+Dpc::Dpc(Controller *controller)
+	: Algorithm(controller)
+{
+}
+
+char const *Dpc::name() const
+{
+	return NAME;
+}
+
+int Dpc::read(const libcamera::YamlObject &params)
+{
+	config_.strength = params["strength"].get<int>(1);
+	if (config_.strength < 0 || config_.strength > 2) {
+		LOG(RPiDpc, Error) << "Bad strength value";
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+void Dpc::prepare(Metadata *imageMetadata)
+{
+	DpcStatus dpcStatus = {};
+	/* Should we vary this with lux level or analogue gain? TBD. */
+	dpcStatus.strength = config_.strength;
+	LOG(RPiDpc, Debug) << "strength " << dpcStatus.strength;
+	imageMetadata->set("dpc.status", dpcStatus);
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Dpc(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/dpc.h b/src/ipa/rpi/controller/rpi/dpc.h
new file mode 100644
index 00000000..84a05604
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/dpc.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * dpc.h - DPC (defective pixel correction) control algorithm
+ */
+#pragma once
+
+#include "../algorithm.h"
+#include "../dpc_status.h"
+
+namespace RPiController {
+
+/* Back End algorithm to apply appropriate GEQ settings. */
+
+struct DpcConfig {
+	int strength;
+};
+
+class Dpc : public Algorithm
+{
+public:
+	Dpc(Controller *controller);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void prepare(Metadata *imageMetadata) override;
+
+private:
+	DpcConfig config_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/focus.h b/src/ipa/rpi/controller/rpi/focus.h
new file mode 100644
index 00000000..8556039d
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/focus.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * focus.h - focus algorithm
+ */
+#pragma once
+
+#include "../algorithm.h"
+#include "../metadata.h"
+
+/*
+ * The "focus" algorithm. All it does it print out a version of the
+ * focus contrast measure; there is no actual auto-focus mechanism to
+ * control.
+ */
+
+namespace RPiController {
+
+class Focus : public Algorithm
+{
+public:
+	Focus(Controller *controller);
+	char const *name() const override;
+	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/geq.cpp b/src/ipa/rpi/controller/rpi/geq.cpp
new file mode 100644
index 00000000..510870e9
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/geq.cpp
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * geq.cpp - GEQ (green equalisation) control algorithm
+ */
+
+#include <libcamera/base/log.h>
+
+#include "../device_status.h"
+#include "../lux_status.h"
+#include "../pwl.h"
+
+#include "geq.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiGeq)
+
+/*
+ * We use the lux status so that we can apply stronger settings in darkness (if
+ * necessary).
+ */
+
+#define NAME "rpi.geq"
+
+Geq::Geq(Controller *controller)
+	: Algorithm(controller)
+{
+}
+
+char const *Geq::name() const
+{
+	return NAME;
+}
+
+int Geq::read(const libcamera::YamlObject &params)
+{
+	config_.offset = params["offset"].get<uint16_t>(0);
+	config_.slope = params["slope"].get<double>(0.0);
+	if (config_.slope < 0.0 || config_.slope >= 1.0) {
+		LOG(RPiGeq, Error) << "Bad slope value";
+		return -EINVAL;
+	}
+
+	if (params.contains("strength")) {
+		int ret = config_.strength.read(params["strength"]);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+void Geq::prepare(Metadata *imageMetadata)
+{
+	LuxStatus luxStatus = {};
+	luxStatus.lux = 400;
+	if (imageMetadata->get("lux.status", luxStatus))
+		LOG(RPiGeq, Warning) << "no lux data found";
+	DeviceStatus deviceStatus;
+	deviceStatus.analogueGain = 1.0; /* in case not found */
+	if (imageMetadata->get("device.status", deviceStatus))
+		LOG(RPiGeq, Warning)
+			<< "no device metadata - use analogue gain of 1x";
+	GeqStatus geqStatus = {};
+	double strength = config_.strength.empty()
+			? 1.0
+			: config_.strength.eval(config_.strength.domain().clip(luxStatus.lux));
+	strength *= deviceStatus.analogueGain;
+	double offset = config_.offset * strength;
+	double slope = config_.slope * strength;
+	geqStatus.offset = std::min(65535.0, std::max(0.0, offset));
+	geqStatus.slope = std::min(.99999, std::max(0.0, slope));
+	LOG(RPiGeq, Debug)
+		<< "offset " << geqStatus.offset << " slope "
+		<< geqStatus.slope << " (analogue gain "
+		<< deviceStatus.analogueGain << " lux "
+		<< luxStatus.lux << ")";
+	imageMetadata->set("geq.status", geqStatus);
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Geq(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/geq.h b/src/ipa/rpi/controller/rpi/geq.h
new file mode 100644
index 00000000..ee3a52ff
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/geq.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * geq.h - GEQ (green equalisation) control algorithm
+ */
+#pragma once
+
+#include "../algorithm.h"
+#include "../geq_status.h"
+
+namespace RPiController {
+
+/* Back End algorithm to apply appropriate GEQ settings. */
+
+struct GeqConfig {
+	uint16_t offset;
+	double slope;
+	Pwl strength; /* lux to strength factor */
+};
+
+class Geq : public Algorithm
+{
+public:
+	Geq(Controller *controller);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void prepare(Metadata *imageMetadata) override;
+
+private:
+	GeqConfig config_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/lux.cpp b/src/ipa/rpi/controller/rpi/lux.cpp
new file mode 100644
index 00000000..06625f3a
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/lux.cpp
@@ -0,0 +1,115 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * lux.cpp - Lux control algorithm
+ */
+#include <math.h>
+
+#include <libcamera/base/log.h>
+
+#include "../device_status.h"
+
+#include "lux.h"
+
+using namespace RPiController;
+using namespace libcamera;
+using namespace std::literals::chrono_literals;
+
+LOG_DEFINE_CATEGORY(RPiLux)
+
+#define NAME "rpi.lux"
+
+Lux::Lux(Controller *controller)
+	: Algorithm(controller)
+{
+	/*
+	 * Put in some defaults as there will be no meaningful values until
+	 * Process has run.
+	 */
+	status_.aperture = 1.0;
+	status_.lux = 400;
+}
+
+char const *Lux::name() const
+{
+	return NAME;
+}
+
+int Lux::read(const libcamera::YamlObject &params)
+{
+	auto value = params["reference_shutter_speed"].get<double>();
+	if (!value)
+		return -EINVAL;
+	referenceShutterSpeed_ = *value * 1.0us;
+
+	value = params["reference_gain"].get<double>();
+	if (!value)
+		return -EINVAL;
+	referenceGain_ = *value;
+
+	referenceAperture_ = params["reference_aperture"].get<double>(1.0);
+
+	value = params["reference_Y"].get<double>();
+	if (!value)
+		return -EINVAL;
+	referenceY_ = *value;
+
+	value = params["reference_lux"].get<double>();
+	if (!value)
+		return -EINVAL;
+	referenceLux_ = *value;
+
+	currentAperture_ = referenceAperture_;
+	return 0;
+}
+
+void Lux::setCurrentAperture(double aperture)
+{
+	currentAperture_ = aperture;
+}
+
+void Lux::prepare(Metadata *imageMetadata)
+{
+	std::unique_lock<std::mutex> lock(mutex_);
+	imageMetadata->set("lux.status", status_);
+}
+
+void Lux::process(StatisticsPtr &stats, Metadata *imageMetadata)
+{
+	DeviceStatus deviceStatus;
+	if (imageMetadata->get("device.status", deviceStatus) == 0) {
+		double currentGain = deviceStatus.analogueGain;
+		double currentAperture = deviceStatus.aperture.value_or(currentAperture_);
+		double currentY = stats->yHist.interQuantileMean(0, 1);
+		double gainRatio = referenceGain_ / currentGain;
+		double shutterSpeedRatio =
+			referenceShutterSpeed_ / deviceStatus.shutterSpeed;
+		double apertureRatio = referenceAperture_ / currentAperture;
+		double yRatio = currentY * (65536 / stats->yHist.bins()) / referenceY_;
+		double estimatedLux = shutterSpeedRatio * gainRatio *
+				      apertureRatio * apertureRatio *
+				      yRatio * referenceLux_;
+		LuxStatus status;
+		status.lux = estimatedLux;
+		status.aperture = currentAperture;
+		LOG(RPiLux, Debug) << ": estimated lux " << estimatedLux;
+		{
+			std::unique_lock<std::mutex> lock(mutex_);
+			status_ = status;
+		}
+		/*
+		 * Overwrite the metadata here as well, so that downstream
+		 * algorithms get the latest value.
+		 */
+		imageMetadata->set("lux.status", status);
+	} else
+		LOG(RPiLux, Warning) << ": no device metadata";
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Lux(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/lux.h b/src/ipa/rpi/controller/rpi/lux.h
new file mode 100644
index 00000000..89411a54
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/lux.h
@@ -0,0 +1,45 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * lux.h - Lux control algorithm
+ */
+#pragma once
+
+#include <mutex>
+
+#include <libcamera/base/utils.h>
+
+#include "../lux_status.h"
+#include "../algorithm.h"
+
+/* This is our implementation of the "lux control algorithm". */
+
+namespace RPiController {
+
+class Lux : public Algorithm
+{
+public:
+	Lux(Controller *controller);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void prepare(Metadata *imageMetadata) override;
+	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
+	void setCurrentAperture(double aperture);
+
+private:
+	/*
+	 * These values define the conditions of the reference image, against
+	 * which we compare the new image.
+	 */
+	libcamera::utils::Duration referenceShutterSpeed_;
+	double referenceGain_;
+	double referenceAperture_; /* units of 1/f */
+	double referenceY_; /* out of 65536 */
+	double referenceLux_;
+	double currentAperture_;
+	LuxStatus status_;
+	std::mutex mutex_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/noise.cpp b/src/ipa/rpi/controller/rpi/noise.cpp
new file mode 100644
index 00000000..bcd8b9ed
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/noise.cpp
@@ -0,0 +1,89 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * noise.cpp - Noise control algorithm
+ */
+
+#include <math.h>
+
+#include <libcamera/base/log.h>
+
+#include "../device_status.h"
+#include "../noise_status.h"
+
+#include "noise.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiNoise)
+
+#define NAME "rpi.noise"
+
+Noise::Noise(Controller *controller)
+	: Algorithm(controller), modeFactor_(1.0)
+{
+}
+
+char const *Noise::name() const
+{
+	return NAME;
+}
+
+void Noise::switchMode(CameraMode const &cameraMode,
+		       [[maybe_unused]] Metadata *metadata)
+{
+	/*
+	 * For example, we would expect a 2x2 binned mode to have a "noise
+	 * factor" of sqrt(2x2) = 2. (can't be less than one, right?)
+	 */
+	modeFactor_ = std::max(1.0, cameraMode.noiseFactor);
+}
+
+int Noise::read(const libcamera::YamlObject &params)
+{
+	auto value = params["reference_constant"].get<double>();
+	if (!value)
+		return -EINVAL;
+	referenceConstant_ = *value;
+
+	value = params["reference_slope"].get<double>();
+	if (!value)
+		return -EINVAL;
+	referenceSlope_ = *value;
+
+	return 0;
+}
+
+void Noise::prepare(Metadata *imageMetadata)
+{
+	struct DeviceStatus deviceStatus;
+	deviceStatus.analogueGain = 1.0; /* keep compiler calm */
+	if (imageMetadata->get("device.status", deviceStatus) == 0) {
+		/*
+		 * There is a slight question as to exactly how the noise
+		 * profile, specifically the constant part of it, scales. For
+		 * now we assume it all scales the same, and we'll revisit this
+		 * if it proves substantially wrong.  NOTE: we may also want to
+		 * make some adjustments based on the camera mode (such as
+		 * binning), if we knew how to discover it...
+		 */
+		double factor = sqrt(deviceStatus.analogueGain) / modeFactor_;
+		struct NoiseStatus status;
+		status.noiseConstant = referenceConstant_ * factor;
+		status.noiseSlope = referenceSlope_ * factor;
+		imageMetadata->set("noise.status", status);
+		LOG(RPiNoise, Debug)
+			<< "constant " << status.noiseConstant
+			<< " slope " << status.noiseSlope;
+	} else
+		LOG(RPiNoise, Warning) << " no metadata";
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return new Noise(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/noise.h b/src/ipa/rpi/controller/rpi/noise.h
new file mode 100644
index 00000000..74c31e64
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/noise.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * noise.h - Noise control algorithm
+ */
+#pragma once
+
+#include "../algorithm.h"
+#include "../noise_status.h"
+
+/* This is our implementation of the "noise algorithm". */
+
+namespace RPiController {
+
+class Noise : public Algorithm
+{
+public:
+	Noise(Controller *controller);
+	char const *name() const override;
+	void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;
+	int read(const libcamera::YamlObject &params) override;
+	void prepare(Metadata *imageMetadata) override;
+
+private:
+	/* the noise profile for analogue gain of 1.0 */
+	double referenceConstant_;
+	double referenceSlope_;
+	double modeFactor_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/sdn.cpp b/src/ipa/rpi/controller/rpi/sdn.cpp
new file mode 100644
index 00000000..b6b66251
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/sdn.cpp
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019-2021, Raspberry Pi Ltd
+ *
+ * sdn.cpp - SDN (spatial denoise) control algorithm
+ */
+
+#include <libcamera/base/log.h>
+
+#include "../denoise_status.h"
+#include "../noise_status.h"
+
+#include "sdn.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiSdn)
+
+/*
+ * Calculate settings for the spatial denoise block using the noise profile in
+ * the image metadata.
+ */
+
+#define NAME "rpi.sdn"
+
+Sdn::Sdn(Controller *controller)
+	: DenoiseAlgorithm(controller), mode_(DenoiseMode::ColourOff)
+{
+}
+
+char const *Sdn::name() const
+{
+	return NAME;
+}
+
+int Sdn::read(const libcamera::YamlObject &params)
+{
+	deviation_ = params["deviation"].get<double>(3.2);
+	strength_ = params["strength"].get<double>(0.75);
+	return 0;
+}
+
+void Sdn::initialise()
+{
+}
+
+void Sdn::prepare(Metadata *imageMetadata)
+{
+	struct NoiseStatus noiseStatus = {};
+	noiseStatus.noiseSlope = 3.0; /* in case no metadata */
+	if (imageMetadata->get("noise.status", noiseStatus) != 0)
+		LOG(RPiSdn, Warning) << "no noise profile found";
+	LOG(RPiSdn, Debug)
+		<< "Noise profile: constant " << noiseStatus.noiseConstant
+		<< " slope " << noiseStatus.noiseSlope;
+	struct DenoiseStatus status;
+	status.noiseConstant = noiseStatus.noiseConstant * deviation_;
+	status.noiseSlope = noiseStatus.noiseSlope * deviation_;
+	status.strength = strength_;
+	status.mode = static_cast<std::underlying_type_t<DenoiseMode>>(mode_);
+	imageMetadata->set("denoise.status", status);
+	LOG(RPiSdn, Debug)
+		<< "programmed constant " << status.noiseConstant
+		<< " slope " << status.noiseSlope
+		<< " strength " << status.strength;
+}
+
+void Sdn::setMode(DenoiseMode mode)
+{
+	/* We only distinguish between off and all other modes. */
+	mode_ = mode;
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Sdn(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/sdn.h b/src/ipa/rpi/controller/rpi/sdn.h
new file mode 100644
index 00000000..9dd73c38
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/sdn.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * sdn.h - SDN (spatial denoise) control algorithm
+ */
+#pragma once
+
+#include "../algorithm.h"
+#include "../denoise_algorithm.h"
+
+namespace RPiController {
+
+/* Algorithm to calculate correct spatial denoise (SDN) settings. */
+
+class Sdn : public DenoiseAlgorithm
+{
+public:
+	Sdn(Controller *controller = NULL);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void initialise() override;
+	void prepare(Metadata *imageMetadata) override;
+	void setMode(DenoiseMode mode) override;
+
+private:
+	double deviation_;
+	double strength_;
+	DenoiseMode mode_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/sharpen.cpp b/src/ipa/rpi/controller/rpi/sharpen.cpp
new file mode 100644
index 00000000..4f6f020a
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/sharpen.cpp
@@ -0,0 +1,92 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * sharpen.cpp - sharpening control algorithm
+ */
+
+#include <math.h>
+
+#include <libcamera/base/log.h>
+
+#include "../sharpen_status.h"
+
+#include "sharpen.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiSharpen)
+
+#define NAME "rpi.sharpen"
+
+Sharpen::Sharpen(Controller *controller)
+	: SharpenAlgorithm(controller), userStrength_(1.0)
+{
+}
+
+char const *Sharpen::name() const
+{
+	return NAME;
+}
+
+void Sharpen::switchMode(CameraMode const &cameraMode,
+			 [[maybe_unused]] Metadata *metadata)
+{
+	/* can't be less than one, right? */
+	modeFactor_ = std::max(1.0, cameraMode.noiseFactor);
+}
+
+int Sharpen::read(const libcamera::YamlObject &params)
+{
+	threshold_ = params["threshold"].get<double>(1.0);
+	strength_ = params["strength"].get<double>(1.0);
+	limit_ = params["limit"].get<double>(1.0);
+	LOG(RPiSharpen, Debug)
+		<< "Read threshold " << threshold_
+		<< " strength " << strength_
+		<< " limit " << limit_;
+	return 0;
+}
+
+void Sharpen::setStrength(double strength)
+{
+	/*
+	 * Note that this function is how an application sets the overall
+	 * sharpening "strength". We call this the "user strength" field
+	 * as there already is a strength_ field - being an internal gain
+	 * parameter that gets passed to the ISP control code. Negative
+	 * values are not allowed - coerce them to zero (no sharpening).
+	 */
+	userStrength_ = std::max(0.0, strength);
+}
+
+void Sharpen::prepare(Metadata *imageMetadata)
+{
+	/*
+	 * The userStrength_ affects the algorithm's internal gain directly, but
+	 * we adjust the limit and threshold less aggressively. Using a sqrt
+	 * function is an arbitrary but gentle way of accomplishing this.
+	 */
+	double userStrengthSqrt = sqrt(userStrength_);
+	struct SharpenStatus status;
+	/*
+	 * Binned modes seem to need the sharpening toned down with this
+	 * pipeline, thus we use the modeFactor_ here. Also avoid
+	 * divide-by-zero with the userStrengthSqrt.
+	 */
+	status.threshold = threshold_ * modeFactor_ /
+			   std::max(0.01, userStrengthSqrt);
+	status.strength = strength_ / modeFactor_ * userStrength_;
+	status.limit = limit_ / modeFactor_ * userStrengthSqrt;
+	/* Finally, report any application-supplied parameters that were used. */
+	status.userStrength = userStrength_;
+	imageMetadata->set("sharpen.status", status);
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return new Sharpen(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/sharpen.h b/src/ipa/rpi/controller/rpi/sharpen.h
new file mode 100644
index 00000000..8bb7631e
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/sharpen.h
@@ -0,0 +1,34 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * sharpen.h - sharpening control algorithm
+ */
+#pragma once
+
+#include "../sharpen_algorithm.h"
+#include "../sharpen_status.h"
+
+/* This is our implementation of the "sharpen algorithm". */
+
+namespace RPiController {
+
+class Sharpen : public SharpenAlgorithm
+{
+public:
+	Sharpen(Controller *controller);
+	char const *name() const override;
+	void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;
+	int read(const libcamera::YamlObject &params) override;
+	void setStrength(double strength) override;
+	void prepare(Metadata *imageMetadata) override;
+
+private:
+	double threshold_;
+	double strength_;
+	double limit_;
+	double modeFactor_;
+	double userStrength_;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/sharpen_algorithm.h b/src/ipa/rpi/controller/sharpen_algorithm.h
new file mode 100644
index 00000000..3be21c32
--- /dev/null
+++ b/src/ipa/rpi/controller/sharpen_algorithm.h
@@ -0,0 +1,21 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * sharpen_algorithm.h - sharpness control algorithm interface
+ */
+#pragma once
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+class SharpenAlgorithm : public Algorithm
+{
+public:
+	SharpenAlgorithm(Controller *controller) : Algorithm(controller) {}
+	/* A sharpness control algorithm must provide the following: */
+	virtual void setStrength(double strength) = 0;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/sharpen_status.h b/src/ipa/rpi/controller/sharpen_status.h
new file mode 100644
index 00000000..106166db
--- /dev/null
+++ b/src/ipa/rpi/controller/sharpen_status.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * sharpen_status.h - Sharpen control algorithm status
+ */
+#pragma once
+
+/* The "sharpen" algorithm stores the strength to use. */
+
+struct SharpenStatus {
+	/* controls the smallest level of detail (or noise!) that sharpening will pick up */
+	double threshold;
+	/* the rate at which the sharpening response ramps once above the threshold */
+	double strength;
+	/* upper limit of the allowed sharpening response */
+	double limit;
+	/* The sharpening strength requested by the user or application. */
+	double userStrength;
+};
diff --git a/src/ipa/rpi/controller/statistics.h b/src/ipa/rpi/controller/statistics.h
new file mode 100644
index 00000000..015d4efc
--- /dev/null
+++ b/src/ipa/rpi/controller/statistics.h
@@ -0,0 +1,78 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022, Raspberry Pi Ltd
+ *
+ * statistics.h - Raspberry Pi generic statistics structure
+ */
+#pragma once
+
+#include <memory>
+#include <stdint.h>
+#include <vector>
+
+#include "histogram.h"
+#include "region_stats.h"
+
+namespace RPiController {
+
+struct RgbySums {
+	RgbySums(uint64_t _rSum = 0, uint64_t _gSum = 0, uint64_t _bSum = 0, uint64_t _ySum = 0)
+		: rSum(_rSum), gSum(_gSum), bSum(_bSum), ySum(_ySum)
+	{
+	}
+	uint64_t rSum;
+	uint64_t gSum;
+	uint64_t bSum;
+	uint64_t ySum;
+};
+
+using RgbyRegions = RegionStats<RgbySums>;
+using FocusRegions = RegionStats<uint64_t>;
+
+struct Statistics {
+	/*
+	 * All region based statistics are normalised to 16-bits, giving a
+	 * maximum value of (1 << NormalisationFactorPow2) - 1.
+	 */
+	static constexpr unsigned int NormalisationFactorPow2 = 16;
+
+	/*
+	 * Positioning of the AGC statistics gathering in the pipeline:
+	 * Pre-WB correction or post-WB correction.
+	 * Assume this is post-LSC.
+	 */
+	enum class AgcStatsPos { PreWb, PostWb };
+	const AgcStatsPos agcStatsPos;
+
+	/*
+	 * Positioning of the AWB/ALSC statistics gathering in the pipeline:
+	 * Pre-LSC or post-LSC.
+	 */
+	enum class ColourStatsPos { PreLsc, PostLsc };
+	const ColourStatsPos colourStatsPos;
+
+	Statistics(AgcStatsPos a, ColourStatsPos c)
+		: agcStatsPos(a), colourStatsPos(c)
+	{
+	}
+
+	/* Histogram statistics. Not all histograms may be populated! */
+	Histogram rHist;
+	Histogram gHist;
+	Histogram bHist;
+	Histogram yHist;
+
+	/* Row sums for flicker avoidance. */
+	std::vector<RgbySums> rowSums;
+
+	/* Region based colour sums. */
+	RgbyRegions agcRegions;
+	RgbyRegions awbRegions;
+
+	/* Region based focus FoM. */
+	FocusRegions focusRegions;
+};
+
+using StatisticsPtr = std::shared_ptr<Statistics>;
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/meson.build b/src/ipa/rpi/meson.build
new file mode 100644
index 00000000..7d7a61f7
--- /dev/null
+++ b/src/ipa/rpi/meson.build
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: CC0-1.0
+
+subdir('cam_helper')
+subdir('controller')
+
+foreach pipeline : pipelines
+    pipeline = pipeline.split('/')
+    if pipeline.length() < 2 or pipeline[0] != 'rpi'
+        continue
+    endif
+
+    subdir(pipeline[1])
+endforeach
diff --git a/src/ipa/rpi/vc4/data/imx219.json b/src/ipa/rpi/vc4/data/imx219.json
new file mode 100644
index 00000000..efe7210a
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx219.json
@@ -0,0 +1,486 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 27685,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 998,
+                "reference_Y": 12744
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 3.67
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 204,
+                "slope": 0.01633
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2498.0, 0.9309, 0.3599,
+                    2911.0, 0.8682, 0.4283,
+                    2919.0, 0.8358, 0.4621,
+                    3627.0, 0.7646, 0.5327,
+                    4600.0, 0.6079, 0.6721,
+                    5716.0, 0.5712, 0.7017,
+                    8575.0, 0.4331, 0.8037
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.04791,
+                "transverse_neg": 0.04881
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 66666 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 33333 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ],
+                    "shadows": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.5,
+                            "y_target":
+                            [
+                                0, 0.17,
+                                1000, 0.17
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.7,
+                "calibrations_Cr": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.487, 1.481, 1.481, 1.445, 1.389, 1.327, 1.307, 1.307, 1.307, 1.309, 1.341, 1.405, 1.458, 1.494, 1.494, 1.497,
+                            1.491, 1.481, 1.448, 1.397, 1.331, 1.275, 1.243, 1.229, 1.229, 1.249, 1.287, 1.349, 1.409, 1.463, 1.494, 1.497,
+                            1.491, 1.469, 1.405, 1.331, 1.275, 1.217, 1.183, 1.172, 1.172, 1.191, 1.231, 1.287, 1.349, 1.424, 1.484, 1.499,
+                            1.487, 1.444, 1.363, 1.283, 1.217, 1.183, 1.148, 1.138, 1.138, 1.159, 1.191, 1.231, 1.302, 1.385, 1.461, 1.492,
+                            1.481, 1.423, 1.334, 1.253, 1.189, 1.148, 1.135, 1.119, 1.123, 1.137, 1.159, 1.203, 1.272, 1.358, 1.442, 1.488,
+                            1.479, 1.413, 1.321, 1.236, 1.176, 1.139, 1.118, 1.114, 1.116, 1.123, 1.149, 1.192, 1.258, 1.344, 1.432, 1.487,
+                            1.479, 1.413, 1.321, 1.236, 1.176, 1.139, 1.116, 1.114, 1.115, 1.123, 1.149, 1.192, 1.258, 1.344, 1.432, 1.487,
+                            1.479, 1.425, 1.336, 1.251, 1.189, 1.149, 1.136, 1.118, 1.121, 1.138, 1.158, 1.206, 1.275, 1.358, 1.443, 1.488,
+                            1.488, 1.448, 1.368, 1.285, 1.219, 1.189, 1.149, 1.139, 1.139, 1.158, 1.195, 1.235, 1.307, 1.387, 1.462, 1.493,
+                            1.496, 1.475, 1.411, 1.337, 1.284, 1.219, 1.189, 1.176, 1.176, 1.195, 1.235, 1.296, 1.356, 1.429, 1.487, 1.501,
+                            1.495, 1.489, 1.458, 1.407, 1.337, 1.287, 1.253, 1.239, 1.239, 1.259, 1.296, 1.356, 1.419, 1.472, 1.499, 1.499,
+                            1.494, 1.489, 1.489, 1.453, 1.398, 1.336, 1.317, 1.317, 1.317, 1.321, 1.351, 1.416, 1.467, 1.501, 1.501, 1.499
+                        ]
+                    },
+                    {
+                        "ct": 3850,
+                        "table":
+                        [
+                            1.694, 1.688, 1.688, 1.649, 1.588, 1.518, 1.495, 1.495, 1.495, 1.497, 1.532, 1.602, 1.659, 1.698, 1.698, 1.703,
+                            1.698, 1.688, 1.653, 1.597, 1.525, 1.464, 1.429, 1.413, 1.413, 1.437, 1.476, 1.542, 1.606, 1.665, 1.698, 1.703,
+                            1.697, 1.673, 1.605, 1.525, 1.464, 1.401, 1.369, 1.354, 1.354, 1.377, 1.417, 1.476, 1.542, 1.623, 1.687, 1.705,
+                            1.692, 1.646, 1.561, 1.472, 1.401, 1.368, 1.337, 1.323, 1.324, 1.348, 1.377, 1.417, 1.492, 1.583, 1.661, 1.697,
+                            1.686, 1.625, 1.528, 1.439, 1.372, 1.337, 1.321, 1.311, 1.316, 1.324, 1.348, 1.389, 1.461, 1.553, 1.642, 1.694,
+                            1.684, 1.613, 1.514, 1.423, 1.359, 1.328, 1.311, 1.306, 1.306, 1.316, 1.339, 1.378, 1.446, 1.541, 1.633, 1.693,
+                            1.684, 1.613, 1.514, 1.423, 1.359, 1.328, 1.311, 1.305, 1.305, 1.316, 1.339, 1.378, 1.446, 1.541, 1.633, 1.693,
+                            1.685, 1.624, 1.529, 1.438, 1.372, 1.336, 1.324, 1.309, 1.314, 1.323, 1.348, 1.392, 1.462, 1.555, 1.646, 1.694,
+                            1.692, 1.648, 1.561, 1.473, 1.403, 1.372, 1.336, 1.324, 1.324, 1.348, 1.378, 1.423, 1.495, 1.585, 1.667, 1.701,
+                            1.701, 1.677, 1.608, 1.527, 1.471, 1.403, 1.375, 1.359, 1.359, 1.378, 1.423, 1.488, 1.549, 1.631, 1.694, 1.709,
+                            1.702, 1.694, 1.656, 1.601, 1.527, 1.473, 1.441, 1.424, 1.424, 1.443, 1.488, 1.549, 1.621, 1.678, 1.706, 1.707,
+                            1.699, 1.694, 1.694, 1.654, 1.593, 1.525, 1.508, 1.508, 1.508, 1.509, 1.546, 1.614, 1.674, 1.708, 1.708, 1.707
+                        ]
+                    },
+                    {
+                        "ct": 6000,
+                        "table":
+                        [
+                            2.179, 2.176, 2.176, 2.125, 2.048, 1.975, 1.955, 1.954, 1.954, 1.956, 1.993, 2.071, 2.141, 2.184, 2.185, 2.188,
+                            2.189, 2.176, 2.128, 2.063, 1.973, 1.908, 1.872, 1.856, 1.856, 1.876, 1.922, 1.999, 2.081, 2.144, 2.184, 2.192,
+                            2.187, 2.152, 2.068, 1.973, 1.907, 1.831, 1.797, 1.786, 1.786, 1.804, 1.853, 1.922, 1.999, 2.089, 2.166, 2.191,
+                            2.173, 2.117, 2.013, 1.908, 1.831, 1.791, 1.755, 1.749, 1.749, 1.767, 1.804, 1.853, 1.939, 2.041, 2.135, 2.181,
+                            2.166, 2.089, 1.975, 1.869, 1.792, 1.755, 1.741, 1.731, 1.734, 1.749, 1.767, 1.818, 1.903, 2.005, 2.111, 2.173,
+                            2.165, 2.074, 1.956, 1.849, 1.777, 1.742, 1.729, 1.725, 1.729, 1.734, 1.758, 1.804, 1.884, 1.991, 2.099, 2.172,
+                            2.165, 2.074, 1.956, 1.849, 1.777, 1.742, 1.727, 1.724, 1.725, 1.734, 1.758, 1.804, 1.884, 1.991, 2.099, 2.172,
+                            2.166, 2.085, 1.975, 1.869, 1.791, 1.755, 1.741, 1.729, 1.733, 1.749, 1.769, 1.819, 1.904, 2.009, 2.114, 2.174,
+                            2.174, 2.118, 2.015, 1.913, 1.831, 1.791, 1.755, 1.749, 1.749, 1.769, 1.811, 1.855, 1.943, 2.047, 2.139, 2.183,
+                            2.187, 2.151, 2.072, 1.979, 1.911, 1.831, 1.801, 1.791, 1.791, 1.811, 1.855, 1.933, 2.006, 2.101, 2.173, 2.197,
+                            2.189, 2.178, 2.132, 2.069, 1.979, 1.913, 1.879, 1.867, 1.867, 1.891, 1.933, 2.006, 2.091, 2.156, 2.195, 2.197,
+                            2.181, 2.179, 2.178, 2.131, 2.057, 1.981, 1.965, 1.965, 1.965, 1.969, 1.999, 2.083, 2.153, 2.197, 2.197, 2.196
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.967, 1.961, 1.955, 1.953, 1.954, 1.957, 1.961, 1.963, 1.963, 1.961, 1.959, 1.957, 1.954, 1.951, 1.951, 1.955,
+                            1.961, 1.959, 1.957, 1.956, 1.962, 1.967, 1.975, 1.979, 1.979, 1.975, 1.971, 1.967, 1.957, 1.952, 1.951, 1.951,
+                            1.959, 1.959, 1.959, 1.966, 1.976, 1.989, 1.999, 2.004, 2.003, 1.997, 1.991, 1.981, 1.967, 1.956, 1.951, 1.951,
+                            1.959, 1.962, 1.967, 1.978, 1.993, 2.009, 2.021, 2.028, 2.026, 2.021, 2.011, 1.995, 1.981, 1.964, 1.953, 1.951,
+                            1.961, 1.965, 1.977, 1.993, 2.009, 2.023, 2.041, 2.047, 2.047, 2.037, 2.024, 2.011, 1.995, 1.975, 1.958, 1.953,
+                            1.963, 1.968, 1.981, 2.001, 2.019, 2.039, 2.046, 2.052, 2.052, 2.051, 2.035, 2.021, 2.001, 1.978, 1.959, 1.955,
+                            1.961, 1.966, 1.981, 2.001, 2.019, 2.038, 2.043, 2.051, 2.052, 2.042, 2.034, 2.019, 2.001, 1.978, 1.959, 1.954,
+                            1.957, 1.961, 1.972, 1.989, 2.003, 2.021, 2.038, 2.039, 2.039, 2.034, 2.019, 2.004, 1.988, 1.971, 1.954, 1.949,
+                            1.952, 1.953, 1.959, 1.972, 1.989, 2.003, 2.016, 2.019, 2.019, 2.014, 2.003, 1.988, 1.971, 1.955, 1.948, 1.947,
+                            1.949, 1.948, 1.949, 1.957, 1.971, 1.978, 1.991, 1.994, 1.994, 1.989, 1.979, 1.967, 1.954, 1.946, 1.947, 1.947,
+                            1.949, 1.946, 1.944, 1.946, 1.949, 1.954, 1.962, 1.967, 1.967, 1.963, 1.956, 1.948, 1.943, 1.943, 1.946, 1.949,
+                            1.951, 1.946, 1.944, 1.942, 1.943, 1.943, 1.947, 1.948, 1.949, 1.947, 1.945, 1.941, 1.938, 1.939, 1.948, 1.952
+                        ]
+                    },
+                    {
+                        "ct": 3850,
+                        "table":
+                        [
+                            1.726, 1.724, 1.722, 1.723, 1.731, 1.735, 1.743, 1.746, 1.746, 1.741, 1.735, 1.729, 1.725, 1.721, 1.721, 1.721,
+                            1.724, 1.723, 1.723, 1.727, 1.735, 1.744, 1.749, 1.756, 1.756, 1.749, 1.744, 1.735, 1.727, 1.719, 1.719, 1.719,
+                            1.723, 1.723, 1.724, 1.735, 1.746, 1.759, 1.767, 1.775, 1.775, 1.766, 1.758, 1.746, 1.735, 1.723, 1.718, 1.716,
+                            1.723, 1.725, 1.732, 1.746, 1.759, 1.775, 1.782, 1.792, 1.792, 1.782, 1.772, 1.759, 1.745, 1.729, 1.718, 1.716,
+                            1.725, 1.729, 1.738, 1.756, 1.775, 1.785, 1.796, 1.803, 1.804, 1.794, 1.783, 1.772, 1.757, 1.736, 1.722, 1.718,
+                            1.728, 1.731, 1.741, 1.759, 1.781, 1.795, 1.803, 1.806, 1.808, 1.805, 1.791, 1.779, 1.762, 1.739, 1.722, 1.721,
+                            1.727, 1.731, 1.741, 1.759, 1.781, 1.791, 1.799, 1.804, 1.806, 1.801, 1.791, 1.779, 1.762, 1.739, 1.722, 1.717,
+                            1.722, 1.724, 1.733, 1.751, 1.768, 1.781, 1.791, 1.796, 1.799, 1.791, 1.781, 1.766, 1.754, 1.731, 1.717, 1.714,
+                            1.718, 1.718, 1.724, 1.737, 1.752, 1.768, 1.776, 1.782, 1.784, 1.781, 1.766, 1.754, 1.737, 1.724, 1.713, 1.709,
+                            1.716, 1.715, 1.716, 1.725, 1.737, 1.749, 1.756, 1.763, 1.764, 1.762, 1.749, 1.737, 1.724, 1.717, 1.709, 1.708,
+                            1.715, 1.714, 1.712, 1.715, 1.722, 1.729, 1.736, 1.741, 1.742, 1.739, 1.731, 1.723, 1.717, 1.712, 1.711, 1.709,
+                            1.716, 1.714, 1.711, 1.712, 1.715, 1.719, 1.723, 1.728, 1.731, 1.729, 1.723, 1.718, 1.711, 1.711, 1.713, 1.713
+                        ]
+                    },
+                    {
+                        "ct": 6000,
+                        "table":
+                        [
+                            1.374, 1.372, 1.373, 1.374, 1.375, 1.378, 1.378, 1.381, 1.382, 1.382, 1.378, 1.373, 1.372, 1.369, 1.365, 1.365,
+                            1.371, 1.371, 1.372, 1.374, 1.378, 1.381, 1.384, 1.386, 1.388, 1.387, 1.384, 1.377, 1.372, 1.368, 1.364, 1.362,
+                            1.369, 1.371, 1.372, 1.377, 1.383, 1.391, 1.394, 1.396, 1.397, 1.395, 1.391, 1.382, 1.374, 1.369, 1.362, 1.361,
+                            1.369, 1.371, 1.375, 1.383, 1.391, 1.399, 1.402, 1.404, 1.405, 1.403, 1.398, 1.391, 1.379, 1.371, 1.363, 1.361,
+                            1.371, 1.373, 1.378, 1.388, 1.399, 1.407, 1.411, 1.413, 1.413, 1.411, 1.405, 1.397, 1.385, 1.374, 1.366, 1.362,
+                            1.371, 1.374, 1.379, 1.389, 1.405, 1.411, 1.414, 1.414, 1.415, 1.415, 1.411, 1.401, 1.388, 1.376, 1.367, 1.363,
+                            1.371, 1.373, 1.379, 1.389, 1.405, 1.408, 1.413, 1.414, 1.414, 1.413, 1.409, 1.401, 1.388, 1.376, 1.367, 1.362,
+                            1.366, 1.369, 1.374, 1.384, 1.396, 1.404, 1.407, 1.408, 1.408, 1.408, 1.401, 1.395, 1.382, 1.371, 1.363, 1.359,
+                            1.364, 1.365, 1.368, 1.375, 1.386, 1.396, 1.399, 1.401, 1.399, 1.399, 1.395, 1.385, 1.374, 1.365, 1.359, 1.357,
+                            1.361, 1.363, 1.365, 1.368, 1.377, 1.384, 1.388, 1.391, 1.391, 1.388, 1.385, 1.375, 1.366, 1.361, 1.358, 1.356,
+                            1.361, 1.362, 1.362, 1.364, 1.367, 1.373, 1.376, 1.377, 1.377, 1.375, 1.373, 1.366, 1.362, 1.358, 1.358, 1.358,
+                            1.361, 1.362, 1.362, 1.362, 1.363, 1.367, 1.369, 1.368, 1.367, 1.367, 1.367, 1.364, 1.358, 1.357, 1.358, 1.359
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    2.716, 2.568, 2.299, 2.065, 1.845, 1.693, 1.605, 1.597, 1.596, 1.634, 1.738, 1.914, 2.145, 2.394, 2.719, 2.901,
+                    2.593, 2.357, 2.093, 1.876, 1.672, 1.528, 1.438, 1.393, 1.394, 1.459, 1.569, 1.731, 1.948, 2.169, 2.481, 2.756,
+                    2.439, 2.197, 1.922, 1.691, 1.521, 1.365, 1.266, 1.222, 1.224, 1.286, 1.395, 1.573, 1.747, 1.988, 2.299, 2.563,
+                    2.363, 2.081, 1.797, 1.563, 1.376, 1.244, 1.152, 1.099, 1.101, 1.158, 1.276, 1.421, 1.607, 1.851, 2.163, 2.455,
+                    2.342, 2.003, 1.715, 1.477, 1.282, 1.152, 1.074, 1.033, 1.035, 1.083, 1.163, 1.319, 1.516, 1.759, 2.064, 2.398,
+                    2.342, 1.985, 1.691, 1.446, 1.249, 1.111, 1.034, 1.004, 1.004, 1.028, 1.114, 1.274, 1.472, 1.716, 2.019, 2.389,
+                    2.342, 1.991, 1.691, 1.446, 1.249, 1.112, 1.034, 1.011, 1.005, 1.035, 1.114, 1.274, 1.472, 1.716, 2.019, 2.389,
+                    2.365, 2.052, 1.751, 1.499, 1.299, 1.171, 1.089, 1.039, 1.042, 1.084, 1.162, 1.312, 1.516, 1.761, 2.059, 2.393,
+                    2.434, 2.159, 1.856, 1.601, 1.403, 1.278, 1.166, 1.114, 1.114, 1.162, 1.266, 1.402, 1.608, 1.847, 2.146, 2.435,
+                    2.554, 2.306, 2.002, 1.748, 1.563, 1.396, 1.299, 1.247, 1.243, 1.279, 1.386, 1.551, 1.746, 1.977, 2.272, 2.518,
+                    2.756, 2.493, 2.195, 1.947, 1.739, 1.574, 1.481, 1.429, 1.421, 1.457, 1.559, 1.704, 1.929, 2.159, 2.442, 2.681,
+                    2.935, 2.739, 2.411, 2.151, 1.922, 1.749, 1.663, 1.628, 1.625, 1.635, 1.716, 1.872, 2.113, 2.368, 2.663, 2.824
+                ],
+                "sigma": 0.00381,
+                "sigma_Cb": 0.00216
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2498,
+                        "ccm":
+                        [
+                            1.58731, -0.18011, -0.40721,
+                            -0.60639, 2.03422, -0.42782,
+                            -0.19612, -1.69203, 2.88815
+                        ]
+                    },
+                    {
+                        "ct": 2811,
+                        "ccm":
+                        [
+                            1.61593, -0.33164, -0.28429,
+                            -0.55048, 1.97779, -0.42731,
+                            -0.12042, -1.42847, 2.54889
+                        ]
+                    },
+                    {
+                        "ct": 2911,
+                        "ccm":
+                        [
+                            1.62771, -0.41282, -0.21489,
+                            -0.57991, 2.04176, -0.46186,
+                            -0.07613, -1.13359, 2.20972
+                        ]
+                    },
+                    {
+                        "ct": 2919,
+                        "ccm":
+                        [
+                            1.62661, -0.37736, -0.24925,
+                            -0.52519, 1.95233, -0.42714,
+                            -0.10842, -1.34929, 2.45771
+                        ]
+                    },
+                    {
+                        "ct": 3627,
+                        "ccm":
+                        [
+                            1.70385, -0.57231, -0.13154,
+                            -0.47763, 1.85998, -0.38235,
+                            -0.07467, -0.82678, 1.90145
+                        ]
+                    },
+                    {
+                        "ct": 4600,
+                        "ccm":
+                        [
+                            1.68486, -0.61085, -0.07402,
+                            -0.41927, 2.04016, -0.62089,
+                            -0.08633, -0.67672, 1.76305
+                        ]
+                    },
+                    {
+                        "ct": 5716,
+                        "ccm":
+                        [
+                            1.80439, -0.73699, -0.06739,
+                            -0.36073, 1.83327, -0.47255,
+                            -0.08378, -0.56403, 1.64781
+                        ]
+                    },
+                    {
+                        "ct": 8575,
+                        "ccm":
+                        [
+                            1.89357, -0.76427, -0.12931,
+                            -0.27399, 2.15605, -0.88206,
+                            -0.12035, -0.68256, 1.80292
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx219_noir.json b/src/ipa/rpi/vc4/data/imx219_noir.json
new file mode 100644
index 00000000..cfedb943
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx219_noir.json
@@ -0,0 +1,402 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 27685,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 998,
+                "reference_Y": 12744
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 3.67
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 204,
+                "slope": 0.01633
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "bayes": 0
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 66666 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 33333 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ],
+                    "shadows": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.5,
+                            "y_target":
+                            [
+                                0, 0.17,
+                                1000, 0.17
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.7,
+                "calibrations_Cr": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.487, 1.481, 1.481, 1.445, 1.389, 1.327, 1.307, 1.307, 1.307, 1.309, 1.341, 1.405, 1.458, 1.494, 1.494, 1.497,
+                            1.491, 1.481, 1.448, 1.397, 1.331, 1.275, 1.243, 1.229, 1.229, 1.249, 1.287, 1.349, 1.409, 1.463, 1.494, 1.497,
+                            1.491, 1.469, 1.405, 1.331, 1.275, 1.217, 1.183, 1.172, 1.172, 1.191, 1.231, 1.287, 1.349, 1.424, 1.484, 1.499,
+                            1.487, 1.444, 1.363, 1.283, 1.217, 1.183, 1.148, 1.138, 1.138, 1.159, 1.191, 1.231, 1.302, 1.385, 1.461, 1.492,
+                            1.481, 1.423, 1.334, 1.253, 1.189, 1.148, 1.135, 1.119, 1.123, 1.137, 1.159, 1.203, 1.272, 1.358, 1.442, 1.488,
+                            1.479, 1.413, 1.321, 1.236, 1.176, 1.139, 1.118, 1.114, 1.116, 1.123, 1.149, 1.192, 1.258, 1.344, 1.432, 1.487,
+                            1.479, 1.413, 1.321, 1.236, 1.176, 1.139, 1.116, 1.114, 1.115, 1.123, 1.149, 1.192, 1.258, 1.344, 1.432, 1.487,
+                            1.479, 1.425, 1.336, 1.251, 1.189, 1.149, 1.136, 1.118, 1.121, 1.138, 1.158, 1.206, 1.275, 1.358, 1.443, 1.488,
+                            1.488, 1.448, 1.368, 1.285, 1.219, 1.189, 1.149, 1.139, 1.139, 1.158, 1.195, 1.235, 1.307, 1.387, 1.462, 1.493,
+                            1.496, 1.475, 1.411, 1.337, 1.284, 1.219, 1.189, 1.176, 1.176, 1.195, 1.235, 1.296, 1.356, 1.429, 1.487, 1.501,
+                            1.495, 1.489, 1.458, 1.407, 1.337, 1.287, 1.253, 1.239, 1.239, 1.259, 1.296, 1.356, 1.419, 1.472, 1.499, 1.499,
+                            1.494, 1.489, 1.489, 1.453, 1.398, 1.336, 1.317, 1.317, 1.317, 1.321, 1.351, 1.416, 1.467, 1.501, 1.501, 1.499
+                        ]
+                    },
+                    {
+                        "ct": 3850,
+                        "table":
+                        [
+                            1.694, 1.688, 1.688, 1.649, 1.588, 1.518, 1.495, 1.495, 1.495, 1.497, 1.532, 1.602, 1.659, 1.698, 1.698, 1.703,
+                            1.698, 1.688, 1.653, 1.597, 1.525, 1.464, 1.429, 1.413, 1.413, 1.437, 1.476, 1.542, 1.606, 1.665, 1.698, 1.703,
+                            1.697, 1.673, 1.605, 1.525, 1.464, 1.401, 1.369, 1.354, 1.354, 1.377, 1.417, 1.476, 1.542, 1.623, 1.687, 1.705,
+                            1.692, 1.646, 1.561, 1.472, 1.401, 1.368, 1.337, 1.323, 1.324, 1.348, 1.377, 1.417, 1.492, 1.583, 1.661, 1.697,
+                            1.686, 1.625, 1.528, 1.439, 1.372, 1.337, 1.321, 1.311, 1.316, 1.324, 1.348, 1.389, 1.461, 1.553, 1.642, 1.694,
+                            1.684, 1.613, 1.514, 1.423, 1.359, 1.328, 1.311, 1.306, 1.306, 1.316, 1.339, 1.378, 1.446, 1.541, 1.633, 1.693,
+                            1.684, 1.613, 1.514, 1.423, 1.359, 1.328, 1.311, 1.305, 1.305, 1.316, 1.339, 1.378, 1.446, 1.541, 1.633, 1.693,
+                            1.685, 1.624, 1.529, 1.438, 1.372, 1.336, 1.324, 1.309, 1.314, 1.323, 1.348, 1.392, 1.462, 1.555, 1.646, 1.694,
+                            1.692, 1.648, 1.561, 1.473, 1.403, 1.372, 1.336, 1.324, 1.324, 1.348, 1.378, 1.423, 1.495, 1.585, 1.667, 1.701,
+                            1.701, 1.677, 1.608, 1.527, 1.471, 1.403, 1.375, 1.359, 1.359, 1.378, 1.423, 1.488, 1.549, 1.631, 1.694, 1.709,
+                            1.702, 1.694, 1.656, 1.601, 1.527, 1.473, 1.441, 1.424, 1.424, 1.443, 1.488, 1.549, 1.621, 1.678, 1.706, 1.707,
+                            1.699, 1.694, 1.694, 1.654, 1.593, 1.525, 1.508, 1.508, 1.508, 1.509, 1.546, 1.614, 1.674, 1.708, 1.708, 1.707
+                        ]
+                    },
+                    {
+                        "ct": 6000,
+                        "table":
+                        [
+                            2.179, 2.176, 2.176, 2.125, 2.048, 1.975, 1.955, 1.954, 1.954, 1.956, 1.993, 2.071, 2.141, 2.184, 2.185, 2.188,
+                            2.189, 2.176, 2.128, 2.063, 1.973, 1.908, 1.872, 1.856, 1.856, 1.876, 1.922, 1.999, 2.081, 2.144, 2.184, 2.192,
+                            2.187, 2.152, 2.068, 1.973, 1.907, 1.831, 1.797, 1.786, 1.786, 1.804, 1.853, 1.922, 1.999, 2.089, 2.166, 2.191,
+                            2.173, 2.117, 2.013, 1.908, 1.831, 1.791, 1.755, 1.749, 1.749, 1.767, 1.804, 1.853, 1.939, 2.041, 2.135, 2.181,
+                            2.166, 2.089, 1.975, 1.869, 1.792, 1.755, 1.741, 1.731, 1.734, 1.749, 1.767, 1.818, 1.903, 2.005, 2.111, 2.173,
+                            2.165, 2.074, 1.956, 1.849, 1.777, 1.742, 1.729, 1.725, 1.729, 1.734, 1.758, 1.804, 1.884, 1.991, 2.099, 2.172,
+                            2.165, 2.074, 1.956, 1.849, 1.777, 1.742, 1.727, 1.724, 1.725, 1.734, 1.758, 1.804, 1.884, 1.991, 2.099, 2.172,
+                            2.166, 2.085, 1.975, 1.869, 1.791, 1.755, 1.741, 1.729, 1.733, 1.749, 1.769, 1.819, 1.904, 2.009, 2.114, 2.174,
+                            2.174, 2.118, 2.015, 1.913, 1.831, 1.791, 1.755, 1.749, 1.749, 1.769, 1.811, 1.855, 1.943, 2.047, 2.139, 2.183,
+                            2.187, 2.151, 2.072, 1.979, 1.911, 1.831, 1.801, 1.791, 1.791, 1.811, 1.855, 1.933, 2.006, 2.101, 2.173, 2.197,
+                            2.189, 2.178, 2.132, 2.069, 1.979, 1.913, 1.879, 1.867, 1.867, 1.891, 1.933, 2.006, 2.091, 2.156, 2.195, 2.197,
+                            2.181, 2.179, 2.178, 2.131, 2.057, 1.981, 1.965, 1.965, 1.965, 1.969, 1.999, 2.083, 2.153, 2.197, 2.197, 2.196
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.967, 1.961, 1.955, 1.953, 1.954, 1.957, 1.961, 1.963, 1.963, 1.961, 1.959, 1.957, 1.954, 1.951, 1.951, 1.955,
+                            1.961, 1.959, 1.957, 1.956, 1.962, 1.967, 1.975, 1.979, 1.979, 1.975, 1.971, 1.967, 1.957, 1.952, 1.951, 1.951,
+                            1.959, 1.959, 1.959, 1.966, 1.976, 1.989, 1.999, 2.004, 2.003, 1.997, 1.991, 1.981, 1.967, 1.956, 1.951, 1.951,
+                            1.959, 1.962, 1.967, 1.978, 1.993, 2.009, 2.021, 2.028, 2.026, 2.021, 2.011, 1.995, 1.981, 1.964, 1.953, 1.951,
+                            1.961, 1.965, 1.977, 1.993, 2.009, 2.023, 2.041, 2.047, 2.047, 2.037, 2.024, 2.011, 1.995, 1.975, 1.958, 1.953,
+                            1.963, 1.968, 1.981, 2.001, 2.019, 2.039, 2.046, 2.052, 2.052, 2.051, 2.035, 2.021, 2.001, 1.978, 1.959, 1.955,
+                            1.961, 1.966, 1.981, 2.001, 2.019, 2.038, 2.043, 2.051, 2.052, 2.042, 2.034, 2.019, 2.001, 1.978, 1.959, 1.954,
+                            1.957, 1.961, 1.972, 1.989, 2.003, 2.021, 2.038, 2.039, 2.039, 2.034, 2.019, 2.004, 1.988, 1.971, 1.954, 1.949,
+                            1.952, 1.953, 1.959, 1.972, 1.989, 2.003, 2.016, 2.019, 2.019, 2.014, 2.003, 1.988, 1.971, 1.955, 1.948, 1.947,
+                            1.949, 1.948, 1.949, 1.957, 1.971, 1.978, 1.991, 1.994, 1.994, 1.989, 1.979, 1.967, 1.954, 1.946, 1.947, 1.947,
+                            1.949, 1.946, 1.944, 1.946, 1.949, 1.954, 1.962, 1.967, 1.967, 1.963, 1.956, 1.948, 1.943, 1.943, 1.946, 1.949,
+                            1.951, 1.946, 1.944, 1.942, 1.943, 1.943, 1.947, 1.948, 1.949, 1.947, 1.945, 1.941, 1.938, 1.939, 1.948, 1.952
+                        ]
+                    },
+                    {
+                        "ct": 3850,
+                        "table":
+                        [
+                            1.726, 1.724, 1.722, 1.723, 1.731, 1.735, 1.743, 1.746, 1.746, 1.741, 1.735, 1.729, 1.725, 1.721, 1.721, 1.721,
+                            1.724, 1.723, 1.723, 1.727, 1.735, 1.744, 1.749, 1.756, 1.756, 1.749, 1.744, 1.735, 1.727, 1.719, 1.719, 1.719,
+                            1.723, 1.723, 1.724, 1.735, 1.746, 1.759, 1.767, 1.775, 1.775, 1.766, 1.758, 1.746, 1.735, 1.723, 1.718, 1.716,
+                            1.723, 1.725, 1.732, 1.746, 1.759, 1.775, 1.782, 1.792, 1.792, 1.782, 1.772, 1.759, 1.745, 1.729, 1.718, 1.716,
+                            1.725, 1.729, 1.738, 1.756, 1.775, 1.785, 1.796, 1.803, 1.804, 1.794, 1.783, 1.772, 1.757, 1.736, 1.722, 1.718,
+                            1.728, 1.731, 1.741, 1.759, 1.781, 1.795, 1.803, 1.806, 1.808, 1.805, 1.791, 1.779, 1.762, 1.739, 1.722, 1.721,
+                            1.727, 1.731, 1.741, 1.759, 1.781, 1.791, 1.799, 1.804, 1.806, 1.801, 1.791, 1.779, 1.762, 1.739, 1.722, 1.717,
+                            1.722, 1.724, 1.733, 1.751, 1.768, 1.781, 1.791, 1.796, 1.799, 1.791, 1.781, 1.766, 1.754, 1.731, 1.717, 1.714,
+                            1.718, 1.718, 1.724, 1.737, 1.752, 1.768, 1.776, 1.782, 1.784, 1.781, 1.766, 1.754, 1.737, 1.724, 1.713, 1.709,
+                            1.716, 1.715, 1.716, 1.725, 1.737, 1.749, 1.756, 1.763, 1.764, 1.762, 1.749, 1.737, 1.724, 1.717, 1.709, 1.708,
+                            1.715, 1.714, 1.712, 1.715, 1.722, 1.729, 1.736, 1.741, 1.742, 1.739, 1.731, 1.723, 1.717, 1.712, 1.711, 1.709,
+                            1.716, 1.714, 1.711, 1.712, 1.715, 1.719, 1.723, 1.728, 1.731, 1.729, 1.723, 1.718, 1.711, 1.711, 1.713, 1.713
+                        ]
+                    },
+                    {
+                        "ct": 6000,
+                        "table":
+                        [
+                            1.374, 1.372, 1.373, 1.374, 1.375, 1.378, 1.378, 1.381, 1.382, 1.382, 1.378, 1.373, 1.372, 1.369, 1.365, 1.365,
+                            1.371, 1.371, 1.372, 1.374, 1.378, 1.381, 1.384, 1.386, 1.388, 1.387, 1.384, 1.377, 1.372, 1.368, 1.364, 1.362,
+                            1.369, 1.371, 1.372, 1.377, 1.383, 1.391, 1.394, 1.396, 1.397, 1.395, 1.391, 1.382, 1.374, 1.369, 1.362, 1.361,
+                            1.369, 1.371, 1.375, 1.383, 1.391, 1.399, 1.402, 1.404, 1.405, 1.403, 1.398, 1.391, 1.379, 1.371, 1.363, 1.361,
+                            1.371, 1.373, 1.378, 1.388, 1.399, 1.407, 1.411, 1.413, 1.413, 1.411, 1.405, 1.397, 1.385, 1.374, 1.366, 1.362,
+                            1.371, 1.374, 1.379, 1.389, 1.405, 1.411, 1.414, 1.414, 1.415, 1.415, 1.411, 1.401, 1.388, 1.376, 1.367, 1.363,
+                            1.371, 1.373, 1.379, 1.389, 1.405, 1.408, 1.413, 1.414, 1.414, 1.413, 1.409, 1.401, 1.388, 1.376, 1.367, 1.362,
+                            1.366, 1.369, 1.374, 1.384, 1.396, 1.404, 1.407, 1.408, 1.408, 1.408, 1.401, 1.395, 1.382, 1.371, 1.363, 1.359,
+                            1.364, 1.365, 1.368, 1.375, 1.386, 1.396, 1.399, 1.401, 1.399, 1.399, 1.395, 1.385, 1.374, 1.365, 1.359, 1.357,
+                            1.361, 1.363, 1.365, 1.368, 1.377, 1.384, 1.388, 1.391, 1.391, 1.388, 1.385, 1.375, 1.366, 1.361, 1.358, 1.356,
+                            1.361, 1.362, 1.362, 1.364, 1.367, 1.373, 1.376, 1.377, 1.377, 1.375, 1.373, 1.366, 1.362, 1.358, 1.358, 1.358,
+                            1.361, 1.362, 1.362, 1.362, 1.363, 1.367, 1.369, 1.368, 1.367, 1.367, 1.367, 1.364, 1.358, 1.357, 1.358, 1.359
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    2.716, 2.568, 2.299, 2.065, 1.845, 1.693, 1.605, 1.597, 1.596, 1.634, 1.738, 1.914, 2.145, 2.394, 2.719, 2.901,
+                    2.593, 2.357, 2.093, 1.876, 1.672, 1.528, 1.438, 1.393, 1.394, 1.459, 1.569, 1.731, 1.948, 2.169, 2.481, 2.756,
+                    2.439, 2.197, 1.922, 1.691, 1.521, 1.365, 1.266, 1.222, 1.224, 1.286, 1.395, 1.573, 1.747, 1.988, 2.299, 2.563,
+                    2.363, 2.081, 1.797, 1.563, 1.376, 1.244, 1.152, 1.099, 1.101, 1.158, 1.276, 1.421, 1.607, 1.851, 2.163, 2.455,
+                    2.342, 2.003, 1.715, 1.477, 1.282, 1.152, 1.074, 1.033, 1.035, 1.083, 1.163, 1.319, 1.516, 1.759, 2.064, 2.398,
+                    2.342, 1.985, 1.691, 1.446, 1.249, 1.111, 1.034, 1.004, 1.004, 1.028, 1.114, 1.274, 1.472, 1.716, 2.019, 2.389,
+                    2.342, 1.991, 1.691, 1.446, 1.249, 1.112, 1.034, 1.011, 1.005, 1.035, 1.114, 1.274, 1.472, 1.716, 2.019, 2.389,
+                    2.365, 2.052, 1.751, 1.499, 1.299, 1.171, 1.089, 1.039, 1.042, 1.084, 1.162, 1.312, 1.516, 1.761, 2.059, 2.393,
+                    2.434, 2.159, 1.856, 1.601, 1.403, 1.278, 1.166, 1.114, 1.114, 1.162, 1.266, 1.402, 1.608, 1.847, 2.146, 2.435,
+                    2.554, 2.306, 2.002, 1.748, 1.563, 1.396, 1.299, 1.247, 1.243, 1.279, 1.386, 1.551, 1.746, 1.977, 2.272, 2.518,
+                    2.756, 2.493, 2.195, 1.947, 1.739, 1.574, 1.481, 1.429, 1.421, 1.457, 1.559, 1.704, 1.929, 2.159, 2.442, 2.681,
+                    2.935, 2.739, 2.411, 2.151, 1.922, 1.749, 1.663, 1.628, 1.625, 1.635, 1.716, 1.872, 2.113, 2.368, 2.663, 2.824
+                ],
+                "sigma": 0.00381,
+                "sigma_Cb": 0.00216
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2498,
+                        "ccm":
+                        [
+                            1.58731, -0.18011, -0.40721,
+                            -0.60639, 2.03422, -0.42782,
+                            -0.19612, -1.69203, 2.88815
+                        ]
+                    },
+                    {
+                        "ct": 2811,
+                        "ccm":
+                        [
+                            1.61593, -0.33164, -0.28429,
+                            -0.55048, 1.97779, -0.42731,
+                            -0.12042, -1.42847, 2.54889
+                        ]
+                    },
+                    {
+                        "ct": 2911,
+                        "ccm":
+                        [
+                            1.62771, -0.41282, -0.21489,
+                            -0.57991, 2.04176, -0.46186,
+                            -0.07613, -1.13359, 2.20972
+                        ]
+                    },
+                    {
+                        "ct": 2919,
+                        "ccm":
+                        [
+                            1.62661, -0.37736, -0.24925,
+                            -0.52519, 1.95233, -0.42714,
+                            -0.10842, -1.34929, 2.45771
+                        ]
+                    },
+                    {
+                        "ct": 3627,
+                        "ccm":
+                        [
+                            1.70385, -0.57231, -0.13154,
+                            -0.47763, 1.85998, -0.38235,
+                            -0.07467, -0.82678, 1.90145
+                        ]
+                    },
+                    {
+                        "ct": 4600,
+                        "ccm":
+                        [
+                            1.68486, -0.61085, -0.07402,
+                            -0.41927, 2.04016, -0.62089,
+                            -0.08633, -0.67672, 1.76305
+                        ]
+                    },
+                    {
+                        "ct": 5716,
+                        "ccm":
+                        [
+                            1.80439, -0.73699, -0.06739,
+                            -0.36073, 1.83327, -0.47255,
+                            -0.08378, -0.56403, 1.64781
+                        ]
+                    },
+                    {
+                        "ct": 8575,
+                        "ccm":
+                        [
+                            1.89357, -0.76427, -0.12931,
+                            -0.27399, 2.15605, -0.88206,
+                            -0.12035, -0.68256, 1.80292
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx290.json b/src/ipa/rpi/vc4/data/imx290.json
new file mode 100644
index 00000000..ace68d0e
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx290.json
@@ -0,0 +1,200 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 3840
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 6813,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 890,
+                "reference_Y": 12900
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.67
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 187,
+                "slope": 0.00842
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "bayes": 0
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "speed": 0.2,
+                "metering_modes":
+                {
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    },
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 10, 30000, 60000 ],
+                        "gain": [ 1.0, 2.0, 8.0 ]
+                    },
+                    "sport":
+                    {
+                        "shutter": [ 10, 5000, 10000, 20000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [  ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.16,
+                    10000, 0.16
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.7,
+                "luminance_lut":
+                [
+                    2.844, 2.349, 2.018, 1.775, 1.599, 1.466, 1.371, 1.321, 1.306, 1.316, 1.357, 1.439, 1.552, 1.705, 1.915, 2.221,
+                    2.576, 2.151, 1.851, 1.639, 1.478, 1.358, 1.272, 1.231, 1.218, 1.226, 1.262, 1.335, 1.438, 1.571, 1.766, 2.067,
+                    2.381, 2.005, 1.739, 1.545, 1.389, 1.278, 1.204, 1.166, 1.153, 1.161, 1.194, 1.263, 1.356, 1.489, 1.671, 1.943,
+                    2.242, 1.899, 1.658, 1.481, 1.329, 1.225, 1.156, 1.113, 1.096, 1.107, 1.143, 1.201, 1.289, 1.423, 1.607, 1.861,
+                    2.152, 1.831, 1.602, 1.436, 1.291, 1.193, 1.121, 1.069, 1.047, 1.062, 1.107, 1.166, 1.249, 1.384, 1.562, 1.801,
+                    2.104, 1.795, 1.572, 1.407, 1.269, 1.174, 1.099, 1.041, 1.008, 1.029, 1.083, 1.146, 1.232, 1.364, 1.547, 1.766,
+                    2.104, 1.796, 1.572, 1.403, 1.264, 1.171, 1.097, 1.036, 1.001, 1.025, 1.077, 1.142, 1.231, 1.363, 1.549, 1.766,
+                    2.148, 1.827, 1.594, 1.413, 1.276, 1.184, 1.114, 1.062, 1.033, 1.049, 1.092, 1.153, 1.242, 1.383, 1.577, 1.795,
+                    2.211, 1.881, 1.636, 1.455, 1.309, 1.214, 1.149, 1.104, 1.081, 1.089, 1.125, 1.184, 1.273, 1.423, 1.622, 1.846,
+                    2.319, 1.958, 1.698, 1.516, 1.362, 1.262, 1.203, 1.156, 1.137, 1.142, 1.171, 1.229, 1.331, 1.484, 1.682, 1.933,
+                    2.459, 2.072, 1.789, 1.594, 1.441, 1.331, 1.261, 1.219, 1.199, 1.205, 1.232, 1.301, 1.414, 1.571, 1.773, 2.052,
+                    2.645, 2.206, 1.928, 1.728, 1.559, 1.451, 1.352, 1.301, 1.282, 1.289, 1.319, 1.395, 1.519, 1.685, 1.904, 2.227
+                ],
+                "sigma": 0.005,
+                "sigma_Cb": 0.005
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 3900,
+                        "ccm":
+                        [
+                            1.54659, -0.17707, -0.36953,
+                            -0.51471, 1.72733, -0.21262,
+                            0.06667, -0.92279, 1.85612
+                        ]
+                    }
+                ]
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx296.json b/src/ipa/rpi/vc4/data/imx296.json
new file mode 100644
index 00000000..ae8722c4
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx296.json
@@ -0,0 +1,537 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 3840
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 7598,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 800,
+                "reference_Y": 14028
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.671
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 215,
+                "slope": 0.01058
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 7600
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 7600
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2500.0, 0.5386, 0.2458,
+                    2800.0, 0.4883, 0.3303,
+                    2900.0, 0.4855, 0.3349,
+                    3620.0, 0.4203, 0.4367,
+                    4560.0, 0.3455, 0.5444,
+                    5600.0, 0.2948, 0.6124,
+                    7400.0, 0.2336, 0.6894
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.03093,
+                "transverse_neg": 0.02374
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 30000, 45000, 60000, 120000 ],
+                        "gain": [ 1.0, 1.0, 2.0, 4.0, 12.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 30000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 8.0, 16.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 4000,
+                        "table":
+                        [
+                            2.726, 2.736, 2.737, 2.739, 2.741, 2.741, 2.742, 2.742, 2.743, 2.743, 2.742, 2.742, 2.742, 2.742, 2.741, 2.739,
+                            2.728, 2.736, 2.739, 2.741, 2.742, 2.743, 2.744, 2.745, 2.746, 2.746, 2.745, 2.743, 2.742, 2.742, 2.742, 2.741,
+                            2.729, 2.737, 2.741, 2.744, 2.746, 2.747, 2.748, 2.749, 2.751, 2.751, 2.749, 2.746, 2.744, 2.743, 2.743, 2.743,
+                            2.729, 2.738, 2.743, 2.746, 2.749, 2.749, 2.751, 2.752, 2.753, 2.753, 2.752, 2.751, 2.746, 2.744, 2.744, 2.746,
+                            2.728, 2.737, 2.742, 2.746, 2.749, 2.751, 2.754, 2.755, 2.754, 2.755, 2.754, 2.751, 2.748, 2.746, 2.747, 2.748,
+                            2.724, 2.738, 2.742, 2.746, 2.749, 2.752, 2.755, 2.755, 2.755, 2.755, 2.754, 2.752, 2.749, 2.749, 2.748, 2.748,
+                            2.726, 2.738, 2.741, 2.745, 2.749, 2.753, 2.754, 2.755, 2.755, 2.755, 2.754, 2.753, 2.749, 2.748, 2.748, 2.748,
+                            2.726, 2.738, 2.741, 2.745, 2.746, 2.752, 2.753, 2.753, 2.753, 2.753, 2.754, 2.751, 2.748, 2.748, 2.746, 2.745,
+                            2.726, 2.736, 2.738, 2.742, 2.745, 2.749, 2.752, 2.753, 2.752, 2.752, 2.751, 2.749, 2.747, 2.745, 2.744, 2.742,
+                            2.724, 2.733, 2.736, 2.739, 2.742, 2.745, 2.748, 2.749, 2.749, 2.748, 2.748, 2.747, 2.744, 2.743, 2.742, 2.741,
+                            2.722, 2.726, 2.733, 2.735, 2.737, 2.741, 2.743, 2.744, 2.744, 2.744, 2.744, 2.742, 2.741, 2.741, 2.739, 2.737,
+                            2.719, 2.722, 2.727, 2.729, 2.731, 2.732, 2.734, 2.734, 2.735, 2.735, 2.735, 2.734, 2.733, 2.732, 2.732, 2.732
+                        ]
+                    },
+                    {
+                        "ct": 6000,
+                        "table":
+                        [
+                            3.507, 3.522, 3.525, 3.527, 3.531, 3.533, 3.534, 3.535, 3.535, 3.536, 3.536, 3.537, 3.537, 3.538, 3.537, 3.536,
+                            3.511, 3.524, 3.528, 3.532, 3.533, 3.535, 3.537, 3.538, 3.538, 3.541, 3.539, 3.539, 3.539, 3.539, 3.538, 3.538,
+                            3.513, 3.528, 3.532, 3.535, 3.538, 3.542, 3.543, 3.546, 3.548, 3.551, 3.547, 3.543, 3.541, 3.541, 3.541, 3.541,
+                            3.513, 3.528, 3.533, 3.539, 3.544, 3.546, 3.548, 3.552, 3.553, 3.553, 3.552, 3.548, 3.543, 3.542, 3.542, 3.545,
+                            3.513, 3.528, 3.534, 3.541, 3.547, 3.549, 3.552, 3.553, 3.554, 3.554, 3.553, 3.549, 3.546, 3.544, 3.547, 3.549,
+                            3.508, 3.528, 3.533, 3.541, 3.548, 3.551, 3.553, 3.554, 3.555, 3.555, 3.555, 3.551, 3.548, 3.547, 3.549, 3.551,
+                            3.511, 3.529, 3.534, 3.541, 3.548, 3.551, 3.553, 3.555, 3.555, 3.555, 3.556, 3.554, 3.549, 3.548, 3.548, 3.548,
+                            3.511, 3.528, 3.533, 3.539, 3.546, 3.549, 3.553, 3.554, 3.554, 3.554, 3.554, 3.553, 3.549, 3.547, 3.547, 3.547,
+                            3.511, 3.527, 3.533, 3.536, 3.541, 3.547, 3.551, 3.553, 3.553, 3.552, 3.551, 3.551, 3.548, 3.544, 3.542, 3.543,
+                            3.507, 3.523, 3.528, 3.533, 3.538, 3.541, 3.546, 3.548, 3.549, 3.548, 3.548, 3.546, 3.542, 3.541, 3.541, 3.541,
+                            3.505, 3.514, 3.523, 3.527, 3.532, 3.537, 3.538, 3.544, 3.544, 3.544, 3.542, 3.541, 3.537, 3.537, 3.536, 3.535,
+                            3.503, 3.508, 3.515, 3.519, 3.521, 3.523, 3.524, 3.525, 3.526, 3.526, 3.527, 3.526, 3.524, 3.526, 3.527, 3.527
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 4000,
+                        "table":
+                        [
+                            2.032, 2.037, 2.039, 2.041, 2.041, 2.042, 2.043, 2.044, 2.045, 2.045, 2.044, 2.043, 2.042, 2.041, 2.041, 2.034,
+                            2.032, 2.036, 2.039, 2.041, 2.042, 2.042, 2.043, 2.044, 2.045, 2.046, 2.045, 2.044, 2.042, 2.041, 2.039, 2.035,
+                            2.032, 2.036, 2.038, 2.041, 2.043, 2.044, 2.044, 2.045, 2.046, 2.047, 2.047, 2.045, 2.043, 2.042, 2.041, 2.037,
+                            2.032, 2.035, 2.039, 2.042, 2.043, 2.044, 2.045, 2.046, 2.048, 2.048, 2.047, 2.046, 2.045, 2.044, 2.042, 2.039,
+                            2.031, 2.034, 2.037, 2.039, 2.043, 2.045, 2.045, 2.046, 2.047, 2.047, 2.047, 2.046, 2.045, 2.044, 2.043, 2.039,
+                            2.029, 2.033, 2.036, 2.039, 2.042, 2.043, 2.045, 2.046, 2.046, 2.046, 2.046, 2.046, 2.046, 2.045, 2.044, 2.041,
+                            2.028, 2.032, 2.035, 2.039, 2.041, 2.043, 2.044, 2.045, 2.045, 2.046, 2.046, 2.046, 2.046, 2.045, 2.044, 2.039,
+                            2.027, 2.032, 2.035, 2.038, 2.039, 2.041, 2.044, 2.044, 2.044, 2.045, 2.046, 2.046, 2.046, 2.045, 2.044, 2.039,
+                            2.027, 2.031, 2.034, 2.035, 2.037, 2.039, 2.042, 2.043, 2.044, 2.045, 2.045, 2.046, 2.045, 2.044, 2.043, 2.038,
+                            2.025, 2.028, 2.032, 2.034, 2.036, 2.037, 2.041, 2.042, 2.043, 2.044, 2.044, 2.044, 2.044, 2.043, 2.041, 2.036,
+                            2.024, 2.026, 2.029, 2.032, 2.034, 2.036, 2.038, 2.041, 2.041, 2.042, 2.043, 2.042, 2.041, 2.041, 2.037, 2.036,
+                            2.022, 2.024, 2.027, 2.029, 2.032, 2.034, 2.036, 2.039, 2.039, 2.039, 2.041, 2.039, 2.039, 2.038, 2.036, 2.034
+                        ]
+                    },
+                    {
+                        "ct": 6000,
+                        "table":
+                        [
+                            1.585, 1.587, 1.589, 1.589, 1.589, 1.591, 1.591, 1.591, 1.591, 1.591, 1.589, 1.589, 1.588, 1.588, 1.587, 1.581,
+                            1.585, 1.587, 1.588, 1.589, 1.591, 1.591, 1.591, 1.591, 1.591, 1.591, 1.591, 1.589, 1.588, 1.588, 1.587, 1.582,
+                            1.585, 1.586, 1.588, 1.589, 1.591, 1.591, 1.591, 1.591, 1.592, 1.592, 1.591, 1.591, 1.589, 1.588, 1.587, 1.584,
+                            1.585, 1.586, 1.588, 1.589, 1.591, 1.592, 1.592, 1.592, 1.593, 1.593, 1.592, 1.591, 1.589, 1.589, 1.588, 1.586,
+                            1.584, 1.586, 1.587, 1.589, 1.591, 1.591, 1.592, 1.592, 1.592, 1.592, 1.591, 1.591, 1.591, 1.589, 1.589, 1.586,
+                            1.583, 1.585, 1.587, 1.588, 1.589, 1.591, 1.591, 1.592, 1.592, 1.591, 1.591, 1.591, 1.591, 1.591, 1.589, 1.586,
+                            1.583, 1.584, 1.586, 1.588, 1.589, 1.589, 1.591, 1.591, 1.591, 1.591, 1.591, 1.591, 1.591, 1.591, 1.589, 1.585,
+                            1.581, 1.584, 1.586, 1.587, 1.588, 1.588, 1.589, 1.591, 1.591, 1.591, 1.591, 1.591, 1.591, 1.589, 1.589, 1.585,
+                            1.581, 1.583, 1.584, 1.586, 1.587, 1.588, 1.589, 1.589, 1.591, 1.591, 1.591, 1.591, 1.591, 1.589, 1.589, 1.585,
+                            1.579, 1.581, 1.583, 1.584, 1.586, 1.586, 1.588, 1.589, 1.589, 1.589, 1.589, 1.589, 1.589, 1.589, 1.587, 1.584,
+                            1.578, 1.579, 1.581, 1.583, 1.584, 1.585, 1.586, 1.587, 1.588, 1.588, 1.588, 1.588, 1.588, 1.587, 1.585, 1.583,
+                            1.577, 1.578, 1.579, 1.582, 1.583, 1.584, 1.585, 1.586, 1.586, 1.587, 1.587, 1.587, 1.586, 1.586, 1.584, 1.583
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    1.112, 1.098, 1.078, 1.062, 1.049, 1.039, 1.031, 1.027, 1.026, 1.027, 1.034, 1.043, 1.054, 1.069, 1.087, 1.096,
+                    1.106, 1.091, 1.073, 1.056, 1.042, 1.032, 1.025, 1.021, 1.021, 1.022, 1.027, 1.036, 1.047, 1.061, 1.077, 1.088,
+                    1.101, 1.085, 1.066, 1.049, 1.035, 1.026, 1.019, 1.013, 1.013, 1.015, 1.021, 1.028, 1.039, 1.052, 1.069, 1.083,
+                    1.098, 1.081, 1.059, 1.045, 1.031, 1.021, 1.013, 1.007, 1.007, 1.009, 1.014, 1.021, 1.033, 1.046, 1.063, 1.081,
+                    1.097, 1.076, 1.057, 1.041, 1.027, 1.016, 1.007, 1.004, 1.002, 1.005, 1.009, 1.017, 1.028, 1.043, 1.061, 1.077,
+                    1.096, 1.075, 1.054, 1.039, 1.025, 1.014, 1.005, 1.001, 1.001, 1.002, 1.006, 1.015, 1.027, 1.041, 1.058, 1.076,
+                    1.096, 1.074, 1.054, 1.039, 1.025, 1.013, 1.005, 1.001, 1.001, 1.001, 1.006, 1.015, 1.026, 1.041, 1.058, 1.076,
+                    1.096, 1.075, 1.056, 1.041, 1.026, 1.014, 1.007, 1.003, 1.002, 1.004, 1.008, 1.016, 1.028, 1.041, 1.059, 1.076,
+                    1.096, 1.079, 1.059, 1.044, 1.029, 1.018, 1.011, 1.007, 1.005, 1.008, 1.012, 1.019, 1.031, 1.044, 1.061, 1.077,
+                    1.101, 1.084, 1.065, 1.049, 1.035, 1.024, 1.017, 1.011, 1.011, 1.012, 1.018, 1.025, 1.036, 1.051, 1.068, 1.081,
+                    1.106, 1.092, 1.072, 1.055, 1.042, 1.033, 1.024, 1.019, 1.018, 1.019, 1.025, 1.032, 1.044, 1.058, 1.076, 1.088,
+                    1.113, 1.097, 1.079, 1.063, 1.049, 1.039, 1.031, 1.025, 1.025, 1.025, 1.031, 1.039, 1.051, 1.065, 1.083, 1.094
+                ],
+                "sigma": 0.00047,
+                "sigma_Cb": 0.00056
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2000,
+                        "ccm":
+                        [
+                            1.48716, -0.1877, -0.35079,
+                            -0.48577, 1.55088, -0.03387,
+                            0.24919, -1.4583, 2.12083
+                        ]
+                    },
+                    {
+                        "ct": 2200,
+                        "ccm":
+                        [
+                            1.53439, -0.28852, -0.29392,
+                            -0.44748, 1.56295, -0.08907,
+                            0.23529, -1.30488, 1.99784
+                        ]
+                    },
+                    {
+                        "ct": 2400,
+                        "ccm":
+                        [
+                            1.57619, -0.36904, -0.25181,
+                            -0.41654, 1.57046, -0.13192,
+                            0.21678, -1.18352, 1.90786
+                        ]
+                    },
+                    {
+                        "ct": 2600,
+                        "ccm":
+                        [
+                            1.61348, -0.43497, -0.2198,
+                            -0.39075, 1.5753, -0.1665,
+                            0.19789, -1.08592, 1.83942
+                        ]
+                    },
+                    {
+                        "ct": 2800,
+                        "ccm":
+                        [
+                            1.64717, -0.49009, -0.1951,
+                            -0.36881, 1.57852, -0.1952,
+                            0.18016, -1.00609, 1.78575
+                        ]
+                    },
+                    {
+                        "ct": 3000,
+                        "ccm":
+                        [
+                            1.67798, -0.53693, -0.17591,
+                            -0.34986, 1.58074, -0.21955,
+                            0.16406, -0.9398, 1.74261
+                        ]
+                    },
+                    {
+                        "ct": 3200,
+                        "ccm":
+                        [
+                            1.70647, -0.5773, -0.161,
+                            -0.33332, 1.58235, -0.24056,
+                            0.14961, -0.88398, 1.70721
+                        ]
+                    },
+                    {
+                        "ct": 3400,
+                        "ccm":
+                        [
+                            1.73305, -0.61248, -0.14951,
+                            -0.31875, 1.58355, -0.25894,
+                            0.13671, -0.83642, 1.67769
+                        ]
+                    },
+                    {
+                        "ct": 3600,
+                        "ccm":
+                        [
+                            1.75802, -0.64343, -0.14077,
+                            -0.30581, 1.5845, -0.27518,
+                            0.12518, -0.79546, 1.65271
+                        ]
+                    },
+                    {
+                        "ct": 4100,
+                        "ccm":
+                        [
+                            1.78116, -0.67459, -0.13048,
+                            -0.26859, 1.58692, -0.31929,
+                            0.11915, -0.77931, 1.64012
+                        ]
+                    },
+                    {
+                        "ct": 4600,
+                        "ccm":
+                        [
+                            1.83867, -0.73605, -0.12044,
+                            -0.24947, 1.58699, -0.34207,
+                            0.09949, -0.71041, 1.59842
+                        ]
+                    },
+                    {
+                        "ct": 5100,
+                        "ccm":
+                        [
+                            1.88967, -0.78455, -0.11744,
+                            -0.23398, 1.58806, -0.36172,
+                            0.08362, -0.6574, 1.56728
+                        ]
+                    },
+                    {
+                        "ct": 5600,
+                        "ccm":
+                        [
+                            1.93485, -0.82318, -0.1191,
+                            -0.22108, 1.58973, -0.37892,
+                            0.07074, -0.61609, 1.54362
+                        ]
+                    },
+                    {
+                        "ct": 6100,
+                        "ccm":
+                        [
+                            1.97481, -0.85423, -0.12371,
+                            -0.21015, 1.59169, -0.39406,
+                            0.06021, -0.58353, 1.52536
+                        ]
+                    },
+                    {
+                        "ct": 6600,
+                        "ccm":
+                        [
+                            2.01029, -0.87946, -0.13017,
+                            -0.20074, 1.59378, -0.4075,
+                            0.05146, -0.55732, 1.51096
+                        ]
+                    },
+                    {
+                        "ct": 7100,
+                        "ccm":
+                        [
+                            2.04183, -0.9002, -0.13765,
+                            -0.19255, 1.59586, -0.41944,
+                            0.04414, -0.53603, 1.49947
+                        ]
+                    },
+                    {
+                        "ct": 7600,
+                        "ccm":
+                        [
+                            2.07001, -0.91744, -0.14566,
+                            -0.18534, 1.59788, -0.43013,
+                            0.03791, -0.51841, 1.49013
+                        ]
+                    },
+                    {
+                        "ct": 8100,
+                        "ccm":
+                        [
+                            2.09534, -0.93195, -0.15388,
+                            -0.17893, 1.59981, -0.43974,
+                            0.03256, -0.50364, 1.48243
+                        ]
+                    },
+                    {
+                        "ct": 8600,
+                        "ccm":
+                        [
+                            2.11799, -0.94416, -0.16203,
+                            -0.17324, 1.60161, -0.44836,
+                            0.02795, -0.4912, 1.47604
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen":
+            {
+                "threshold": 0.1,
+                "strength": 1.0,
+                "limit": 0.18
+            }
+        }
+    ]
+}
diff --git a/src/ipa/rpi/vc4/data/imx296_mono.json b/src/ipa/rpi/vc4/data/imx296_mono.json
new file mode 100644
index 00000000..30965b4b
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx296_mono.json
@@ -0,0 +1,233 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 3840
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 19184,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 432,
+                "reference_Y": 13773
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.957
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 185,
+                "slope": 0.0105
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 6.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 6.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 0,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 4000,
+                        "table":
+                        [
+                            2.554, 2.554, 2.541, 2.534, 2.495, 2.506, 2.516, 2.517, 2.518, 2.515, 2.513, 2.495, 2.481, 2.533, 2.533, 2.521,
+                            2.522, 2.534, 2.539, 2.531, 2.531, 2.506, 2.506, 2.513, 2.513, 2.509, 2.498, 2.496, 2.508, 2.517, 2.521, 2.521,
+                            2.509, 2.517, 2.534, 2.529, 2.531, 2.521, 2.517, 2.517, 2.515, 2.514, 2.506, 2.499, 2.508, 2.508, 2.521, 2.537,
+                            2.507, 2.508, 2.517, 2.516, 2.495, 2.487, 2.519, 2.534, 2.535, 2.531, 2.499, 2.494, 2.501, 2.511, 2.526, 2.526,
+                            2.509, 2.517, 2.507, 2.501, 2.494, 2.519, 2.539, 2.539, 2.537, 2.537, 2.533, 2.499, 2.503, 2.511, 2.529, 2.525,
+                            2.521, 2.522, 2.476, 2.501, 2.501, 2.539, 2.546, 2.538, 2.531, 2.538, 2.541, 2.531, 2.529, 2.526, 2.529, 2.525,
+                            2.516, 2.519, 2.469, 2.499, 2.499, 2.543, 2.543, 2.531, 2.528, 2.534, 2.541, 2.535, 2.531, 2.526, 2.531, 2.528,
+                            2.509, 2.515, 2.465, 2.487, 2.487, 2.539, 2.543, 2.539, 2.533, 2.549, 2.542, 2.531, 2.529, 2.524, 2.532, 2.533,
+                            2.499, 2.499, 2.475, 2.482, 2.471, 2.509, 2.539, 2.544, 2.543, 2.545, 2.533, 2.498, 2.521, 2.521, 2.537, 2.536,
+                            2.499, 2.488, 2.488, 2.488, 2.471, 2.462, 2.509, 2.539, 2.539, 2.532, 2.498, 2.498, 2.518, 2.518, 2.539, 2.539,
+                            2.483, 2.484, 2.488, 2.488, 2.502, 2.496, 2.508, 2.514, 2.518, 2.517, 2.521, 2.518, 2.518, 2.518, 2.525, 2.539,
+                            2.483, 2.487, 2.478, 2.478, 2.507, 2.509, 2.514, 2.513, 2.514, 2.517, 2.536, 2.559, 2.501, 2.501, 2.503, 2.525
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 4000,
+                        "table":
+                        [
+                            2.619, 2.603, 2.599, 2.597, 2.595, 2.594, 2.589, 2.587, 2.586, 2.589, 2.592, 2.597, 2.601, 2.608, 2.621, 2.621,
+                            2.619, 2.615, 2.603, 2.601, 2.596, 2.595, 2.591, 2.589, 2.589, 2.592, 2.599, 2.593, 2.601, 2.613, 2.622, 2.631,
+                            2.617, 2.617, 2.612, 2.611, 2.604, 2.598, 2.593, 2.591, 2.592, 2.591, 2.593, 2.595, 2.599, 2.614, 2.623, 2.631,
+                            2.624, 2.619, 2.615, 2.612, 2.605, 2.602, 2.597, 2.596, 2.592, 2.592, 2.595, 2.599, 2.602, 2.606, 2.619, 2.624,
+                            2.629, 2.627, 2.627, 2.617, 2.609, 2.598, 2.612, 2.623, 2.615, 2.604, 2.589, 2.595, 2.599, 2.608, 2.611, 2.614,
+                            2.629, 2.632, 2.637, 2.627, 2.612, 2.612, 2.629, 2.631, 2.628, 2.621, 2.604, 2.597, 2.598, 2.604, 2.609, 2.609,
+                            2.635, 2.636, 2.642, 2.628, 2.623, 2.623, 2.636, 2.636, 2.634, 2.628, 2.616, 2.599, 2.597, 2.601, 2.603, 2.601,
+                            2.641, 2.639, 2.646, 2.632, 2.627, 2.625, 2.632, 2.635, 2.634, 2.627, 2.614, 2.596, 2.595, 2.599, 2.599, 2.598,
+                            2.643, 2.644, 2.651, 2.649, 2.629, 2.617, 2.624, 2.629, 2.625, 2.614, 2.586, 2.599, 2.595, 2.597, 2.592, 2.595,
+                            2.645, 2.646, 2.649, 2.649, 2.638, 2.624, 2.616, 2.617, 2.609, 2.604, 2.603, 2.603, 2.595, 2.589, 2.587, 2.592,
+                            2.641, 2.643, 2.649, 2.647, 2.638, 2.618, 2.615, 2.608, 2.602, 2.595, 2.596, 2.595, 2.593, 2.584, 2.581, 2.583,
+                            2.638, 2.637, 2.647, 2.634, 2.634, 2.618, 2.621, 2.621, 2.611, 2.602, 2.596, 2.583, 2.581, 2.581, 2.576, 2.574
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    1.308, 1.293, 1.228, 1.175, 1.139, 1.108, 1.092, 1.082, 1.082, 1.086, 1.097, 1.114, 1.149, 1.199, 1.279, 1.303,
+                    1.293, 1.249, 1.199, 1.162, 1.136, 1.109, 1.087, 1.077, 1.072, 1.081, 1.095, 1.103, 1.133, 1.172, 1.225, 1.282,
+                    1.251, 1.212, 1.186, 1.159, 1.129, 1.114, 1.102, 1.088, 1.088, 1.088, 1.095, 1.117, 1.123, 1.158, 1.198, 1.249,
+                    1.223, 1.192, 1.177, 1.163, 1.147, 1.139, 1.132, 1.112, 1.111, 1.107, 1.113, 1.118, 1.139, 1.155, 1.186, 1.232,
+                    1.207, 1.186, 1.171, 1.162, 1.168, 1.163, 1.153, 1.138, 1.129, 1.128, 1.132, 1.136, 1.149, 1.167, 1.189, 1.216,
+                    1.198, 1.186, 1.176, 1.176, 1.177, 1.185, 1.171, 1.157, 1.146, 1.144, 1.146, 1.149, 1.161, 1.181, 1.201, 1.221,
+                    1.203, 1.181, 1.176, 1.178, 1.191, 1.189, 1.188, 1.174, 1.159, 1.153, 1.158, 1.161, 1.169, 1.185, 1.211, 1.227,
+                    1.211, 1.179, 1.177, 1.187, 1.194, 1.196, 1.194, 1.187, 1.176, 1.169, 1.171, 1.171, 1.175, 1.189, 1.214, 1.226,
+                    1.219, 1.182, 1.184, 1.191, 1.195, 1.199, 1.197, 1.194, 1.188, 1.185, 1.179, 1.179, 1.182, 1.194, 1.212, 1.227,
+                    1.237, 1.192, 1.194, 1.194, 1.198, 1.199, 1.198, 1.197, 1.196, 1.193, 1.189, 1.189, 1.192, 1.203, 1.214, 1.231,
+                    1.282, 1.199, 1.199, 1.197, 1.199, 1.199, 1.192, 1.193, 1.193, 1.194, 1.196, 1.197, 1.206, 1.216, 1.228, 1.244,
+                    1.309, 1.236, 1.204, 1.203, 1.202, 1.194, 1.194, 1.188, 1.192, 1.192, 1.199, 1.201, 1.212, 1.221, 1.235, 1.247
+                ],
+                "sigma": 0.005,
+                "sigma_Cb": 0.005
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.sharpen":
+            {
+                "threshold": 0.1,
+                "strength": 1.0,
+                "limit": 0.18
+            }
+        }
+    ]
+}
diff --git a/src/ipa/rpi/vc4/data/imx378.json b/src/ipa/rpi/vc4/data/imx378.json
new file mode 100644
index 00000000..8b4ed225
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx378.json
@@ -0,0 +1,413 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 9999,
+                "reference_gain": 1.95,
+                "reference_aperture": 1.0,
+                "reference_lux": 1000,
+                "reference_Y": 12996
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.641
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 235,
+                "slope": 0.00902
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8100
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2850.0, 0.6361, 0.3911,
+                    3550.0, 0.5386, 0.5077,
+                    4500.0, 0.4472, 0.6171,
+                    5600.0, 0.3906, 0.6848,
+                    8000.0, 0.3412, 0.7441
+                ],
+                "sensitivity_r": 1.0,
+                "sensitivity_b": 1.0,
+                "transverse_pos": 0.01667,
+                "transverse_neg": 0.01195
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 2800,
+                        "table":
+                        [
+                            1.604, 1.601, 1.593, 1.581, 1.568, 1.561, 1.561, 1.561, 1.561, 1.567, 1.582, 1.596, 1.609, 1.622, 1.632, 1.636,
+                            1.601, 1.594, 1.586, 1.571, 1.555, 1.546, 1.543, 1.543, 1.547, 1.555, 1.572, 1.584, 1.599, 1.614, 1.625, 1.632,
+                            1.599, 1.586, 1.571, 1.555, 1.542, 1.528, 1.518, 1.518, 1.523, 1.537, 1.555, 1.572, 1.589, 1.607, 1.622, 1.629,
+                            1.597, 1.579, 1.561, 1.542, 1.528, 1.512, 1.493, 1.493, 1.499, 1.523, 1.537, 1.563, 1.582, 1.601, 1.619, 1.629,
+                            1.597, 1.577, 1.557, 1.535, 1.512, 1.493, 1.481, 1.479, 1.492, 1.499, 1.524, 1.555, 1.578, 1.599, 1.619, 1.629,
+                            1.597, 1.577, 1.557, 1.534, 1.508, 1.483, 1.476, 1.476, 1.481, 1.496, 1.522, 1.554, 1.578, 1.599, 1.619, 1.629,
+                            1.597, 1.578, 1.557, 1.534, 1.508, 1.483, 1.481, 1.479, 1.481, 1.496, 1.522, 1.554, 1.579, 1.601, 1.619, 1.631,
+                            1.597, 1.581, 1.562, 1.539, 1.517, 1.504, 1.483, 1.481, 1.496, 1.511, 1.531, 1.561, 1.585, 1.607, 1.623, 1.632,
+                            1.601, 1.589, 1.569, 1.554, 1.539, 1.517, 1.504, 1.504, 1.511, 1.531, 1.553, 1.573, 1.596, 1.614, 1.629, 1.636,
+                            1.609, 1.601, 1.586, 1.569, 1.554, 1.542, 1.535, 1.535, 1.541, 1.553, 1.573, 1.592, 1.608, 1.625, 1.637, 1.645,
+                            1.617, 1.611, 1.601, 1.586, 1.574, 1.565, 1.564, 1.564, 1.571, 1.579, 1.592, 1.608, 1.622, 1.637, 1.646, 1.654,
+                            1.619, 1.617, 1.611, 1.601, 1.588, 1.585, 1.585, 1.585, 1.588, 1.592, 1.607, 1.622, 1.637, 1.645, 1.654, 1.655
+                        ]
+                    },
+                    {
+                        "ct": 5500,
+                        "table":
+                        [
+                            2.664, 2.658, 2.645, 2.629, 2.602, 2.602, 2.602, 2.606, 2.617, 2.628, 2.649, 2.677, 2.699, 2.722, 2.736, 2.747,
+                            2.658, 2.653, 2.629, 2.605, 2.576, 2.575, 2.577, 2.592, 2.606, 2.618, 2.629, 2.651, 2.678, 2.707, 2.727, 2.741,
+                            2.649, 2.631, 2.605, 2.576, 2.563, 2.552, 2.552, 2.557, 2.577, 2.604, 2.619, 2.641, 2.669, 2.698, 2.721, 2.741,
+                            2.643, 2.613, 2.583, 2.563, 2.552, 2.531, 2.527, 2.527, 2.551, 2.577, 2.604, 2.638, 2.665, 2.694, 2.721, 2.741,
+                            2.643, 2.606, 2.575, 2.558, 2.531, 2.516, 2.504, 2.516, 2.527, 2.551, 2.596, 2.635, 2.665, 2.694, 2.721, 2.741,
+                            2.643, 2.606, 2.575, 2.558, 2.531, 2.503, 2.501, 2.502, 2.522, 2.551, 2.592, 2.635, 2.669, 2.696, 2.727, 2.744,
+                            2.648, 2.611, 2.579, 2.558, 2.532, 2.511, 2.502, 2.511, 2.522, 2.552, 2.592, 2.642, 2.673, 2.702, 2.731, 2.752,
+                            2.648, 2.619, 2.589, 2.571, 2.556, 2.532, 2.519, 2.522, 2.552, 2.568, 2.605, 2.648, 2.683, 2.715, 2.743, 2.758,
+                            2.659, 2.637, 2.613, 2.589, 2.571, 2.556, 2.555, 2.555, 2.568, 2.605, 2.641, 2.671, 2.699, 2.729, 2.758, 2.776,
+                            2.679, 2.665, 2.637, 2.613, 2.602, 2.599, 2.599, 2.606, 2.619, 2.641, 2.671, 2.698, 2.723, 2.754, 2.776, 2.787,
+                            2.695, 2.684, 2.671, 2.646, 2.636, 2.636, 2.641, 2.648, 2.661, 2.681, 2.698, 2.723, 2.751, 2.776, 2.788, 2.803,
+                            2.702, 2.699, 2.684, 2.671, 2.664, 2.664, 2.664, 2.668, 2.681, 2.698, 2.723, 2.751, 2.773, 2.788, 2.803, 2.805
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 2800,
+                        "table":
+                        [
+                            2.876, 2.868, 2.863, 2.851, 2.846, 2.846, 2.847, 2.851, 2.851, 2.857, 2.867, 2.875, 2.889, 2.899, 2.913, 2.926,
+                            2.863, 2.861, 2.856, 2.846, 2.846, 2.847, 2.848, 2.851, 2.857, 2.859, 2.875, 2.882, 2.886, 2.896, 2.909, 2.917,
+                            2.861, 2.856, 2.846, 2.841, 2.841, 2.855, 2.867, 2.875, 2.888, 2.888, 2.885, 2.883, 2.886, 2.889, 2.901, 2.913,
+                            2.858, 2.851, 2.846, 2.846, 2.855, 2.867, 2.884, 2.895, 2.902, 2.902, 2.901, 2.891, 2.891, 2.894, 2.901, 2.909,
+                            2.858, 2.851, 2.846, 2.846, 2.867, 2.884, 2.895, 2.902, 2.909, 2.915, 2.911, 2.901, 2.895, 2.898, 2.904, 2.909,
+                            2.858, 2.851, 2.849, 2.853, 2.874, 2.888, 2.901, 2.909, 2.917, 2.922, 2.917, 2.911, 2.901, 2.899, 2.905, 2.908,
+                            2.861, 2.855, 2.853, 2.855, 2.874, 2.888, 2.901, 2.913, 2.918, 2.922, 2.921, 2.911, 2.901, 2.901, 2.907, 2.908,
+                            2.862, 2.859, 2.855, 2.856, 2.872, 2.885, 2.899, 2.906, 2.915, 2.917, 2.911, 2.907, 2.907, 2.907, 2.908, 2.909,
+                            2.863, 2.863, 2.859, 2.864, 2.871, 2.881, 2.885, 2.899, 2.905, 2.905, 2.904, 2.904, 2.907, 2.909, 2.913, 2.913,
+                            2.866, 2.865, 2.865, 2.867, 2.868, 2.872, 2.881, 2.885, 2.889, 2.894, 2.895, 2.902, 2.906, 2.913, 2.914, 2.917,
+                            2.875, 2.875, 2.871, 2.871, 2.871, 2.871, 2.869, 2.869, 2.878, 2.889, 2.894, 2.895, 2.906, 2.914, 2.917, 2.921,
+                            2.882, 2.879, 2.876, 2.874, 2.871, 2.871, 2.869, 2.869, 2.869, 2.878, 2.891, 2.894, 2.905, 2.914, 2.919, 2.921
+                        ]
+                    },
+                    {
+                        "ct": 5500,
+                        "table":
+                        [
+                            1.488, 1.488, 1.488, 1.488, 1.491, 1.492, 1.492, 1.491, 1.491, 1.491, 1.492, 1.495, 1.497, 1.499, 1.499, 1.503,
+                            1.482, 1.485, 1.485, 1.487, 1.489, 1.492, 1.492, 1.492, 1.492, 1.492, 1.494, 1.494, 1.492, 1.491, 1.493, 1.494,
+                            1.482, 1.482, 1.484, 1.485, 1.487, 1.492, 1.496, 1.498, 1.499, 1.498, 1.494, 1.492, 1.491, 1.491, 1.491, 1.491,
+                            1.481, 1.481, 1.482, 1.485, 1.491, 1.496, 1.498, 1.499, 1.501, 1.499, 1.498, 1.493, 1.491, 1.488, 1.488, 1.488,
+                            1.481, 1.481, 1.481, 1.483, 1.491, 1.497, 1.498, 1.499, 1.501, 1.499, 1.498, 1.492, 1.488, 1.485, 1.483, 1.483,
+                            1.479, 1.479, 1.481, 1.482, 1.489, 1.495, 1.497, 1.498, 1.499, 1.499, 1.495, 1.492, 1.485, 1.482, 1.482, 1.481,
+                            1.479, 1.479, 1.479, 1.481, 1.489, 1.494, 1.496, 1.497, 1.497, 1.496, 1.495, 1.489, 1.482, 1.481, 1.479, 1.477,
+                            1.478, 1.478, 1.479, 1.481, 1.487, 1.491, 1.494, 1.496, 1.496, 1.495, 1.492, 1.487, 1.482, 1.479, 1.478, 1.476,
+                            1.478, 1.478, 1.479, 1.482, 1.486, 1.488, 1.491, 1.493, 1.493, 1.492, 1.487, 1.484, 1.481, 1.479, 1.476, 1.476,
+                            1.477, 1.479, 1.481, 1.483, 1.485, 1.486, 1.488, 1.488, 1.487, 1.487, 1.484, 1.483, 1.481, 1.479, 1.476, 1.476,
+                            1.477, 1.479, 1.482, 1.483, 1.484, 1.485, 1.484, 1.482, 1.482, 1.484, 1.483, 1.482, 1.481, 1.479, 1.477, 1.476,
+                            1.477, 1.479, 1.482, 1.483, 1.484, 1.484, 1.482, 1.482, 1.482, 1.482, 1.482, 1.481, 1.479, 1.479, 1.479, 1.479
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    2.764, 2.654, 2.321, 2.043, 1.768, 1.594, 1.558, 1.558, 1.558, 1.568, 1.661, 1.904, 2.193, 2.497, 2.888, 3.043,
+                    2.654, 2.373, 2.049, 1.819, 1.569, 1.446, 1.381, 1.356, 1.356, 1.403, 1.501, 1.679, 1.939, 2.218, 2.586, 2.888,
+                    2.376, 2.154, 1.819, 1.569, 1.438, 1.301, 1.246, 1.224, 1.224, 1.263, 1.349, 1.501, 1.679, 1.985, 2.359, 2.609,
+                    2.267, 1.987, 1.662, 1.438, 1.301, 1.235, 1.132, 1.105, 1.105, 1.164, 1.263, 1.349, 1.528, 1.808, 2.184, 2.491,
+                    2.218, 1.876, 1.568, 1.367, 1.235, 1.132, 1.087, 1.022, 1.023, 1.104, 1.164, 1.278, 1.439, 1.695, 2.066, 2.429,
+                    2.218, 1.832, 1.533, 1.341, 1.206, 1.089, 1.013, 1.002, 1.013, 1.026, 1.122, 1.246, 1.399, 1.642, 2.004, 2.426,
+                    2.218, 1.832, 1.533, 1.341, 1.206, 1.089, 1.011, 1.001, 1.009, 1.026, 1.122, 1.246, 1.399, 1.642, 2.004, 2.426,
+                    2.224, 1.896, 1.584, 1.382, 1.248, 1.147, 1.088, 1.016, 1.026, 1.118, 1.168, 1.283, 1.444, 1.697, 2.066, 2.428,
+                    2.292, 2.019, 1.689, 1.462, 1.322, 1.247, 1.147, 1.118, 1.118, 1.168, 1.275, 1.358, 1.532, 1.809, 2.189, 2.491,
+                    2.444, 2.204, 1.856, 1.606, 1.462, 1.322, 1.257, 1.234, 1.234, 1.275, 1.358, 1.516, 1.686, 1.993, 2.371, 2.622,
+                    2.748, 2.444, 2.108, 1.856, 1.606, 1.476, 1.399, 1.376, 1.376, 1.422, 1.516, 1.686, 1.968, 2.238, 2.611, 2.935,
+                    2.862, 2.748, 2.395, 2.099, 1.811, 1.621, 1.582, 1.582, 1.582, 1.592, 1.677, 1.919, 2.223, 2.534, 2.935, 3.078
+                ],
+                "sigma": 0.00428,
+                "sigma_Cb": 0.00363
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2850,
+                        "ccm":
+                        [
+                            1.42601, -0.20537, -0.22063,
+                            -0.47682, 1.81987, -0.34305,
+                            0.01854, -0.86036, 1.84181
+                        ]
+                    },
+                    {
+                        "ct": 2900,
+                        "ccm":
+                        [
+                            1.29755, 0.04602, -0.34356,
+                            -0.41491, 1.73477, -0.31987,
+                            -0.01345, -0.97115, 1.98459
+                        ]
+                    },
+                    {
+                        "ct": 3550,
+                        "ccm":
+                        [
+                            1.49811, -0.33412, -0.16398,
+                            -0.40869, 1.72995, -0.32127,
+                            -0.01924, -0.62181, 1.64105
+                        ]
+                    },
+                    {
+                        "ct": 4500,
+                        "ccm":
+                        [
+                            1.47015, -0.29229, -0.17786,
+                            -0.36561, 1.88919, -0.52358,
+                            -0.03552, -0.56717, 1.60269
+                        ]
+                    },
+                    {
+                        "ct": 5600,
+                        "ccm":
+                        [
+                            1.60962, -0.47434, -0.13528,
+                            -0.32701, 1.73797, -0.41096,
+                            -0.07626, -0.40171, 1.47796
+                        ]
+                    },
+                    {
+                        "ct": 8000,
+                        "ccm":
+                        [
+                            1.54642, -0.20396, -0.34246,
+                            -0.31748, 2.22559, -0.90811,
+                            -0.10035, -0.65877, 1.75912
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx477.json b/src/ipa/rpi/vc4/data/imx477.json
new file mode 100644
index 00000000..daffc268
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx477.json
@@ -0,0 +1,518 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 27242,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 830,
+                "reference_Y": 17755
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.767
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 204,
+                "slope": 0.01078
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2360.0, 0.6009, 0.3093,
+                    2848.0, 0.5071, 0.4000,
+                    2903.0, 0.4905, 0.4392,
+                    3628.0, 0.4261, 0.5564,
+                    3643.0, 0.4228, 0.5623,
+                    4660.0, 0.3529, 0.6800,
+                    5579.0, 0.3227, 0.7000,
+                    6125.0, 0.3129, 0.7100,
+                    6671.0, 0.3065, 0.7200,
+                    7217.0, 0.3014, 0.7300,
+                    7763.0, 0.2950, 0.7400,
+                    9505.0, 0.2524, 0.7856
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.0238,
+                "transverse_neg": 0.04429
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 66666 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 33333 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.3,
+                                1000, 0.3
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.3,
+                                1000, 0.3
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ],
+                    "shadows": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.5,
+                            "y_target":
+                            [
+                                0, 0.17,
+                                1000, 0.17
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 2960,
+                        "table":
+                        [
+                            2.088, 2.086, 2.082, 2.081, 2.077, 2.071, 2.068, 2.068, 2.072, 2.073, 2.075, 2.078, 2.084, 2.092, 2.095, 2.098,
+                            2.086, 2.084, 2.079, 2.078, 2.075, 2.068, 2.064, 2.063, 2.068, 2.071, 2.072, 2.075, 2.081, 2.089, 2.092, 2.094,
+                            2.083, 2.081, 2.077, 2.072, 2.069, 2.062, 2.059, 2.059, 2.063, 2.067, 2.069, 2.072, 2.079, 2.088, 2.089, 2.089,
+                            2.081, 2.077, 2.072, 2.068, 2.065, 2.058, 2.055, 2.054, 2.057, 2.062, 2.066, 2.069, 2.077, 2.084, 2.086, 2.086,
+                            2.078, 2.075, 2.069, 2.065, 2.061, 2.055, 2.052, 2.049, 2.051, 2.056, 2.062, 2.065, 2.072, 2.079, 2.081, 2.079,
+                            2.079, 2.075, 2.069, 2.064, 2.061, 2.053, 2.049, 2.046, 2.049, 2.051, 2.057, 2.062, 2.069, 2.075, 2.077, 2.075,
+                            2.082, 2.079, 2.072, 2.065, 2.061, 2.054, 2.049, 2.047, 2.049, 2.051, 2.056, 2.061, 2.066, 2.073, 2.073, 2.069,
+                            2.086, 2.082, 2.075, 2.068, 2.062, 2.054, 2.051, 2.049, 2.051, 2.052, 2.056, 2.061, 2.066, 2.073, 2.073, 2.072,
+                            2.088, 2.086, 2.079, 2.074, 2.066, 2.057, 2.051, 2.051, 2.054, 2.055, 2.056, 2.061, 2.067, 2.072, 2.073, 2.072,
+                            2.091, 2.087, 2.079, 2.075, 2.068, 2.057, 2.052, 2.052, 2.056, 2.055, 2.055, 2.059, 2.066, 2.072, 2.072, 2.072,
+                            2.093, 2.088, 2.081, 2.077, 2.069, 2.059, 2.054, 2.054, 2.057, 2.056, 2.056, 2.058, 2.066, 2.072, 2.073, 2.073,
+                            2.095, 2.091, 2.084, 2.078, 2.075, 2.067, 2.057, 2.057, 2.059, 2.059, 2.058, 2.059, 2.068, 2.073, 2.075, 2.078
+                        ]
+                    },
+                    {
+                        "ct": 4850,
+                        "table":
+                        [
+                            2.973, 2.968, 2.956, 2.943, 2.941, 2.932, 2.923, 2.921, 2.924, 2.929, 2.931, 2.939, 2.953, 2.965, 2.966, 2.976,
+                            2.969, 2.962, 2.951, 2.941, 2.934, 2.928, 2.919, 2.918, 2.919, 2.923, 2.927, 2.933, 2.945, 2.957, 2.962, 2.962,
+                            2.964, 2.956, 2.944, 2.932, 2.929, 2.924, 2.915, 2.914, 2.915, 2.919, 2.924, 2.928, 2.941, 2.952, 2.958, 2.959,
+                            2.957, 2.951, 2.939, 2.928, 2.924, 2.919, 2.913, 2.911, 2.911, 2.915, 2.919, 2.925, 2.936, 2.947, 2.952, 2.953,
+                            2.954, 2.947, 2.935, 2.924, 2.919, 2.915, 2.908, 2.906, 2.906, 2.907, 2.914, 2.921, 2.932, 2.941, 2.943, 2.942,
+                            2.953, 2.946, 2.932, 2.921, 2.916, 2.911, 2.904, 2.902, 2.901, 2.904, 2.909, 2.919, 2.926, 2.937, 2.939, 2.939,
+                            2.953, 2.947, 2.932, 2.918, 2.915, 2.909, 2.903, 2.901, 2.901, 2.906, 2.911, 2.918, 2.924, 2.936, 2.936, 2.932,
+                            2.956, 2.948, 2.934, 2.919, 2.916, 2.908, 2.903, 2.901, 2.902, 2.907, 2.909, 2.917, 2.926, 2.936, 2.939, 2.939,
+                            2.957, 2.951, 2.936, 2.923, 2.917, 2.907, 2.904, 2.901, 2.902, 2.908, 2.911, 2.919, 2.929, 2.939, 2.942, 2.942,
+                            2.961, 2.951, 2.936, 2.922, 2.918, 2.906, 2.904, 2.901, 2.901, 2.907, 2.911, 2.921, 2.931, 2.941, 2.942, 2.944,
+                            2.964, 2.954, 2.936, 2.924, 2.918, 2.909, 2.905, 2.905, 2.905, 2.907, 2.912, 2.923, 2.933, 2.942, 2.944, 2.944,
+                            2.964, 2.958, 2.943, 2.927, 2.921, 2.914, 2.909, 2.907, 2.907, 2.912, 2.916, 2.928, 2.936, 2.944, 2.947, 2.952
+                        ]
+                    },
+                    {
+                        "ct": 5930,
+                        "table":
+                        [
+                            3.312, 3.308, 3.301, 3.294, 3.288, 3.277, 3.268, 3.261, 3.259, 3.261, 3.267, 3.273, 3.285, 3.301, 3.303, 3.312,
+                            3.308, 3.304, 3.294, 3.291, 3.283, 3.271, 3.263, 3.259, 3.257, 3.258, 3.261, 3.268, 3.278, 3.293, 3.299, 3.299,
+                            3.302, 3.296, 3.288, 3.282, 3.276, 3.267, 3.259, 3.254, 3.252, 3.253, 3.256, 3.261, 3.273, 3.289, 3.292, 3.292,
+                            3.296, 3.289, 3.282, 3.276, 3.269, 3.263, 3.256, 3.251, 3.248, 3.249, 3.251, 3.257, 3.268, 3.279, 3.284, 3.284,
+                            3.292, 3.285, 3.279, 3.271, 3.264, 3.257, 3.249, 3.243, 3.241, 3.241, 3.246, 3.252, 3.261, 3.274, 3.275, 3.273,
+                            3.291, 3.285, 3.276, 3.268, 3.259, 3.251, 3.242, 3.239, 3.236, 3.238, 3.244, 3.248, 3.258, 3.268, 3.269, 3.265,
+                            3.294, 3.288, 3.275, 3.266, 3.257, 3.248, 3.239, 3.238, 3.237, 3.238, 3.243, 3.246, 3.255, 3.264, 3.264, 3.257,
+                            3.297, 3.293, 3.279, 3.268, 3.258, 3.249, 3.238, 3.237, 3.239, 3.239, 3.243, 3.245, 3.255, 3.264, 3.264, 3.263,
+                            3.301, 3.295, 3.281, 3.271, 3.259, 3.248, 3.237, 3.237, 3.239, 3.241, 3.243, 3.246, 3.257, 3.265, 3.266, 3.264,
+                            3.306, 3.295, 3.279, 3.271, 3.261, 3.247, 3.235, 3.234, 3.239, 3.239, 3.243, 3.247, 3.258, 3.265, 3.265, 3.264,
+                            3.308, 3.297, 3.279, 3.272, 3.261, 3.249, 3.239, 3.239, 3.241, 3.243, 3.245, 3.248, 3.261, 3.265, 3.266, 3.265,
+                            3.309, 3.301, 3.286, 3.276, 3.267, 3.256, 3.246, 3.242, 3.244, 3.244, 3.249, 3.253, 3.263, 3.267, 3.271, 3.274
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 2960,
+                        "table":
+                        [
+                            2.133, 2.134, 2.139, 2.143, 2.148, 2.155, 2.158, 2.158, 2.158, 2.161, 2.161, 2.162, 2.159, 2.156, 2.152, 2.151,
+                            2.132, 2.133, 2.135, 2.142, 2.147, 2.153, 2.158, 2.158, 2.158, 2.158, 2.159, 2.159, 2.157, 2.154, 2.151, 2.148,
+                            2.133, 2.133, 2.135, 2.142, 2.149, 2.154, 2.158, 2.158, 2.157, 2.156, 2.158, 2.157, 2.155, 2.153, 2.148, 2.146,
+                            2.133, 2.133, 2.138, 2.145, 2.149, 2.154, 2.158, 2.159, 2.158, 2.155, 2.157, 2.156, 2.153, 2.149, 2.146, 2.144,
+                            2.133, 2.134, 2.139, 2.146, 2.149, 2.154, 2.158, 2.159, 2.159, 2.156, 2.154, 2.154, 2.149, 2.145, 2.143, 2.139,
+                            2.135, 2.135, 2.139, 2.146, 2.151, 2.155, 2.158, 2.159, 2.158, 2.156, 2.153, 2.151, 2.146, 2.143, 2.139, 2.136,
+                            2.135, 2.135, 2.138, 2.145, 2.151, 2.154, 2.157, 2.158, 2.157, 2.156, 2.153, 2.151, 2.147, 2.143, 2.141, 2.137,
+                            2.135, 2.134, 2.135, 2.141, 2.149, 2.154, 2.157, 2.157, 2.157, 2.157, 2.157, 2.153, 2.149, 2.146, 2.142, 2.139,
+                            2.132, 2.133, 2.135, 2.139, 2.148, 2.153, 2.158, 2.159, 2.159, 2.161, 2.161, 2.157, 2.154, 2.149, 2.144, 2.141,
+                            2.132, 2.133, 2.135, 2.141, 2.149, 2.155, 2.161, 2.161, 2.162, 2.162, 2.163, 2.159, 2.154, 2.149, 2.144, 2.138,
+                            2.136, 2.136, 2.137, 2.143, 2.149, 2.156, 2.162, 2.163, 2.162, 2.163, 2.164, 2.161, 2.157, 2.152, 2.146, 2.138,
+                            2.137, 2.137, 2.141, 2.147, 2.152, 2.157, 2.162, 2.162, 2.159, 2.161, 2.162, 2.162, 2.157, 2.152, 2.148, 2.148
+                        ]
+                    },
+                    {
+                        "ct": 4850,
+                        "table":
+                        [
+                            1.463, 1.464, 1.471, 1.478, 1.479, 1.483, 1.484, 1.486, 1.486, 1.484, 1.483, 1.481, 1.478, 1.475, 1.471, 1.468,
+                            1.463, 1.463, 1.468, 1.476, 1.479, 1.482, 1.484, 1.487, 1.486, 1.484, 1.483, 1.482, 1.478, 1.473, 1.469, 1.468,
+                            1.463, 1.464, 1.468, 1.476, 1.479, 1.483, 1.484, 1.486, 1.486, 1.485, 1.484, 1.482, 1.477, 1.473, 1.469, 1.468,
+                            1.463, 1.464, 1.469, 1.477, 1.481, 1.483, 1.485, 1.487, 1.487, 1.485, 1.485, 1.482, 1.478, 1.474, 1.469, 1.468,
+                            1.465, 1.465, 1.471, 1.478, 1.481, 1.484, 1.486, 1.488, 1.488, 1.487, 1.485, 1.482, 1.477, 1.472, 1.468, 1.467,
+                            1.465, 1.466, 1.472, 1.479, 1.482, 1.485, 1.486, 1.488, 1.488, 1.486, 1.484, 1.479, 1.475, 1.472, 1.468, 1.466,
+                            1.466, 1.466, 1.472, 1.478, 1.482, 1.484, 1.485, 1.488, 1.487, 1.485, 1.483, 1.479, 1.475, 1.472, 1.469, 1.468,
+                            1.465, 1.466, 1.469, 1.476, 1.481, 1.485, 1.485, 1.486, 1.486, 1.485, 1.483, 1.479, 1.477, 1.474, 1.471, 1.469,
+                            1.464, 1.465, 1.469, 1.476, 1.481, 1.484, 1.485, 1.487, 1.487, 1.486, 1.485, 1.481, 1.478, 1.475, 1.471, 1.469,
+                            1.463, 1.464, 1.469, 1.477, 1.481, 1.485, 1.485, 1.488, 1.488, 1.487, 1.486, 1.481, 1.478, 1.475, 1.471, 1.468,
+                            1.464, 1.465, 1.471, 1.478, 1.482, 1.486, 1.486, 1.488, 1.488, 1.487, 1.486, 1.481, 1.478, 1.475, 1.472, 1.468,
+                            1.465, 1.466, 1.472, 1.481, 1.483, 1.487, 1.487, 1.488, 1.488, 1.486, 1.485, 1.481, 1.479, 1.476, 1.473, 1.472
+                        ]
+                    },
+                    {
+                        "ct": 5930,
+                        "table":
+                        [
+                            1.443, 1.444, 1.448, 1.453, 1.459, 1.463, 1.465, 1.467, 1.469, 1.469, 1.467, 1.466, 1.462, 1.457, 1.454, 1.451,
+                            1.443, 1.444, 1.445, 1.451, 1.459, 1.463, 1.465, 1.467, 1.469, 1.469, 1.467, 1.465, 1.461, 1.456, 1.452, 1.451,
+                            1.444, 1.444, 1.445, 1.451, 1.459, 1.463, 1.466, 1.468, 1.469, 1.469, 1.467, 1.465, 1.461, 1.456, 1.452, 1.449,
+                            1.444, 1.444, 1.447, 1.452, 1.459, 1.464, 1.467, 1.469, 1.471, 1.469, 1.467, 1.466, 1.461, 1.456, 1.452, 1.449,
+                            1.444, 1.445, 1.448, 1.452, 1.459, 1.465, 1.469, 1.471, 1.471, 1.471, 1.468, 1.465, 1.461, 1.455, 1.451, 1.449,
+                            1.445, 1.446, 1.449, 1.453, 1.461, 1.466, 1.469, 1.471, 1.472, 1.469, 1.467, 1.465, 1.459, 1.455, 1.451, 1.447,
+                            1.446, 1.446, 1.449, 1.453, 1.461, 1.466, 1.469, 1.469, 1.469, 1.469, 1.467, 1.465, 1.459, 1.455, 1.452, 1.449,
+                            1.446, 1.446, 1.447, 1.451, 1.459, 1.466, 1.469, 1.469, 1.469, 1.469, 1.467, 1.465, 1.461, 1.457, 1.454, 1.451,
+                            1.444, 1.444, 1.447, 1.451, 1.459, 1.466, 1.469, 1.469, 1.471, 1.471, 1.468, 1.466, 1.462, 1.458, 1.454, 1.452,
+                            1.444, 1.444, 1.448, 1.453, 1.459, 1.466, 1.469, 1.471, 1.472, 1.472, 1.468, 1.466, 1.462, 1.458, 1.454, 1.449,
+                            1.446, 1.447, 1.449, 1.454, 1.461, 1.466, 1.471, 1.471, 1.471, 1.471, 1.468, 1.466, 1.462, 1.459, 1.455, 1.449,
+                            1.447, 1.447, 1.452, 1.457, 1.462, 1.468, 1.472, 1.472, 1.471, 1.471, 1.468, 1.466, 1.462, 1.459, 1.456, 1.455
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    1.548, 1.499, 1.387, 1.289, 1.223, 1.183, 1.164, 1.154, 1.153, 1.169, 1.211, 1.265, 1.345, 1.448, 1.581, 1.619,
+                    1.513, 1.412, 1.307, 1.228, 1.169, 1.129, 1.105, 1.098, 1.103, 1.127, 1.157, 1.209, 1.272, 1.361, 1.481, 1.583,
+                    1.449, 1.365, 1.257, 1.175, 1.124, 1.085, 1.062, 1.054, 1.059, 1.079, 1.113, 1.151, 1.211, 1.293, 1.407, 1.488,
+                    1.424, 1.324, 1.222, 1.139, 1.089, 1.056, 1.034, 1.031, 1.034, 1.049, 1.075, 1.115, 1.164, 1.241, 1.351, 1.446,
+                    1.412, 1.297, 1.203, 1.119, 1.069, 1.039, 1.021, 1.016, 1.022, 1.032, 1.052, 1.086, 1.135, 1.212, 1.321, 1.439,
+                    1.406, 1.287, 1.195, 1.115, 1.059, 1.028, 1.014, 1.012, 1.015, 1.026, 1.041, 1.074, 1.125, 1.201, 1.302, 1.425,
+                    1.406, 1.294, 1.205, 1.126, 1.062, 1.031, 1.013, 1.009, 1.011, 1.019, 1.042, 1.079, 1.129, 1.203, 1.302, 1.435,
+                    1.415, 1.318, 1.229, 1.146, 1.076, 1.039, 1.019, 1.014, 1.017, 1.031, 1.053, 1.093, 1.144, 1.219, 1.314, 1.436,
+                    1.435, 1.348, 1.246, 1.164, 1.094, 1.059, 1.036, 1.032, 1.037, 1.049, 1.072, 1.114, 1.167, 1.257, 1.343, 1.462,
+                    1.471, 1.385, 1.278, 1.189, 1.124, 1.084, 1.064, 1.061, 1.069, 1.078, 1.101, 1.146, 1.207, 1.298, 1.415, 1.496,
+                    1.522, 1.436, 1.323, 1.228, 1.169, 1.118, 1.101, 1.094, 1.099, 1.113, 1.146, 1.194, 1.265, 1.353, 1.474, 1.571,
+                    1.578, 1.506, 1.378, 1.281, 1.211, 1.156, 1.135, 1.134, 1.139, 1.158, 1.194, 1.251, 1.327, 1.427, 1.559, 1.611
+                ],
+                "sigma": 0.00121,
+                "sigma_Cb": 0.00115
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2360,
+                        "ccm":
+                        [
+                            1.66078, -0.23588, -0.42491,
+                            -0.47456, 1.82763, -0.35307,
+                            -0.00545, -1.44729, 2.45273
+                        ]
+                    },
+                    {
+                        "ct": 2870,
+                        "ccm":
+                        [
+                            1.78373, -0.55344, -0.23029,
+                            -0.39951, 1.69701, -0.29751,
+                            0.01986, -1.06525, 2.04539
+                        ]
+                    },
+                    {
+                        "ct": 2970,
+                        "ccm":
+                        [
+                            1.73511, -0.56973, -0.16537,
+                            -0.36338, 1.69878, -0.33539,
+                            -0.02354, -0.76813, 1.79168
+                        ]
+                    },
+                    {
+                        "ct": 3000,
+                        "ccm":
+                        [
+                            2.06374, -0.92218, -0.14156,
+                            -0.41721, 1.69289, -0.27568,
+                            -0.00554, -0.92741, 1.93295
+                        ]
+                    },
+                    {
+                        "ct": 3700,
+                        "ccm":
+                        [
+                            2.13792, -1.08136, -0.05655,
+                            -0.34739, 1.58989, -0.24249,
+                            -0.00349, -0.76789, 1.77138
+                        ]
+                    },
+                    {
+                        "ct": 3870,
+                        "ccm":
+                        [
+                            1.83834, -0.70528, -0.13307,
+                            -0.30499, 1.60523, -0.30024,
+                            -0.05701, -0.58313, 1.64014
+                        ]
+                    },
+                    {
+                        "ct": 4000,
+                        "ccm":
+                        [
+                            2.15741, -1.10295, -0.05447,
+                            -0.34631, 1.61158, -0.26528,
+                            -0.02723, -0.70288, 1.73011
+                        ]
+                    },
+                    {
+                        "ct": 4400,
+                        "ccm":
+                        [
+                            2.05729, -0.95007, -0.10723,
+                            -0.41712, 1.78606, -0.36894,
+                            -0.11899, -0.55727, 1.67626
+                        ]
+                    },
+                    {
+                        "ct": 4715,
+                        "ccm":
+                        [
+                            1.90255, -0.77478, -0.12777,
+                            -0.31338, 1.88197, -0.56858,
+                            -0.06001, -0.61785, 1.67786
+                        ]
+                    },
+                    {
+                        "ct": 5920,
+                        "ccm":
+                        [
+                            1.98691, -0.84671, -0.14019,
+                            -0.26581, 1.70615, -0.44035,
+                            -0.09532, -0.47332, 1.56864
+                        ]
+                    },
+                    {
+                        "ct": 9050,
+                        "ccm":
+                        [
+                            2.09255, -0.76541, -0.32714,
+                            -0.28973, 2.27462, -0.98489,
+                            -0.17299, -0.61275, 1.78574
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx477_noir.json b/src/ipa/rpi/vc4/data/imx477_noir.json
new file mode 100644
index 00000000..52d7f072
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx477_noir.json
@@ -0,0 +1,429 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 27242,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 830,
+                "reference_Y": 17755
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.767
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 204,
+                "slope": 0.01078
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "bayes": 0
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 66666 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 33333 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.3,
+                                1000, 0.3
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.3,
+                                1000, 0.3
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ],
+                    "shadows": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.5,
+                            "y_target":
+                            [
+                                0, 0.17,
+                                1000, 0.17
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 2960,
+                        "table":
+                        [
+                            2.088, 2.086, 2.082, 2.081, 2.077, 2.071, 2.068, 2.068, 2.072, 2.073, 2.075, 2.078, 2.084, 2.092, 2.095, 2.098,
+                            2.086, 2.084, 2.079, 2.078, 2.075, 2.068, 2.064, 2.063, 2.068, 2.071, 2.072, 2.075, 2.081, 2.089, 2.092, 2.094,
+                            2.083, 2.081, 2.077, 2.072, 2.069, 2.062, 2.059, 2.059, 2.063, 2.067, 2.069, 2.072, 2.079, 2.088, 2.089, 2.089,
+                            2.081, 2.077, 2.072, 2.068, 2.065, 2.058, 2.055, 2.054, 2.057, 2.062, 2.066, 2.069, 2.077, 2.084, 2.086, 2.086,
+                            2.078, 2.075, 2.069, 2.065, 2.061, 2.055, 2.052, 2.049, 2.051, 2.056, 2.062, 2.065, 2.072, 2.079, 2.081, 2.079,
+                            2.079, 2.075, 2.069, 2.064, 2.061, 2.053, 2.049, 2.046, 2.049, 2.051, 2.057, 2.062, 2.069, 2.075, 2.077, 2.075,
+                            2.082, 2.079, 2.072, 2.065, 2.061, 2.054, 2.049, 2.047, 2.049, 2.051, 2.056, 2.061, 2.066, 2.073, 2.073, 2.069,
+                            2.086, 2.082, 2.075, 2.068, 2.062, 2.054, 2.051, 2.049, 2.051, 2.052, 2.056, 2.061, 2.066, 2.073, 2.073, 2.072,
+                            2.088, 2.086, 2.079, 2.074, 2.066, 2.057, 2.051, 2.051, 2.054, 2.055, 2.056, 2.061, 2.067, 2.072, 2.073, 2.072,
+                            2.091, 2.087, 2.079, 2.075, 2.068, 2.057, 2.052, 2.052, 2.056, 2.055, 2.055, 2.059, 2.066, 2.072, 2.072, 2.072,
+                            2.093, 2.088, 2.081, 2.077, 2.069, 2.059, 2.054, 2.054, 2.057, 2.056, 2.056, 2.058, 2.066, 2.072, 2.073, 2.073,
+                            2.095, 2.091, 2.084, 2.078, 2.075, 2.067, 2.057, 2.057, 2.059, 2.059, 2.058, 2.059, 2.068, 2.073, 2.075, 2.078
+                        ]
+                    },
+                    {
+                        "ct": 4850,
+                        "table":
+                        [
+                            2.973, 2.968, 2.956, 2.943, 2.941, 2.932, 2.923, 2.921, 2.924, 2.929, 2.931, 2.939, 2.953, 2.965, 2.966, 2.976,
+                            2.969, 2.962, 2.951, 2.941, 2.934, 2.928, 2.919, 2.918, 2.919, 2.923, 2.927, 2.933, 2.945, 2.957, 2.962, 2.962,
+                            2.964, 2.956, 2.944, 2.932, 2.929, 2.924, 2.915, 2.914, 2.915, 2.919, 2.924, 2.928, 2.941, 2.952, 2.958, 2.959,
+                            2.957, 2.951, 2.939, 2.928, 2.924, 2.919, 2.913, 2.911, 2.911, 2.915, 2.919, 2.925, 2.936, 2.947, 2.952, 2.953,
+                            2.954, 2.947, 2.935, 2.924, 2.919, 2.915, 2.908, 2.906, 2.906, 2.907, 2.914, 2.921, 2.932, 2.941, 2.943, 2.942,
+                            2.953, 2.946, 2.932, 2.921, 2.916, 2.911, 2.904, 2.902, 2.901, 2.904, 2.909, 2.919, 2.926, 2.937, 2.939, 2.939,
+                            2.953, 2.947, 2.932, 2.918, 2.915, 2.909, 2.903, 2.901, 2.901, 2.906, 2.911, 2.918, 2.924, 2.936, 2.936, 2.932,
+                            2.956, 2.948, 2.934, 2.919, 2.916, 2.908, 2.903, 2.901, 2.902, 2.907, 2.909, 2.917, 2.926, 2.936, 2.939, 2.939,
+                            2.957, 2.951, 2.936, 2.923, 2.917, 2.907, 2.904, 2.901, 2.902, 2.908, 2.911, 2.919, 2.929, 2.939, 2.942, 2.942,
+                            2.961, 2.951, 2.936, 2.922, 2.918, 2.906, 2.904, 2.901, 2.901, 2.907, 2.911, 2.921, 2.931, 2.941, 2.942, 2.944,
+                            2.964, 2.954, 2.936, 2.924, 2.918, 2.909, 2.905, 2.905, 2.905, 2.907, 2.912, 2.923, 2.933, 2.942, 2.944, 2.944,
+                            2.964, 2.958, 2.943, 2.927, 2.921, 2.914, 2.909, 2.907, 2.907, 2.912, 2.916, 2.928, 2.936, 2.944, 2.947, 2.952
+                        ]
+                    },
+                    {
+                        "ct": 5930,
+                        "table":
+                        [
+                            3.312, 3.308, 3.301, 3.294, 3.288, 3.277, 3.268, 3.261, 3.259, 3.261, 3.267, 3.273, 3.285, 3.301, 3.303, 3.312,
+                            3.308, 3.304, 3.294, 3.291, 3.283, 3.271, 3.263, 3.259, 3.257, 3.258, 3.261, 3.268, 3.278, 3.293, 3.299, 3.299,
+                            3.302, 3.296, 3.288, 3.282, 3.276, 3.267, 3.259, 3.254, 3.252, 3.253, 3.256, 3.261, 3.273, 3.289, 3.292, 3.292,
+                            3.296, 3.289, 3.282, 3.276, 3.269, 3.263, 3.256, 3.251, 3.248, 3.249, 3.251, 3.257, 3.268, 3.279, 3.284, 3.284,
+                            3.292, 3.285, 3.279, 3.271, 3.264, 3.257, 3.249, 3.243, 3.241, 3.241, 3.246, 3.252, 3.261, 3.274, 3.275, 3.273,
+                            3.291, 3.285, 3.276, 3.268, 3.259, 3.251, 3.242, 3.239, 3.236, 3.238, 3.244, 3.248, 3.258, 3.268, 3.269, 3.265,
+                            3.294, 3.288, 3.275, 3.266, 3.257, 3.248, 3.239, 3.238, 3.237, 3.238, 3.243, 3.246, 3.255, 3.264, 3.264, 3.257,
+                            3.297, 3.293, 3.279, 3.268, 3.258, 3.249, 3.238, 3.237, 3.239, 3.239, 3.243, 3.245, 3.255, 3.264, 3.264, 3.263,
+                            3.301, 3.295, 3.281, 3.271, 3.259, 3.248, 3.237, 3.237, 3.239, 3.241, 3.243, 3.246, 3.257, 3.265, 3.266, 3.264,
+                            3.306, 3.295, 3.279, 3.271, 3.261, 3.247, 3.235, 3.234, 3.239, 3.239, 3.243, 3.247, 3.258, 3.265, 3.265, 3.264,
+                            3.308, 3.297, 3.279, 3.272, 3.261, 3.249, 3.239, 3.239, 3.241, 3.243, 3.245, 3.248, 3.261, 3.265, 3.266, 3.265,
+                            3.309, 3.301, 3.286, 3.276, 3.267, 3.256, 3.246, 3.242, 3.244, 3.244, 3.249, 3.253, 3.263, 3.267, 3.271, 3.274
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 2960,
+                        "table":
+                        [
+                            2.133, 2.134, 2.139, 2.143, 2.148, 2.155, 2.158, 2.158, 2.158, 2.161, 2.161, 2.162, 2.159, 2.156, 2.152, 2.151,
+                            2.132, 2.133, 2.135, 2.142, 2.147, 2.153, 2.158, 2.158, 2.158, 2.158, 2.159, 2.159, 2.157, 2.154, 2.151, 2.148,
+                            2.133, 2.133, 2.135, 2.142, 2.149, 2.154, 2.158, 2.158, 2.157, 2.156, 2.158, 2.157, 2.155, 2.153, 2.148, 2.146,
+                            2.133, 2.133, 2.138, 2.145, 2.149, 2.154, 2.158, 2.159, 2.158, 2.155, 2.157, 2.156, 2.153, 2.149, 2.146, 2.144,
+                            2.133, 2.134, 2.139, 2.146, 2.149, 2.154, 2.158, 2.159, 2.159, 2.156, 2.154, 2.154, 2.149, 2.145, 2.143, 2.139,
+                            2.135, 2.135, 2.139, 2.146, 2.151, 2.155, 2.158, 2.159, 2.158, 2.156, 2.153, 2.151, 2.146, 2.143, 2.139, 2.136,
+                            2.135, 2.135, 2.138, 2.145, 2.151, 2.154, 2.157, 2.158, 2.157, 2.156, 2.153, 2.151, 2.147, 2.143, 2.141, 2.137,
+                            2.135, 2.134, 2.135, 2.141, 2.149, 2.154, 2.157, 2.157, 2.157, 2.157, 2.157, 2.153, 2.149, 2.146, 2.142, 2.139,
+                            2.132, 2.133, 2.135, 2.139, 2.148, 2.153, 2.158, 2.159, 2.159, 2.161, 2.161, 2.157, 2.154, 2.149, 2.144, 2.141,
+                            2.132, 2.133, 2.135, 2.141, 2.149, 2.155, 2.161, 2.161, 2.162, 2.162, 2.163, 2.159, 2.154, 2.149, 2.144, 2.138,
+                            2.136, 2.136, 2.137, 2.143, 2.149, 2.156, 2.162, 2.163, 2.162, 2.163, 2.164, 2.161, 2.157, 2.152, 2.146, 2.138,
+                            2.137, 2.137, 2.141, 2.147, 2.152, 2.157, 2.162, 2.162, 2.159, 2.161, 2.162, 2.162, 2.157, 2.152, 2.148, 2.148
+                        ]
+                    },
+                    {
+                        "ct": 4850,
+                        "table":
+                        [
+                            1.463, 1.464, 1.471, 1.478, 1.479, 1.483, 1.484, 1.486, 1.486, 1.484, 1.483, 1.481, 1.478, 1.475, 1.471, 1.468,
+                            1.463, 1.463, 1.468, 1.476, 1.479, 1.482, 1.484, 1.487, 1.486, 1.484, 1.483, 1.482, 1.478, 1.473, 1.469, 1.468,
+                            1.463, 1.464, 1.468, 1.476, 1.479, 1.483, 1.484, 1.486, 1.486, 1.485, 1.484, 1.482, 1.477, 1.473, 1.469, 1.468,
+                            1.463, 1.464, 1.469, 1.477, 1.481, 1.483, 1.485, 1.487, 1.487, 1.485, 1.485, 1.482, 1.478, 1.474, 1.469, 1.468,
+                            1.465, 1.465, 1.471, 1.478, 1.481, 1.484, 1.486, 1.488, 1.488, 1.487, 1.485, 1.482, 1.477, 1.472, 1.468, 1.467,
+                            1.465, 1.466, 1.472, 1.479, 1.482, 1.485, 1.486, 1.488, 1.488, 1.486, 1.484, 1.479, 1.475, 1.472, 1.468, 1.466,
+                            1.466, 1.466, 1.472, 1.478, 1.482, 1.484, 1.485, 1.488, 1.487, 1.485, 1.483, 1.479, 1.475, 1.472, 1.469, 1.468,
+                            1.465, 1.466, 1.469, 1.476, 1.481, 1.485, 1.485, 1.486, 1.486, 1.485, 1.483, 1.479, 1.477, 1.474, 1.471, 1.469,
+                            1.464, 1.465, 1.469, 1.476, 1.481, 1.484, 1.485, 1.487, 1.487, 1.486, 1.485, 1.481, 1.478, 1.475, 1.471, 1.469,
+                            1.463, 1.464, 1.469, 1.477, 1.481, 1.485, 1.485, 1.488, 1.488, 1.487, 1.486, 1.481, 1.478, 1.475, 1.471, 1.468,
+                            1.464, 1.465, 1.471, 1.478, 1.482, 1.486, 1.486, 1.488, 1.488, 1.487, 1.486, 1.481, 1.478, 1.475, 1.472, 1.468,
+                            1.465, 1.466, 1.472, 1.481, 1.483, 1.487, 1.487, 1.488, 1.488, 1.486, 1.485, 1.481, 1.479, 1.476, 1.473, 1.472
+                        ]
+                    },
+                    {
+                        "ct": 5930,
+                        "table":
+                        [
+                            1.443, 1.444, 1.448, 1.453, 1.459, 1.463, 1.465, 1.467, 1.469, 1.469, 1.467, 1.466, 1.462, 1.457, 1.454, 1.451,
+                            1.443, 1.444, 1.445, 1.451, 1.459, 1.463, 1.465, 1.467, 1.469, 1.469, 1.467, 1.465, 1.461, 1.456, 1.452, 1.451,
+                            1.444, 1.444, 1.445, 1.451, 1.459, 1.463, 1.466, 1.468, 1.469, 1.469, 1.467, 1.465, 1.461, 1.456, 1.452, 1.449,
+                            1.444, 1.444, 1.447, 1.452, 1.459, 1.464, 1.467, 1.469, 1.471, 1.469, 1.467, 1.466, 1.461, 1.456, 1.452, 1.449,
+                            1.444, 1.445, 1.448, 1.452, 1.459, 1.465, 1.469, 1.471, 1.471, 1.471, 1.468, 1.465, 1.461, 1.455, 1.451, 1.449,
+                            1.445, 1.446, 1.449, 1.453, 1.461, 1.466, 1.469, 1.471, 1.472, 1.469, 1.467, 1.465, 1.459, 1.455, 1.451, 1.447,
+                            1.446, 1.446, 1.449, 1.453, 1.461, 1.466, 1.469, 1.469, 1.469, 1.469, 1.467, 1.465, 1.459, 1.455, 1.452, 1.449,
+                            1.446, 1.446, 1.447, 1.451, 1.459, 1.466, 1.469, 1.469, 1.469, 1.469, 1.467, 1.465, 1.461, 1.457, 1.454, 1.451,
+                            1.444, 1.444, 1.447, 1.451, 1.459, 1.466, 1.469, 1.469, 1.471, 1.471, 1.468, 1.466, 1.462, 1.458, 1.454, 1.452,
+                            1.444, 1.444, 1.448, 1.453, 1.459, 1.466, 1.469, 1.471, 1.472, 1.472, 1.468, 1.466, 1.462, 1.458, 1.454, 1.449,
+                            1.446, 1.447, 1.449, 1.454, 1.461, 1.466, 1.471, 1.471, 1.471, 1.471, 1.468, 1.466, 1.462, 1.459, 1.455, 1.449,
+                            1.447, 1.447, 1.452, 1.457, 1.462, 1.468, 1.472, 1.472, 1.471, 1.471, 1.468, 1.466, 1.462, 1.459, 1.456, 1.455
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    1.548, 1.499, 1.387, 1.289, 1.223, 1.183, 1.164, 1.154, 1.153, 1.169, 1.211, 1.265, 1.345, 1.448, 1.581, 1.619,
+                    1.513, 1.412, 1.307, 1.228, 1.169, 1.129, 1.105, 1.098, 1.103, 1.127, 1.157, 1.209, 1.272, 1.361, 1.481, 1.583,
+                    1.449, 1.365, 1.257, 1.175, 1.124, 1.085, 1.062, 1.054, 1.059, 1.079, 1.113, 1.151, 1.211, 1.293, 1.407, 1.488,
+                    1.424, 1.324, 1.222, 1.139, 1.089, 1.056, 1.034, 1.031, 1.034, 1.049, 1.075, 1.115, 1.164, 1.241, 1.351, 1.446,
+                    1.412, 1.297, 1.203, 1.119, 1.069, 1.039, 1.021, 1.016, 1.022, 1.032, 1.052, 1.086, 1.135, 1.212, 1.321, 1.439,
+                    1.406, 1.287, 1.195, 1.115, 1.059, 1.028, 1.014, 1.012, 1.015, 1.026, 1.041, 1.074, 1.125, 1.201, 1.302, 1.425,
+                    1.406, 1.294, 1.205, 1.126, 1.062, 1.031, 1.013, 1.009, 1.011, 1.019, 1.042, 1.079, 1.129, 1.203, 1.302, 1.435,
+                    1.415, 1.318, 1.229, 1.146, 1.076, 1.039, 1.019, 1.014, 1.017, 1.031, 1.053, 1.093, 1.144, 1.219, 1.314, 1.436,
+                    1.435, 1.348, 1.246, 1.164, 1.094, 1.059, 1.036, 1.032, 1.037, 1.049, 1.072, 1.114, 1.167, 1.257, 1.343, 1.462,
+                    1.471, 1.385, 1.278, 1.189, 1.124, 1.084, 1.064, 1.061, 1.069, 1.078, 1.101, 1.146, 1.207, 1.298, 1.415, 1.496,
+                    1.522, 1.436, 1.323, 1.228, 1.169, 1.118, 1.101, 1.094, 1.099, 1.113, 1.146, 1.194, 1.265, 1.353, 1.474, 1.571,
+                    1.578, 1.506, 1.378, 1.281, 1.211, 1.156, 1.135, 1.134, 1.139, 1.158, 1.194, 1.251, 1.327, 1.427, 1.559, 1.611
+                ],
+                "sigma": 0.00121,
+                "sigma_Cb": 0.00115
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2360,
+                        "ccm":
+                        [
+                            1.66078, -0.23588, -0.42491,
+                            -0.47456, 1.82763, -0.35307,
+                            -0.00545, -1.44729, 2.45273
+                        ]
+                    },
+                    {
+                        "ct": 2870,
+                        "ccm":
+                        [
+                            1.78373, -0.55344, -0.23029,
+                            -0.39951, 1.69701, -0.29751,
+                            0.01986, -1.06525, 2.04539
+                        ]
+                    },
+                    {
+                        "ct": 2970,
+                        "ccm":
+                        [
+                            1.73511, -0.56973, -0.16537,
+                            -0.36338, 1.69878, -0.33539,
+                            -0.02354, -0.76813, 1.79168
+                        ]
+                    },
+                    {
+                        "ct": 3000,
+                        "ccm":
+                        [
+                            2.06374, -0.92218, -0.14156,
+                            -0.41721, 1.69289, -0.27568,
+                            -0.00554, -0.92741, 1.93295
+                        ]
+                    },
+                    {
+                        "ct": 3700,
+                        "ccm":
+                        [
+                            2.13792, -1.08136, -0.05655,
+                            -0.34739, 1.58989, -0.24249,
+                            -0.00349, -0.76789, 1.77138
+                        ]
+                    },
+                    {
+                        "ct": 3870,
+                        "ccm":
+                        [
+                            1.83834, -0.70528, -0.13307,
+                            -0.30499, 1.60523, -0.30024,
+                            -0.05701, -0.58313, 1.64014
+                        ]
+                    },
+                    {
+                        "ct": 4000,
+                        "ccm":
+                        [
+                            2.15741, -1.10295, -0.05447,
+                            -0.34631, 1.61158, -0.26528,
+                            -0.02723, -0.70288, 1.73011
+                        ]
+                    },
+                    {
+                        "ct": 4400,
+                        "ccm":
+                        [
+                            2.05729, -0.95007, -0.10723,
+                            -0.41712, 1.78606, -0.36894,
+                            -0.11899, -0.55727, 1.67626
+                        ]
+                    },
+                    {
+                        "ct": 4715,
+                        "ccm":
+                        [
+                            1.90255, -0.77478, -0.12777,
+                            -0.31338, 1.88197, -0.56858,
+                            -0.06001, -0.61785, 1.67786
+                        ]
+                    },
+                    {
+                        "ct": 5920,
+                        "ccm":
+                        [
+                            1.98691, -0.84671, -0.14019,
+                            -0.26581, 1.70615, -0.44035,
+                            -0.09532, -0.47332, 1.56864
+                        ]
+                    },
+                    {
+                        "ct": 9050,
+                        "ccm":
+                        [
+                            2.09255, -0.76541, -0.32714,
+                            -0.28973, 2.27462, -0.98489,
+                            -0.17299, -0.61275, 1.78574
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx477_scientific.json b/src/ipa/rpi/vc4/data/imx477_scientific.json
new file mode 100644
index 00000000..26c692fd
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx477_scientific.json
@@ -0,0 +1,479 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 27242,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 830,
+                "reference_Y": 17755
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.767
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 204,
+                "slope": 0.01078
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2000.0, 0.6331025775790707, 0.27424225990946915,
+                    2200.0, 0.5696117366212947, 0.3116091368689487,
+                    2400.0, 0.5204264653110015, 0.34892179554105873,
+                    2600.0, 0.48148675531667223, 0.38565229719076793,
+                    2800.0, 0.450085403501908, 0.42145684622485047,
+                    3000.0, 0.42436130159169017, 0.45611835670028816,
+                    3200.0, 0.40300023695527337, 0.48950766215198593,
+                    3400.0, 0.3850520052612984, 0.5215567075837261,
+                    3600.0, 0.36981508088230314, 0.5522397906415475,
+                    4100.0, 0.333468007836758, 0.5909770465167908,
+                    4600.0, 0.31196097364221376, 0.6515706327327178,
+                    5100.0, 0.2961860409294588, 0.7068178946570284,
+                    5600.0, 0.2842607232745885, 0.7564837749584288,
+                    6100.0, 0.2750265787051251, 0.8006183524920533,
+                    6600.0, 0.2677057225584924, 0.8398879225373039,
+                    7100.0, 0.2617955199757274, 0.8746456080032436,
+                    7600.0, 0.25693714288250125, 0.905569559506562,
+                    8100.0, 0.25287531441063316, 0.9331696750390895,
+                    8600.0, 0.24946601483331993, 0.9576820904825795
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.0238,
+                "transverse_neg": 0.04429,
+                "coarse_step": 0.1
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 66666 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 33333 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.3,
+                                1000, 0.3
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.3,
+                                1000, 0.3
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ],
+                    "shadows": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.5,
+                            "y_target":
+                            [
+                                0, 0.17,
+                                1000, 0.17
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 0,
+                "gamma_curve":
+                [
+                    0, 0,
+                    512, 2304,
+                    1024, 4608,
+                    1536, 6573,
+                    2048, 8401,
+                    2560, 9992,
+                    3072, 11418,
+                    3584, 12719,
+                    4096, 13922,
+                    4608, 15045,
+                    5120, 16103,
+                    5632, 17104,
+                    6144, 18056,
+                    6656, 18967,
+                    7168, 19839,
+                    7680, 20679,
+                    8192, 21488,
+                    9216, 23028,
+                    10240, 24477,
+                    11264, 25849,
+                    12288, 27154,
+                    13312, 28401,
+                    14336, 29597,
+                    15360, 30747,
+                    16384, 31856,
+                    17408, 32928,
+                    18432, 33966,
+                    19456, 34973,
+                    20480, 35952,
+                    22528, 37832,
+                    24576, 39621,
+                    26624, 41330,
+                    28672, 42969,
+                    30720, 44545,
+                    32768, 46065,
+                    34816, 47534,
+                    36864, 48956,
+                    38912, 50336,
+                    40960, 51677,
+                    43008, 52982,
+                    45056, 54253,
+                    47104, 55493,
+                    49152, 56704,
+                    51200, 57888,
+                    53248, 59046,
+                    55296, 60181,
+                    57344, 61292,
+                    59392, 62382,
+                    61440, 63452,
+                    63488, 64503,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2000,
+                        "ccm":
+                        [
+                            1.5813882365848004, -0.35293683714581114, -0.27378771561617715,
+                            -0.4347297185453639, 1.5792631087746074, -0.12102601986382337,
+                            0.2322290578987574, -1.4382672640468128, 2.1386425781770755
+                        ]
+                    },
+                    {
+                        "ct": 2200,
+                        "ccm":
+                        [
+                            1.6322048484088305, -0.45932286857238486, -0.21373542690252198,
+                            -0.3970719209901105, 1.5877868651467202, -0.17249380832122455,
+                            0.20753774825903412, -1.2660673594740142, 2.005654261091916
+                        ]
+                    },
+                    {
+                        "ct": 2400,
+                        "ccm":
+                        [
+                            1.6766610071470398, -0.5447101051688111, -0.16838641107407676,
+                            -0.3659845183388154, 1.592223692670396, -0.2127091997471162,
+                            0.1833964516767549, -1.1339155942419321, 1.9089342978542396
+                        ]
+                    },
+                    {
+                        "ct": 2600,
+                        "ccm":
+                        [
+                            1.7161984340622154, -0.6152585785678794, -0.1331100845092582,
+                            -0.33972082628066275, 1.5944888273736966, -0.2453979465898787,
+                            0.1615577497676328, -1.0298684958833109, 1.8357854177422053
+                        ]
+                    },
+                    {
+                        "ct": 2800,
+                        "ccm":
+                        [
+                            1.7519307259815728, -0.6748682080165339, -0.10515169074540848,
+                            -0.3171703484479931, 1.5955820297498486, -0.2727395854813966,
+                            0.14230870739974305, -0.9460976023551511, 1.778709391659538
+                        ]
+                    },
+                    {
+                        "ct": 3000,
+                        "ccm":
+                        [
+                            1.7846716625128374, -0.7261240476375332, -0.08274697420358428,
+                            -0.2975654035173307, 1.5960425637021738, -0.2961043416505157,
+                            0.12546426281675097, -0.8773434727076518, 1.7330356805246685
+                        ]
+                    },
+                    {
+                        "ct": 3200,
+                        "ccm":
+                        [
+                            1.8150085872943436, -0.7708109672515514, -0.06469468211419174,
+                            -0.2803468940646277, 1.596168842967451, -0.3164044170681625,
+                            0.11071494533513807, -0.8199772290209191, 1.69572135046367
+                        ]
+                    },
+                    {
+                        "ct": 3400,
+                        "ccm":
+                        [
+                            1.8433668304932087, -0.8102060605062592, -0.05013485852801454,
+                            -0.2650934036324084, 1.5961288492969294, -0.33427554893845535,
+                            0.0977478941863518, -0.7714303112098978, 1.6647070820146963
+                        ]
+                    },
+                    {
+                        "ct": 3600,
+                        "ccm":
+                        [
+                            1.8700575831917468, -0.8452518300291346, -0.03842644337477299,
+                            -0.2514794528347016, 1.5960178299141876, -0.3501774949366156,
+                            0.08628520830733245, -0.729841503339915, 1.638553343939267
+                        ]
+                    },
+                    {
+                        "ct": 4100,
+                        "ccm":
+                        [
+                            1.8988700903560716, -0.8911278803351247, -0.018848644425650693,
+                            -0.21487101487384094, 1.599236541382614, -0.39405450457918206,
+                            0.08251488056482173, -0.7178919368326191, 1.6267009056502704
+                        ]
+                    },
+                    {
+                        "ct": 4600,
+                        "ccm":
+                        [
+                            1.960355191764125, -0.9624344812121991, -0.0017122408632169205,
+                            -0.19444620905212898, 1.5978493736948447, -0.416727638296156,
+                            0.06310261513271084, -0.6483790952487849, 1.5834605477213093
+                        ]
+                    },
+                    {
+                        "ct": 5100,
+                        "ccm":
+                        [
+                            2.014680536961399, -1.0195930302148566, 0.007728256612638915,
+                            -0.17751999660735496, 1.5977081555831, -0.4366085498741474,
+                            0.04741267583041334, -0.5950327902073489, 1.5512919847321853
+                        ]
+                    },
+                    {
+                        "ct": 5600,
+                        "ccm":
+                        [
+                            2.062652337917251, -1.0658386679125478, 0.011886354256281267,
+                            -0.16319197721451495, 1.598363237584736, -0.45422061523742235,
+                            0.03465810928795378, -0.5535454108047286, 1.5269025836946852
+                        ]
+                    },
+                    {
+                        "ct": 6100,
+                        "ccm":
+                        [
+                            2.104985902038069, -1.103597868736314, 0.012503517136539277,
+                            -0.15090797064906178, 1.5994703078166095, -0.4698414300864995,
+                            0.02421766063474242, -0.5208922818196823, 1.5081270847783788
+                        ]
+                    },
+                    {
+                        "ct": 6600,
+                        "ccm":
+                        [
+                            2.1424988751299714, -1.134760232367728, 0.010730356010435522,
+                            -0.14021846798466234, 1.600822462230719, -0.48379204794526487,
+                            0.015521315410496622, -0.49463630325832275, 1.4933313534840327
+                        ]
+                    },
+                    {
+                        "ct": 7100,
+                        "ccm":
+                        [
+                            2.1758034100130925, -1.1607558481037359, 0.007452724895469076,
+                            -0.13085694672641826, 1.6022648614493245, -0.4962330524084075,
+                            0.008226943206113427, -0.4733077192319791, 1.4815336120437468
+                        ]
+                    },
+                    {
+                        "ct": 7600,
+                        "ccm":
+                        [
+                            2.205529206931895, -1.1826662383072108, 0.0032019529917605167,
+                            -0.122572009780486, 1.6037258133595753, -0.5073973734282445,
+                            0.0020132587619863425, -0.4556590236414181, 1.471939788496745
+                        ]
+                    },
+                    {
+                        "ct": 8100,
+                        "ccm":
+                        [
+                            2.232224969223067, -1.2013672897252885, -0.0016234598095482985,
+                            -0.11518026734442414, 1.6051544769439803, -0.5174558699422255,
+                            -0.0033378143542219835, -0.4408590373867774, 1.4640252230667452
+                        ]
+                    },
+                    {
+                        "ct": 8600,
+                        "ccm":
+                        [
+                            2.256082295891265, -1.2173210549996634, -0.0067231350481711675,
+                            -0.10860272839843167, 1.6065150139140594, -0.5264728573611493,
+                            -0.007952618707984149, -0.4284003574050791, 1.4574646927117558
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx477_v1.json b/src/ipa/rpi/vc4/data/imx477_v1.json
new file mode 100644
index 00000000..d6402009
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx477_v1.json
@@ -0,0 +1,516 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 27242,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 830,
+                "reference_Y": 17755
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.767
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 204,
+                "slope": 0.01078
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2360.0, 0.6009, 0.3093,
+                    2870.0, 0.5047, 0.3936,
+                    2970.0, 0.4782, 0.4221,
+                    3700.0, 0.4212, 0.4923,
+                    3870.0, 0.4037, 0.5166,
+                    4000.0, 0.3965, 0.5271,
+                    4400.0, 0.3703, 0.5666,
+                    4715.0, 0.3411, 0.6147,
+                    5920.0, 0.3108, 0.6687,
+                    9050.0, 0.2524, 0.7856
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.0238,
+                "transverse_neg": 0.04429
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 66666 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 33333 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.3,
+                                1000, 0.3
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.3,
+                                1000, 0.3
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ],
+                    "shadows": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.5,
+                            "y_target":
+                            [
+                                0, 0.17,
+                                1000, 0.17
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 2960,
+                        "table":
+                        [
+                            2.088, 2.086, 2.082, 2.081, 2.077, 2.071, 2.068, 2.068, 2.072, 2.073, 2.075, 2.078, 2.084, 2.092, 2.095, 2.098,
+                            2.086, 2.084, 2.079, 2.078, 2.075, 2.068, 2.064, 2.063, 2.068, 2.071, 2.072, 2.075, 2.081, 2.089, 2.092, 2.094,
+                            2.083, 2.081, 2.077, 2.072, 2.069, 2.062, 2.059, 2.059, 2.063, 2.067, 2.069, 2.072, 2.079, 2.088, 2.089, 2.089,
+                            2.081, 2.077, 2.072, 2.068, 2.065, 2.058, 2.055, 2.054, 2.057, 2.062, 2.066, 2.069, 2.077, 2.084, 2.086, 2.086,
+                            2.078, 2.075, 2.069, 2.065, 2.061, 2.055, 2.052, 2.049, 2.051, 2.056, 2.062, 2.065, 2.072, 2.079, 2.081, 2.079,
+                            2.079, 2.075, 2.069, 2.064, 2.061, 2.053, 2.049, 2.046, 2.049, 2.051, 2.057, 2.062, 2.069, 2.075, 2.077, 2.075,
+                            2.082, 2.079, 2.072, 2.065, 2.061, 2.054, 2.049, 2.047, 2.049, 2.051, 2.056, 2.061, 2.066, 2.073, 2.073, 2.069,
+                            2.086, 2.082, 2.075, 2.068, 2.062, 2.054, 2.051, 2.049, 2.051, 2.052, 2.056, 2.061, 2.066, 2.073, 2.073, 2.072,
+                            2.088, 2.086, 2.079, 2.074, 2.066, 2.057, 2.051, 2.051, 2.054, 2.055, 2.056, 2.061, 2.067, 2.072, 2.073, 2.072,
+                            2.091, 2.087, 2.079, 2.075, 2.068, 2.057, 2.052, 2.052, 2.056, 2.055, 2.055, 2.059, 2.066, 2.072, 2.072, 2.072,
+                            2.093, 2.088, 2.081, 2.077, 2.069, 2.059, 2.054, 2.054, 2.057, 2.056, 2.056, 2.058, 2.066, 2.072, 2.073, 2.073,
+                            2.095, 2.091, 2.084, 2.078, 2.075, 2.067, 2.057, 2.057, 2.059, 2.059, 2.058, 2.059, 2.068, 2.073, 2.075, 2.078
+                        ]
+                    },
+                    {
+                        "ct": 4850,
+                        "table":
+                        [
+                            2.973, 2.968, 2.956, 2.943, 2.941, 2.932, 2.923, 2.921, 2.924, 2.929, 2.931, 2.939, 2.953, 2.965, 2.966, 2.976,
+                            2.969, 2.962, 2.951, 2.941, 2.934, 2.928, 2.919, 2.918, 2.919, 2.923, 2.927, 2.933, 2.945, 2.957, 2.962, 2.962,
+                            2.964, 2.956, 2.944, 2.932, 2.929, 2.924, 2.915, 2.914, 2.915, 2.919, 2.924, 2.928, 2.941, 2.952, 2.958, 2.959,
+                            2.957, 2.951, 2.939, 2.928, 2.924, 2.919, 2.913, 2.911, 2.911, 2.915, 2.919, 2.925, 2.936, 2.947, 2.952, 2.953,
+                            2.954, 2.947, 2.935, 2.924, 2.919, 2.915, 2.908, 2.906, 2.906, 2.907, 2.914, 2.921, 2.932, 2.941, 2.943, 2.942,
+                            2.953, 2.946, 2.932, 2.921, 2.916, 2.911, 2.904, 2.902, 2.901, 2.904, 2.909, 2.919, 2.926, 2.937, 2.939, 2.939,
+                            2.953, 2.947, 2.932, 2.918, 2.915, 2.909, 2.903, 2.901, 2.901, 2.906, 2.911, 2.918, 2.924, 2.936, 2.936, 2.932,
+                            2.956, 2.948, 2.934, 2.919, 2.916, 2.908, 2.903, 2.901, 2.902, 2.907, 2.909, 2.917, 2.926, 2.936, 2.939, 2.939,
+                            2.957, 2.951, 2.936, 2.923, 2.917, 2.907, 2.904, 2.901, 2.902, 2.908, 2.911, 2.919, 2.929, 2.939, 2.942, 2.942,
+                            2.961, 2.951, 2.936, 2.922, 2.918, 2.906, 2.904, 2.901, 2.901, 2.907, 2.911, 2.921, 2.931, 2.941, 2.942, 2.944,
+                            2.964, 2.954, 2.936, 2.924, 2.918, 2.909, 2.905, 2.905, 2.905, 2.907, 2.912, 2.923, 2.933, 2.942, 2.944, 2.944,
+                            2.964, 2.958, 2.943, 2.927, 2.921, 2.914, 2.909, 2.907, 2.907, 2.912, 2.916, 2.928, 2.936, 2.944, 2.947, 2.952
+                        ]
+                    },
+                    {
+                        "ct": 5930,
+                        "table":
+                        [
+                            3.312, 3.308, 3.301, 3.294, 3.288, 3.277, 3.268, 3.261, 3.259, 3.261, 3.267, 3.273, 3.285, 3.301, 3.303, 3.312,
+                            3.308, 3.304, 3.294, 3.291, 3.283, 3.271, 3.263, 3.259, 3.257, 3.258, 3.261, 3.268, 3.278, 3.293, 3.299, 3.299,
+                            3.302, 3.296, 3.288, 3.282, 3.276, 3.267, 3.259, 3.254, 3.252, 3.253, 3.256, 3.261, 3.273, 3.289, 3.292, 3.292,
+                            3.296, 3.289, 3.282, 3.276, 3.269, 3.263, 3.256, 3.251, 3.248, 3.249, 3.251, 3.257, 3.268, 3.279, 3.284, 3.284,
+                            3.292, 3.285, 3.279, 3.271, 3.264, 3.257, 3.249, 3.243, 3.241, 3.241, 3.246, 3.252, 3.261, 3.274, 3.275, 3.273,
+                            3.291, 3.285, 3.276, 3.268, 3.259, 3.251, 3.242, 3.239, 3.236, 3.238, 3.244, 3.248, 3.258, 3.268, 3.269, 3.265,
+                            3.294, 3.288, 3.275, 3.266, 3.257, 3.248, 3.239, 3.238, 3.237, 3.238, 3.243, 3.246, 3.255, 3.264, 3.264, 3.257,
+                            3.297, 3.293, 3.279, 3.268, 3.258, 3.249, 3.238, 3.237, 3.239, 3.239, 3.243, 3.245, 3.255, 3.264, 3.264, 3.263,
+                            3.301, 3.295, 3.281, 3.271, 3.259, 3.248, 3.237, 3.237, 3.239, 3.241, 3.243, 3.246, 3.257, 3.265, 3.266, 3.264,
+                            3.306, 3.295, 3.279, 3.271, 3.261, 3.247, 3.235, 3.234, 3.239, 3.239, 3.243, 3.247, 3.258, 3.265, 3.265, 3.264,
+                            3.308, 3.297, 3.279, 3.272, 3.261, 3.249, 3.239, 3.239, 3.241, 3.243, 3.245, 3.248, 3.261, 3.265, 3.266, 3.265,
+                            3.309, 3.301, 3.286, 3.276, 3.267, 3.256, 3.246, 3.242, 3.244, 3.244, 3.249, 3.253, 3.263, 3.267, 3.271, 3.274
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 2960,
+                        "table":
+                        [
+                            2.133, 2.134, 2.139, 2.143, 2.148, 2.155, 2.158, 2.158, 2.158, 2.161, 2.161, 2.162, 2.159, 2.156, 2.152, 2.151,
+                            2.132, 2.133, 2.135, 2.142, 2.147, 2.153, 2.158, 2.158, 2.158, 2.158, 2.159, 2.159, 2.157, 2.154, 2.151, 2.148,
+                            2.133, 2.133, 2.135, 2.142, 2.149, 2.154, 2.158, 2.158, 2.157, 2.156, 2.158, 2.157, 2.155, 2.153, 2.148, 2.146,
+                            2.133, 2.133, 2.138, 2.145, 2.149, 2.154, 2.158, 2.159, 2.158, 2.155, 2.157, 2.156, 2.153, 2.149, 2.146, 2.144,
+                            2.133, 2.134, 2.139, 2.146, 2.149, 2.154, 2.158, 2.159, 2.159, 2.156, 2.154, 2.154, 2.149, 2.145, 2.143, 2.139,
+                            2.135, 2.135, 2.139, 2.146, 2.151, 2.155, 2.158, 2.159, 2.158, 2.156, 2.153, 2.151, 2.146, 2.143, 2.139, 2.136,
+                            2.135, 2.135, 2.138, 2.145, 2.151, 2.154, 2.157, 2.158, 2.157, 2.156, 2.153, 2.151, 2.147, 2.143, 2.141, 2.137,
+                            2.135, 2.134, 2.135, 2.141, 2.149, 2.154, 2.157, 2.157, 2.157, 2.157, 2.157, 2.153, 2.149, 2.146, 2.142, 2.139,
+                            2.132, 2.133, 2.135, 2.139, 2.148, 2.153, 2.158, 2.159, 2.159, 2.161, 2.161, 2.157, 2.154, 2.149, 2.144, 2.141,
+                            2.132, 2.133, 2.135, 2.141, 2.149, 2.155, 2.161, 2.161, 2.162, 2.162, 2.163, 2.159, 2.154, 2.149, 2.144, 2.138,
+                            2.136, 2.136, 2.137, 2.143, 2.149, 2.156, 2.162, 2.163, 2.162, 2.163, 2.164, 2.161, 2.157, 2.152, 2.146, 2.138,
+                            2.137, 2.137, 2.141, 2.147, 2.152, 2.157, 2.162, 2.162, 2.159, 2.161, 2.162, 2.162, 2.157, 2.152, 2.148, 2.148
+                        ]
+                    },
+                    {
+                        "ct": 4850,
+                        "table":
+                        [
+                            1.463, 1.464, 1.471, 1.478, 1.479, 1.483, 1.484, 1.486, 1.486, 1.484, 1.483, 1.481, 1.478, 1.475, 1.471, 1.468,
+                            1.463, 1.463, 1.468, 1.476, 1.479, 1.482, 1.484, 1.487, 1.486, 1.484, 1.483, 1.482, 1.478, 1.473, 1.469, 1.468,
+                            1.463, 1.464, 1.468, 1.476, 1.479, 1.483, 1.484, 1.486, 1.486, 1.485, 1.484, 1.482, 1.477, 1.473, 1.469, 1.468,
+                            1.463, 1.464, 1.469, 1.477, 1.481, 1.483, 1.485, 1.487, 1.487, 1.485, 1.485, 1.482, 1.478, 1.474, 1.469, 1.468,
+                            1.465, 1.465, 1.471, 1.478, 1.481, 1.484, 1.486, 1.488, 1.488, 1.487, 1.485, 1.482, 1.477, 1.472, 1.468, 1.467,
+                            1.465, 1.466, 1.472, 1.479, 1.482, 1.485, 1.486, 1.488, 1.488, 1.486, 1.484, 1.479, 1.475, 1.472, 1.468, 1.466,
+                            1.466, 1.466, 1.472, 1.478, 1.482, 1.484, 1.485, 1.488, 1.487, 1.485, 1.483, 1.479, 1.475, 1.472, 1.469, 1.468,
+                            1.465, 1.466, 1.469, 1.476, 1.481, 1.485, 1.485, 1.486, 1.486, 1.485, 1.483, 1.479, 1.477, 1.474, 1.471, 1.469,
+                            1.464, 1.465, 1.469, 1.476, 1.481, 1.484, 1.485, 1.487, 1.487, 1.486, 1.485, 1.481, 1.478, 1.475, 1.471, 1.469,
+                            1.463, 1.464, 1.469, 1.477, 1.481, 1.485, 1.485, 1.488, 1.488, 1.487, 1.486, 1.481, 1.478, 1.475, 1.471, 1.468,
+                            1.464, 1.465, 1.471, 1.478, 1.482, 1.486, 1.486, 1.488, 1.488, 1.487, 1.486, 1.481, 1.478, 1.475, 1.472, 1.468,
+                            1.465, 1.466, 1.472, 1.481, 1.483, 1.487, 1.487, 1.488, 1.488, 1.486, 1.485, 1.481, 1.479, 1.476, 1.473, 1.472
+                        ]
+                    },
+                    {
+                        "ct": 5930,
+                        "table":
+                        [
+                            1.443, 1.444, 1.448, 1.453, 1.459, 1.463, 1.465, 1.467, 1.469, 1.469, 1.467, 1.466, 1.462, 1.457, 1.454, 1.451,
+                            1.443, 1.444, 1.445, 1.451, 1.459, 1.463, 1.465, 1.467, 1.469, 1.469, 1.467, 1.465, 1.461, 1.456, 1.452, 1.451,
+                            1.444, 1.444, 1.445, 1.451, 1.459, 1.463, 1.466, 1.468, 1.469, 1.469, 1.467, 1.465, 1.461, 1.456, 1.452, 1.449,
+                            1.444, 1.444, 1.447, 1.452, 1.459, 1.464, 1.467, 1.469, 1.471, 1.469, 1.467, 1.466, 1.461, 1.456, 1.452, 1.449,
+                            1.444, 1.445, 1.448, 1.452, 1.459, 1.465, 1.469, 1.471, 1.471, 1.471, 1.468, 1.465, 1.461, 1.455, 1.451, 1.449,
+                            1.445, 1.446, 1.449, 1.453, 1.461, 1.466, 1.469, 1.471, 1.472, 1.469, 1.467, 1.465, 1.459, 1.455, 1.451, 1.447,
+                            1.446, 1.446, 1.449, 1.453, 1.461, 1.466, 1.469, 1.469, 1.469, 1.469, 1.467, 1.465, 1.459, 1.455, 1.452, 1.449,
+                            1.446, 1.446, 1.447, 1.451, 1.459, 1.466, 1.469, 1.469, 1.469, 1.469, 1.467, 1.465, 1.461, 1.457, 1.454, 1.451,
+                            1.444, 1.444, 1.447, 1.451, 1.459, 1.466, 1.469, 1.469, 1.471, 1.471, 1.468, 1.466, 1.462, 1.458, 1.454, 1.452,
+                            1.444, 1.444, 1.448, 1.453, 1.459, 1.466, 1.469, 1.471, 1.472, 1.472, 1.468, 1.466, 1.462, 1.458, 1.454, 1.449,
+                            1.446, 1.447, 1.449, 1.454, 1.461, 1.466, 1.471, 1.471, 1.471, 1.471, 1.468, 1.466, 1.462, 1.459, 1.455, 1.449,
+                            1.447, 1.447, 1.452, 1.457, 1.462, 1.468, 1.472, 1.472, 1.471, 1.471, 1.468, 1.466, 1.462, 1.459, 1.456, 1.455
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    1.548, 1.499, 1.387, 1.289, 1.223, 1.183, 1.164, 1.154, 1.153, 1.169, 1.211, 1.265, 1.345, 1.448, 1.581, 1.619,
+                    1.513, 1.412, 1.307, 1.228, 1.169, 1.129, 1.105, 1.098, 1.103, 1.127, 1.157, 1.209, 1.272, 1.361, 1.481, 1.583,
+                    1.449, 1.365, 1.257, 1.175, 1.124, 1.085, 1.062, 1.054, 1.059, 1.079, 1.113, 1.151, 1.211, 1.293, 1.407, 1.488,
+                    1.424, 1.324, 1.222, 1.139, 1.089, 1.056, 1.034, 1.031, 1.034, 1.049, 1.075, 1.115, 1.164, 1.241, 1.351, 1.446,
+                    1.412, 1.297, 1.203, 1.119, 1.069, 1.039, 1.021, 1.016, 1.022, 1.032, 1.052, 1.086, 1.135, 1.212, 1.321, 1.439,
+                    1.406, 1.287, 1.195, 1.115, 1.059, 1.028, 1.014, 1.012, 1.015, 1.026, 1.041, 1.074, 1.125, 1.201, 1.302, 1.425,
+                    1.406, 1.294, 1.205, 1.126, 1.062, 1.031, 1.013, 1.009, 1.011, 1.019, 1.042, 1.079, 1.129, 1.203, 1.302, 1.435,
+                    1.415, 1.318, 1.229, 1.146, 1.076, 1.039, 1.019, 1.014, 1.017, 1.031, 1.053, 1.093, 1.144, 1.219, 1.314, 1.436,
+                    1.435, 1.348, 1.246, 1.164, 1.094, 1.059, 1.036, 1.032, 1.037, 1.049, 1.072, 1.114, 1.167, 1.257, 1.343, 1.462,
+                    1.471, 1.385, 1.278, 1.189, 1.124, 1.084, 1.064, 1.061, 1.069, 1.078, 1.101, 1.146, 1.207, 1.298, 1.415, 1.496,
+                    1.522, 1.436, 1.323, 1.228, 1.169, 1.118, 1.101, 1.094, 1.099, 1.113, 1.146, 1.194, 1.265, 1.353, 1.474, 1.571,
+                    1.578, 1.506, 1.378, 1.281, 1.211, 1.156, 1.135, 1.134, 1.139, 1.158, 1.194, 1.251, 1.327, 1.427, 1.559, 1.611
+                ],
+                "sigma": 0.00121,
+                "sigma_Cb": 0.00115
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2360,
+                        "ccm":
+                        [
+                            1.66078, -0.23588, -0.42491,
+                            -0.47456, 1.82763, -0.35307,
+                            -0.00545, -1.44729, 2.45273
+                        ]
+                    },
+                    {
+                        "ct": 2870,
+                        "ccm":
+                        [
+                            1.78373, -0.55344, -0.23029,
+                            -0.39951, 1.69701, -0.29751,
+                            0.01986, -1.06525, 2.04539
+                        ]
+                    },
+                    {
+                        "ct": 2970,
+                        "ccm":
+                        [
+                            1.73511, -0.56973, -0.16537,
+                            -0.36338, 1.69878, -0.33539,
+                            -0.02354, -0.76813, 1.79168
+                        ]
+                    },
+                    {
+                        "ct": 3000,
+                        "ccm":
+                        [
+                            2.06374, -0.92218, -0.14156,
+                            -0.41721, 1.69289, -0.27568,
+                            -0.00554, -0.92741, 1.93295
+                        ]
+                    },
+                    {
+                        "ct": 3700,
+                        "ccm":
+                        [
+                            2.13792, -1.08136, -0.05655,
+                            -0.34739, 1.58989, -0.24249,
+                            -0.00349, -0.76789, 1.77138
+                        ]
+                    },
+                    {
+                        "ct": 3870,
+                        "ccm":
+                        [
+                            1.83834, -0.70528, -0.13307,
+                            -0.30499, 1.60523, -0.30024,
+                            -0.05701, -0.58313, 1.64014
+                        ]
+                    },
+                    {
+                        "ct": 4000,
+                        "ccm":
+                        [
+                            2.15741, -1.10295, -0.05447,
+                            -0.34631, 1.61158, -0.26528,
+                            -0.02723, -0.70288, 1.73011
+                        ]
+                    },
+                    {
+                        "ct": 4400,
+                        "ccm":
+                        [
+                            2.05729, -0.95007, -0.10723,
+                            -0.41712, 1.78606, -0.36894,
+                            -0.11899, -0.55727, 1.67626
+                        ]
+                    },
+                    {
+                        "ct": 4715,
+                        "ccm":
+                        [
+                            1.90255, -0.77478, -0.12777,
+                            -0.31338, 1.88197, -0.56858,
+                            -0.06001, -0.61785, 1.67786
+                        ]
+                    },
+                    {
+                        "ct": 5920,
+                        "ccm":
+                        [
+                            1.98691, -0.84671, -0.14019,
+                            -0.26581, 1.70615, -0.44035,
+                            -0.09532, -0.47332, 1.56864
+                        ]
+                    },
+                    {
+                        "ct": 9050,
+                        "ccm":
+                        [
+                            2.09255, -0.76541, -0.32714,
+                            -0.28973, 2.27462, -0.98489,
+                            -0.17299, -0.61275, 1.78574
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx519.json b/src/ipa/rpi/vc4/data/imx519.json
new file mode 100644
index 00000000..8ccfd3a6
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx519.json
@@ -0,0 +1,413 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 13841,
+                "reference_gain": 2.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 900,
+                "reference_Y": 12064
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.776
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 189,
+                "slope": 0.01495
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 7900
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8000
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2890.0, 0.7328, 0.3734,
+                    3550.0, 0.6228, 0.4763,
+                    4500.0, 0.5208, 0.5825,
+                    5700.0, 0.4467, 0.6671,
+                    7900.0, 0.3858, 0.7411
+                ],
+                "sensitivity_r": 1.0,
+                "sensitivity_b": 1.0,
+                "transverse_pos": 0.02027,
+                "transverse_neg": 0.01935
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.527, 1.521, 1.508, 1.493, 1.476, 1.455, 1.442, 1.441, 1.441, 1.441, 1.448, 1.467, 1.483, 1.494, 1.503, 1.504,
+                            1.525, 1.513, 1.496, 1.477, 1.461, 1.434, 1.418, 1.409, 1.409, 1.416, 1.429, 1.449, 1.469, 1.485, 1.495, 1.503,
+                            1.517, 1.506, 1.485, 1.461, 1.434, 1.412, 1.388, 1.376, 1.376, 1.386, 1.405, 1.429, 1.449, 1.471, 1.488, 1.495,
+                            1.512, 1.496, 1.471, 1.442, 1.412, 1.388, 1.361, 1.344, 1.344, 1.358, 1.384, 1.405, 1.431, 1.456, 1.479, 1.489,
+                            1.508, 1.488, 1.458, 1.425, 1.393, 1.361, 1.343, 1.322, 1.321, 1.342, 1.358, 1.385, 1.416, 1.445, 1.471, 1.484,
+                            1.507, 1.482, 1.453, 1.418, 1.382, 1.349, 1.322, 1.318, 1.318, 1.321, 1.345, 1.373, 1.405, 1.437, 1.465, 1.483,
+                            1.507, 1.482, 1.453, 1.418, 1.382, 1.349, 1.322, 1.313, 1.313, 1.321, 1.345, 1.373, 1.405, 1.437, 1.465, 1.483,
+                            1.507, 1.485, 1.455, 1.422, 1.387, 1.355, 1.333, 1.319, 1.321, 1.333, 1.351, 1.381, 1.411, 1.441, 1.467, 1.483,
+                            1.508, 1.489, 1.463, 1.432, 1.401, 1.372, 1.355, 1.333, 1.333, 1.351, 1.369, 1.393, 1.422, 1.448, 1.471, 1.484,
+                            1.511, 1.494, 1.472, 1.444, 1.416, 1.398, 1.372, 1.361, 1.361, 1.369, 1.393, 1.411, 1.436, 1.458, 1.477, 1.487,
+                            1.511, 1.496, 1.478, 1.455, 1.436, 1.416, 1.399, 1.391, 1.391, 1.397, 1.411, 1.429, 1.451, 1.466, 1.479, 1.487,
+                            1.511, 1.495, 1.478, 1.462, 1.448, 1.432, 1.419, 1.419, 1.419, 1.419, 1.429, 1.445, 1.459, 1.471, 1.482, 1.487
+                        ]
+                    },
+                    {
+                        "ct": 6000,
+                        "table":
+                        [
+                            2.581, 2.573, 2.558, 2.539, 2.514, 2.487, 2.473, 2.471, 2.471, 2.471, 2.479, 2.499, 2.517, 2.532, 2.543, 2.544,
+                            2.575, 2.559, 2.539, 2.521, 2.491, 2.458, 2.435, 2.421, 2.421, 2.429, 2.449, 2.477, 2.499, 2.519, 2.534, 2.543,
+                            2.561, 2.549, 2.521, 2.491, 2.457, 2.423, 2.393, 2.375, 2.375, 2.387, 2.412, 2.444, 2.475, 2.499, 2.519, 2.532,
+                            2.552, 2.531, 2.498, 2.459, 2.423, 2.391, 2.349, 2.325, 2.325, 2.344, 2.374, 2.412, 2.444, 2.476, 2.505, 2.519,
+                            2.543, 2.518, 2.479, 2.435, 2.392, 2.349, 2.324, 2.285, 2.283, 2.313, 2.344, 2.374, 2.417, 2.457, 2.489, 2.506,
+                            2.541, 2.511, 2.469, 2.421, 2.372, 2.326, 2.284, 2.277, 2.279, 2.283, 2.313, 2.357, 2.401, 2.443, 2.479, 2.504,
+                            2.541, 2.511, 2.469, 2.421, 2.372, 2.326, 2.284, 2.267, 2.267, 2.281, 2.313, 2.357, 2.401, 2.443, 2.479, 2.504,
+                            2.541, 2.512, 2.472, 2.425, 2.381, 2.338, 2.302, 2.278, 2.279, 2.301, 2.324, 2.364, 2.407, 2.447, 2.481, 2.504,
+                            2.544, 2.519, 2.483, 2.441, 2.401, 2.363, 2.338, 2.302, 2.302, 2.324, 2.355, 2.385, 2.423, 2.459, 2.488, 2.506,
+                            2.549, 2.527, 2.497, 2.463, 2.427, 2.401, 2.363, 2.345, 2.345, 2.355, 2.385, 2.412, 2.444, 2.473, 2.497, 2.509,
+                            2.552, 2.532, 2.507, 2.481, 2.459, 2.427, 2.402, 2.389, 2.389, 2.394, 2.412, 2.444, 2.465, 2.481, 2.499, 2.511,
+                            2.553, 2.533, 2.508, 2.489, 2.475, 2.454, 2.429, 2.429, 2.429, 2.429, 2.439, 2.463, 2.481, 2.492, 2.504, 2.511
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            3.132, 3.126, 3.116, 3.103, 3.097, 3.091, 3.087, 3.086, 3.088, 3.091, 3.092, 3.102, 3.113, 3.121, 3.141, 3.144,
+                            3.149, 3.132, 3.123, 3.108, 3.101, 3.096, 3.091, 3.089, 3.091, 3.092, 3.101, 3.107, 3.116, 3.129, 3.144, 3.153,
+                            3.161, 3.149, 3.129, 3.121, 3.108, 3.103, 3.101, 3.101, 3.101, 3.103, 3.107, 3.116, 3.125, 3.134, 3.153, 3.159,
+                            3.176, 3.161, 3.144, 3.129, 3.124, 3.121, 3.117, 3.118, 3.118, 3.119, 3.122, 3.125, 3.134, 3.146, 3.159, 3.171,
+                            3.183, 3.176, 3.157, 3.144, 3.143, 3.143, 3.139, 3.141, 3.141, 3.141, 3.141, 3.141, 3.146, 3.161, 3.171, 3.179,
+                            3.189, 3.183, 3.165, 3.157, 3.156, 3.157, 3.159, 3.163, 3.163, 3.163, 3.163, 3.161, 3.163, 3.169, 3.179, 3.187,
+                            3.199, 3.189, 3.171, 3.165, 3.164, 3.167, 3.171, 3.173, 3.173, 3.172, 3.171, 3.169, 3.169, 3.175, 3.187, 3.189,
+                            3.206, 3.196, 3.177, 3.171, 3.165, 3.167, 3.171, 3.173, 3.173, 3.172, 3.171, 3.171, 3.173, 3.177, 3.192, 3.194,
+                            3.209, 3.197, 3.178, 3.171, 3.164, 3.161, 3.159, 3.161, 3.162, 3.164, 3.167, 3.171, 3.173, 3.181, 3.193, 3.198,
+                            3.204, 3.194, 3.176, 3.165, 3.161, 3.156, 3.154, 3.154, 3.159, 3.161, 3.164, 3.168, 3.173, 3.182, 3.198, 3.199,
+                            3.199, 3.191, 3.176, 3.169, 3.161, 3.157, 3.153, 3.153, 3.156, 3.161, 3.164, 3.168, 3.173, 3.186, 3.196, 3.199,
+                            3.199, 3.188, 3.179, 3.173, 3.165, 3.157, 3.153, 3.154, 3.156, 3.159, 3.167, 3.171, 3.176, 3.185, 3.193, 3.198
+                        ]
+                    },
+                    {
+                        "ct": 6000,
+                        "table":
+                        [
+                            1.579, 1.579, 1.577, 1.574, 1.573, 1.571, 1.571, 1.571, 1.571, 1.569, 1.569, 1.571, 1.572, 1.574, 1.577, 1.578,
+                            1.584, 1.579, 1.578, 1.575, 1.573, 1.572, 1.571, 1.572, 1.572, 1.571, 1.571, 1.572, 1.573, 1.576, 1.578, 1.579,
+                            1.587, 1.584, 1.579, 1.578, 1.575, 1.573, 1.573, 1.575, 1.575, 1.574, 1.573, 1.574, 1.576, 1.578, 1.581, 1.581,
+                            1.591, 1.587, 1.584, 1.579, 1.578, 1.579, 1.579, 1.581, 1.581, 1.581, 1.578, 1.577, 1.578, 1.581, 1.585, 1.586,
+                            1.595, 1.591, 1.587, 1.585, 1.585, 1.586, 1.587, 1.587, 1.588, 1.588, 1.585, 1.584, 1.584, 1.586, 1.589, 1.589,
+                            1.597, 1.595, 1.591, 1.589, 1.591, 1.593, 1.595, 1.596, 1.597, 1.597, 1.595, 1.594, 1.592, 1.592, 1.593, 1.593,
+                            1.601, 1.597, 1.593, 1.592, 1.593, 1.595, 1.598, 1.599, 1.602, 1.601, 1.598, 1.596, 1.595, 1.596, 1.595, 1.595,
+                            1.601, 1.599, 1.594, 1.593, 1.593, 1.595, 1.598, 1.599, 1.602, 1.601, 1.598, 1.597, 1.597, 1.597, 1.597, 1.597,
+                            1.602, 1.599, 1.594, 1.593, 1.592, 1.593, 1.595, 1.597, 1.597, 1.598, 1.598, 1.597, 1.597, 1.597, 1.598, 1.598,
+                            1.599, 1.598, 1.594, 1.592, 1.591, 1.591, 1.592, 1.595, 1.596, 1.597, 1.597, 1.597, 1.597, 1.599, 1.599, 1.599,
+                            1.598, 1.596, 1.594, 1.593, 1.592, 1.592, 1.592, 1.594, 1.595, 1.597, 1.597, 1.597, 1.598, 1.599, 1.599, 1.599,
+                            1.597, 1.595, 1.594, 1.594, 1.593, 1.592, 1.593, 1.595, 1.595, 1.597, 1.598, 1.598, 1.598, 1.599, 1.599, 1.599
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    2.887, 2.754, 2.381, 2.105, 1.859, 1.678, 1.625, 1.623, 1.623, 1.624, 1.669, 1.849, 2.092, 2.362, 2.723, 2.838,
+                    2.754, 2.443, 2.111, 1.905, 1.678, 1.542, 1.455, 1.412, 1.412, 1.452, 1.535, 1.665, 1.893, 2.096, 2.413, 2.723,
+                    2.443, 2.216, 1.911, 1.678, 1.537, 1.372, 1.288, 1.245, 1.245, 1.283, 1.363, 1.527, 1.665, 1.895, 2.193, 2.413,
+                    2.318, 2.057, 1.764, 1.541, 1.372, 1.282, 1.159, 1.113, 1.113, 1.151, 1.269, 1.363, 1.527, 1.749, 2.034, 2.278,
+                    2.259, 1.953, 1.671, 1.452, 1.283, 1.159, 1.107, 1.018, 1.017, 1.097, 1.151, 1.269, 1.437, 1.655, 1.931, 2.222,
+                    2.257, 1.902, 1.624, 1.408, 1.239, 1.111, 1.019, 1.011, 1.005, 1.014, 1.098, 1.227, 1.395, 1.608, 1.883, 2.222,
+                    2.257, 1.902, 1.624, 1.408, 1.239, 1.111, 1.016, 1.001, 1.001, 1.007, 1.098, 1.227, 1.395, 1.608, 1.883, 2.222,
+                    2.257, 1.946, 1.666, 1.448, 1.281, 1.153, 1.093, 1.013, 1.008, 1.089, 1.143, 1.269, 1.437, 1.654, 1.934, 2.226,
+                    2.309, 2.044, 1.756, 1.532, 1.363, 1.259, 1.153, 1.093, 1.093, 1.143, 1.264, 1.354, 1.524, 1.746, 2.035, 2.284,
+                    2.425, 2.201, 1.896, 1.662, 1.519, 1.363, 1.259, 1.214, 1.214, 1.264, 1.354, 1.519, 1.655, 1.888, 2.191, 2.413,
+                    2.724, 2.417, 2.091, 1.888, 1.662, 1.519, 1.419, 1.373, 1.373, 1.425, 1.521, 1.655, 1.885, 2.089, 2.409, 2.722,
+                    2.858, 2.724, 2.356, 2.085, 1.842, 1.658, 1.581, 1.577, 1.577, 1.579, 1.653, 1.838, 2.084, 2.359, 2.722, 2.842
+                ],
+                "sigma": 0.00372,
+                "sigma_Cb": 0.00244
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2890,
+                        "ccm":
+                        [
+                            1.36754, -0.18448, -0.18306,
+                            -0.32356, 1.44826, -0.12471,
+                            -0.00412, -0.69936, 1.70348
+                        ]
+                    },
+                    {
+                        "ct": 2920,
+                        "ccm":
+                        [
+                            1.26704, 0.01624, -0.28328,
+                            -0.28516, 1.38934, -0.10419,
+                            -0.04854, -0.82211, 1.87066
+                        ]
+                    },
+                    {
+                        "ct": 3550,
+                        "ccm":
+                        [
+                            1.42836, -0.27235, -0.15601,
+                            -0.28751, 1.41075, -0.12325,
+                            -0.01812, -0.54849, 1.56661
+                        ]
+                    },
+                    {
+                        "ct": 4500,
+                        "ccm":
+                        [
+                            1.36328, -0.19569, -0.16759,
+                            -0.25254, 1.52248, -0.26994,
+                            -0.01575, -0.53155, 1.54729
+                        ]
+                    },
+                    {
+                        "ct": 5700,
+                        "ccm":
+                        [
+                            1.49207, -0.37245, -0.11963,
+                            -0.21493, 1.40005, -0.18512,
+                            -0.03781, -0.38779, 1.42561
+                        ]
+                    },
+                    {
+                        "ct": 7900,
+                        "ccm":
+                        [
+                            1.34849, -0.05425, -0.29424,
+                            -0.22182, 1.77684, -0.55502,
+                            -0.07403, -0.55336, 1.62739
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx708.json b/src/ipa/rpi/vc4/data/imx708.json
new file mode 100644
index 00000000..b9830a3b
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx708.json
@@ -0,0 +1,556 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 10672,
+                "reference_gain": 1.12,
+                "reference_aperture": 1.0,
+                "reference_lux": 977,
+                "reference_Y": 8627
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 16.0,
+                "reference_slope": 4.0
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 215,
+                "slope": 0.00287
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2498.0, 0.8733, 0.2606,
+                    2821.0, 0.7707, 0.3245,
+                    2925.0, 0.7338, 0.3499,
+                    2926.0, 0.7193, 0.3603,
+                    2951.0, 0.7144, 0.3639,
+                    2954.0, 0.7111, 0.3663,
+                    3578.0, 0.6038, 0.4516,
+                    3717.0, 0.5861, 0.4669,
+                    3784.0, 0.5786, 0.4737,
+                    4485.0, 0.5113, 0.5368,
+                    4615.0, 0.4994, 0.5486,
+                    4671.0, 0.4927, 0.5554,
+                    5753.0, 0.4274, 0.6246,
+                    5773.0, 0.4265, 0.6256,
+                    7433.0, 0.3723, 0.6881
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.03148,
+                "transverse_neg": 0.03061
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 15000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 1.0, 2.0, 4.0, 6.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 6.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.2,
+                                1000, 0.2
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.2,
+                                1000, 0.2
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ],
+                "startup_frames": 5,
+                "convergence_frames": 6,
+                "speed": 0.15
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.562, 1.566, 1.566, 1.556, 1.533, 1.506, 1.475, 1.475, 1.475, 1.475, 1.506, 1.533, 1.555, 1.563, 1.562, 1.555,
+                            1.563, 1.564, 1.561, 1.538, 1.508, 1.482, 1.449, 1.436, 1.436, 1.449, 1.481, 1.508, 1.537, 1.557, 1.558, 1.557,
+                            1.564, 1.563, 1.554, 1.522, 1.482, 1.449, 1.421, 1.403, 1.403, 1.419, 1.449, 1.481, 1.519, 1.549, 1.557, 1.559,
+                            1.564, 1.563, 1.545, 1.506, 1.462, 1.421, 1.403, 1.378, 1.378, 1.402, 1.419, 1.459, 1.503, 1.541, 1.557, 1.559,
+                            1.564, 1.562, 1.537, 1.494, 1.447, 1.404, 1.378, 1.364, 1.364, 1.377, 1.402, 1.444, 1.491, 1.532, 1.556, 1.559,
+                            1.564, 1.559, 1.532, 1.487, 1.438, 1.395, 1.365, 1.359, 1.359, 1.364, 1.393, 1.436, 1.484, 1.527, 1.555, 1.558,
+                            1.564, 1.559, 1.532, 1.487, 1.438, 1.395, 1.365, 1.356, 1.356, 1.364, 1.393, 1.436, 1.484, 1.527, 1.554, 1.557,
+                            1.564, 1.561, 1.536, 1.492, 1.444, 1.402, 1.374, 1.364, 1.363, 1.373, 1.401, 1.442, 1.489, 1.531, 1.554, 1.557,
+                            1.564, 1.563, 1.544, 1.504, 1.458, 1.418, 1.397, 1.374, 1.374, 1.395, 1.416, 1.456, 1.501, 1.538, 1.556, 1.557,
+                            1.564, 1.562, 1.551, 1.518, 1.477, 1.441, 1.418, 1.397, 1.397, 1.416, 1.438, 1.474, 1.514, 1.546, 1.556, 1.556,
+                            1.562, 1.562, 1.558, 1.534, 1.499, 1.476, 1.441, 1.426, 1.426, 1.438, 1.473, 1.496, 1.531, 1.552, 1.556, 1.555,
+                            1.561, 1.564, 1.564, 1.552, 1.525, 1.497, 1.466, 1.461, 1.461, 1.464, 1.495, 1.523, 1.548, 1.556, 1.556, 1.552
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            2.609, 2.616, 2.617, 2.607, 2.573, 2.527, 2.483, 2.481, 2.481, 2.483, 2.529, 2.573, 2.604, 2.613, 2.613, 2.604,
+                            2.609, 2.615, 2.608, 2.576, 2.533, 2.489, 2.439, 2.418, 2.418, 2.439, 2.491, 2.532, 2.577, 2.605, 2.609, 2.607,
+                            2.611, 2.611, 2.597, 2.551, 2.489, 2.439, 2.391, 2.364, 2.364, 2.391, 2.439, 2.491, 2.551, 2.592, 2.607, 2.609,
+                            2.612, 2.608, 2.583, 2.526, 2.457, 2.391, 2.362, 2.318, 2.318, 2.362, 2.391, 2.458, 2.526, 2.581, 2.607, 2.611,
+                            2.612, 2.604, 2.571, 2.507, 2.435, 2.362, 2.317, 2.293, 2.294, 2.318, 2.363, 2.434, 2.508, 2.568, 2.604, 2.612,
+                            2.611, 2.602, 2.564, 2.496, 2.419, 2.349, 2.293, 2.284, 2.284, 2.294, 2.347, 2.421, 2.497, 2.562, 2.603, 2.611,
+                            2.609, 2.601, 2.564, 2.496, 2.419, 2.349, 2.293, 2.278, 2.278, 2.294, 2.347, 2.421, 2.497, 2.562, 2.602, 2.609,
+                            2.609, 2.602, 2.568, 2.503, 2.429, 2.361, 2.311, 2.292, 2.292, 2.309, 2.357, 2.429, 2.504, 2.567, 2.602, 2.609,
+                            2.606, 2.604, 2.579, 2.519, 2.449, 2.384, 2.348, 2.311, 2.311, 2.346, 2.383, 2.449, 2.521, 2.577, 2.604, 2.608,
+                            2.604, 2.603, 2.586, 2.537, 2.474, 2.418, 2.384, 2.348, 2.348, 2.383, 2.417, 2.476, 2.538, 2.586, 2.601, 2.603,
+                            2.603, 2.605, 2.596, 2.561, 2.508, 2.474, 2.418, 2.396, 2.396, 2.417, 2.474, 2.511, 2.562, 2.596, 2.603, 2.602,
+                            2.601, 2.607, 2.606, 2.589, 2.549, 2.507, 2.456, 2.454, 2.454, 2.458, 2.508, 2.554, 2.594, 2.605, 2.605, 2.602
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            3.221, 3.226, 3.231, 3.236, 3.239, 3.243, 3.245, 3.247, 3.249, 3.253, 3.255, 3.254, 3.253, 3.242, 3.235, 3.226,
+                            3.225, 3.231, 3.235, 3.238, 3.241, 3.244, 3.246, 3.247, 3.249, 3.254, 3.256, 3.255, 3.252, 3.248, 3.241, 3.232,
+                            3.226, 3.234, 3.239, 3.243, 3.243, 3.245, 3.247, 3.248, 3.251, 3.255, 3.256, 3.256, 3.254, 3.249, 3.244, 3.236,
+                            3.232, 3.238, 3.245, 3.245, 3.246, 3.247, 3.248, 3.251, 3.251, 3.256, 3.257, 3.257, 3.256, 3.254, 3.249, 3.239,
+                            3.232, 3.243, 3.246, 3.246, 3.246, 3.247, 3.248, 3.251, 3.253, 3.257, 3.258, 3.258, 3.257, 3.256, 3.254, 3.239,
+                            3.232, 3.242, 3.246, 3.247, 3.246, 3.246, 3.248, 3.251, 3.252, 3.253, 3.256, 3.255, 3.255, 3.254, 3.251, 3.239,
+                            3.233, 3.241, 3.244, 3.245, 3.244, 3.245, 3.246, 3.249, 3.251, 3.252, 3.253, 3.252, 3.252, 3.252, 3.249, 3.238,
+                            3.238, 3.241, 3.246, 3.246, 3.245, 3.245, 3.247, 3.249, 3.251, 3.252, 3.253, 3.253, 3.252, 3.252, 3.249, 3.239,
+                            3.235, 3.241, 3.245, 3.245, 3.245, 3.245, 3.246, 3.247, 3.251, 3.254, 3.253, 3.255, 3.256, 3.255, 3.251, 3.241,
+                            3.226, 3.235, 3.241, 3.241, 3.241, 3.241, 3.243, 3.245, 3.246, 3.252, 3.253, 3.254, 3.256, 3.254, 3.241, 3.237,
+                            3.205, 3.213, 3.213, 3.214, 3.214, 3.214, 3.214, 3.213, 3.213, 3.216, 3.218, 3.216, 3.214, 3.213, 3.211, 3.208,
+                            3.205, 3.205, 3.212, 3.212, 3.212, 3.213, 3.211, 3.211, 3.211, 3.213, 3.216, 3.214, 3.213, 3.211, 3.208, 3.196
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.645, 1.646, 1.649, 1.653, 1.654, 1.657, 1.659, 1.661, 1.663, 1.662, 1.661, 1.659, 1.656, 1.651, 1.645, 1.642,
+                            1.646, 1.649, 1.652, 1.654, 1.656, 1.659, 1.662, 1.663, 1.664, 1.664, 1.662, 1.661, 1.657, 1.653, 1.649, 1.644,
+                            1.648, 1.652, 1.654, 1.656, 1.658, 1.662, 1.665, 1.668, 1.668, 1.668, 1.665, 1.662, 1.658, 1.655, 1.652, 1.646,
+                            1.649, 1.653, 1.656, 1.658, 1.661, 1.665, 1.667, 1.671, 1.673, 1.671, 1.668, 1.663, 1.659, 1.656, 1.654, 1.647,
+                            1.649, 1.655, 1.657, 1.659, 1.661, 1.666, 1.671, 1.674, 1.675, 1.673, 1.671, 1.664, 1.659, 1.656, 1.654, 1.648,
+                            1.649, 1.654, 1.656, 1.659, 1.661, 1.666, 1.673, 1.676, 1.676, 1.675, 1.671, 1.664, 1.659, 1.656, 1.654, 1.648,
+                            1.649, 1.654, 1.656, 1.658, 1.659, 1.665, 1.672, 1.675, 1.675, 1.674, 1.668, 1.662, 1.658, 1.655, 1.654, 1.646,
+                            1.652, 1.655, 1.657, 1.659, 1.661, 1.665, 1.671, 1.673, 1.673, 1.672, 1.668, 1.662, 1.658, 1.655, 1.654, 1.647,
+                            1.652, 1.655, 1.657, 1.659, 1.661, 1.664, 1.667, 1.671, 1.672, 1.668, 1.666, 1.662, 1.659, 1.656, 1.654, 1.647,
+                            1.647, 1.652, 1.655, 1.656, 1.657, 1.661, 1.664, 1.665, 1.665, 1.665, 1.663, 1.661, 1.657, 1.655, 1.647, 1.647,
+                            1.639, 1.642, 1.644, 1.645, 1.646, 1.648, 1.648, 1.648, 1.649, 1.649, 1.649, 1.646, 1.645, 1.642, 1.639, 1.636,
+                            1.639, 1.641, 1.642, 1.644, 1.645, 1.646, 1.647, 1.647, 1.648, 1.648, 1.647, 1.645, 1.642, 1.639, 1.636, 1.633
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    2.644, 2.396, 2.077, 1.863, 1.682, 1.535, 1.392, 1.382, 1.382, 1.382, 1.515, 1.657, 1.826, 2.035, 2.351, 2.604,
+                    2.497, 2.229, 1.947, 1.733, 1.539, 1.424, 1.296, 1.249, 1.249, 1.285, 1.401, 1.519, 1.699, 1.908, 2.183, 2.456,
+                    2.389, 2.109, 1.848, 1.622, 1.424, 1.296, 1.201, 1.146, 1.146, 1.188, 1.285, 1.401, 1.591, 1.811, 2.065, 2.347,
+                    2.317, 2.026, 1.771, 1.535, 1.339, 1.201, 1.145, 1.069, 1.069, 1.134, 1.188, 1.318, 1.505, 1.734, 1.983, 2.273,
+                    2.276, 1.972, 1.715, 1.474, 1.281, 1.148, 1.069, 1.033, 1.024, 1.065, 1.134, 1.262, 1.446, 1.679, 1.929, 2.233,
+                    2.268, 1.941, 1.682, 1.441, 1.251, 1.119, 1.033, 1.013, 1.013, 1.024, 1.105, 1.231, 1.415, 1.649, 1.898, 2.227,
+                    2.268, 1.941, 1.682, 1.441, 1.251, 1.119, 1.033, 1.001, 1.001, 1.024, 1.105, 1.231, 1.415, 1.649, 1.898, 2.227,
+                    2.268, 1.951, 1.694, 1.456, 1.265, 1.131, 1.044, 1.026, 1.019, 1.039, 1.118, 1.246, 1.429, 1.663, 1.912, 2.227,
+                    2.291, 1.992, 1.738, 1.505, 1.311, 1.175, 1.108, 1.044, 1.041, 1.106, 1.161, 1.292, 1.478, 1.707, 1.955, 2.252,
+                    2.347, 2.058, 1.803, 1.581, 1.384, 1.245, 1.175, 1.108, 1.108, 1.161, 1.239, 1.364, 1.551, 1.773, 2.023, 2.311,
+                    2.438, 2.156, 1.884, 1.674, 1.484, 1.373, 1.245, 1.199, 1.199, 1.239, 1.363, 1.463, 1.647, 1.858, 2.123, 2.406,
+                    2.563, 2.305, 1.998, 1.792, 1.615, 1.472, 1.339, 1.322, 1.322, 1.326, 1.456, 1.593, 1.767, 1.973, 2.273, 2.532
+                ],
+                "sigma": 0.00178,
+                "sigma_Cb": 0.00217
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2498,
+                        "ccm":
+                        [
+                            1.14912, 0.28638, -0.43551,
+                            -0.49691, 1.60391, -0.10701,
+                            -0.10513, -1.09534, 2.20047
+                        ]
+                    },
+                    {
+                        "ct": 2821,
+                        "ccm":
+                        [
+                            1.18251, 0.15501, -0.33752,
+                            -0.44304, 1.58495, -0.14191,
+                            -0.05077, -0.96422, 2.01498
+                        ]
+                    },
+                    {
+                        "ct": 2925,
+                        "ccm":
+                        [
+                            1.18668, 0.00195, -0.18864,
+                            -0.41617, 1.50514, -0.08897,
+                            -0.02675, -0.91143, 1.93818
+                        ]
+                    },
+                    {
+                        "ct": 2926,
+                        "ccm":
+                        [
+                            1.50948, -0.44421, -0.06527,
+                            -0.37241, 1.41726, -0.04486,
+                            0.07098, -0.84694, 1.77596
+                        ]
+                    },
+                    {
+                        "ct": 2951,
+                        "ccm":
+                        [
+                            1.52743, -0.47333, -0.05411,
+                            -0.36485, 1.40764, -0.04279,
+                            0.08672, -0.90479, 1.81807
+                        ]
+                    },
+                    {
+                        "ct": 2954,
+                        "ccm":
+                        [
+                            1.51683, -0.46841, -0.04841,
+                            -0.36288, 1.39914, -0.03625,
+                            0.06421, -0.82034, 1.75613
+                        ]
+                    },
+                    {
+                        "ct": 3578,
+                        "ccm":
+                        [
+                            1.59888, -0.59105, -0.00784,
+                            -0.29366, 1.32037, -0.02671,
+                            0.06627, -0.76465, 1.69838
+                        ]
+                    },
+                    {
+                        "ct": 3717,
+                        "ccm":
+                        [
+                            1.59063, -0.58059, -0.01003,
+                            -0.29583, 1.32715, -0.03132,
+                            0.03613, -0.67431, 1.63817
+                        ]
+                    },
+                    {
+                        "ct": 3784,
+                        "ccm":
+                        [
+                            1.59379, -0.58861, -0.00517,
+                            -0.29178, 1.33292, -0.04115,
+                            0.03541, -0.66162, 1.62622
+                        ]
+                    },
+                    {
+                        "ct": 4485,
+                        "ccm":
+                        [
+                            1.40761, -0.34561, -0.06201,
+                            -0.32388, 1.57221, -0.24832,
+                            -0.01014, -0.63427, 1.64441
+                        ]
+                    },
+                    {
+                        "ct": 4615,
+                        "ccm":
+                        [
+                            1.41537, -0.35832, -0.05705,
+                            -0.31429, 1.56019, -0.24591,
+                            -0.01761, -0.61859, 1.63621
+                        ]
+                    },
+                    {
+                        "ct": 4671,
+                        "ccm":
+                        [
+                            1.42941, -0.38178, -0.04764,
+                            -0.31421, 1.55925, -0.24504,
+                            -0.01141, -0.62987, 1.64129
+                        ]
+                    },
+                    {
+                        "ct": 5753,
+                        "ccm":
+                        [
+                            1.64549, -0.63329, -0.01221,
+                            -0.22431, 1.36423, -0.13992,
+                            -0.00831, -0.55373, 1.56204
+                        ]
+                    },
+                    {
+                        "ct": 5773,
+                        "ccm":
+                        [
+                            1.63668, -0.63557, -0.00111,
+                            -0.21919, 1.36234, -0.14315,
+                            -0.00399, -0.57428, 1.57827
+                        ]
+                    },
+                    {
+                        "ct": 7433,
+                        "ccm":
+                        [
+                            1.36007, -0.09277, -0.26729,
+                            -0.36886, 2.09249, -0.72363,
+                            -0.12573, -0.76761, 1.89334
+                        ]
+                    },
+                    {
+                        "ct": 55792,
+                        "ccm":
+                        [
+                            1.65091, -0.63689, -0.01401,
+                            -0.22277, 1.35752, -0.13475,
+                            -0.00943, -0.55091, 1.56033
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        },
+        {
+            "rpi.af":
+            {
+                "ranges":
+                {
+                    "normal":
+                    {
+                        "min": 0.0,
+                        "max": 12.0,
+                        "default": 1.0
+                    },
+                    "macro":
+                    {
+                        "min": 3.0,
+                        "max": 15.0,
+                        "default": 4.0
+                    }
+                },
+                "speeds":
+                {
+                    "normal":
+                    {
+                        "step_coarse": 1.0,
+                        "step_fine": 0.25,
+                        "contrast_ratio": 0.75,
+                        "pdaf_gain": -0.02,
+                        "pdaf_squelch": 0.125,
+                        "max_slew": 2.0,
+                        "pdaf_frames": 20,
+                        "dropout_frames": 6,
+                        "step_frames": 4
+                    }
+                },
+                "conf_epsilon": 8,
+                "conf_thresh": 16,
+                "conf_clip": 512,
+                "skip_frames": 5,
+                "map": [ 0.0, 445, 15.0, 925 ]
+            }
+        }
+    ]
+}
diff --git a/src/ipa/rpi/vc4/data/imx708_noir.json b/src/ipa/rpi/vc4/data/imx708_noir.json
new file mode 100644
index 00000000..075f7035
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx708_noir.json
@@ -0,0 +1,556 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 10672,
+                "reference_gain": 1.12,
+                "reference_aperture": 1.0,
+                "reference_lux": 977,
+                "reference_Y": 8627
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 16.0,
+                "reference_slope": 4.0
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 215,
+                "slope": 0.00287
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 0,
+                "ct_curve":
+                [
+                    2498.0, 0.8733, 0.2606,
+                    2821.0, 0.7707, 0.3245,
+                    2925.0, 0.7338, 0.3499,
+                    2926.0, 0.7193, 0.3603,
+                    2951.0, 0.7144, 0.3639,
+                    2954.0, 0.7111, 0.3663,
+                    3578.0, 0.6038, 0.4516,
+                    3717.0, 0.5861, 0.4669,
+                    3784.0, 0.5786, 0.4737,
+                    4485.0, 0.5113, 0.5368,
+                    4615.0, 0.4994, 0.5486,
+                    4671.0, 0.4927, 0.5554,
+                    5753.0, 0.4274, 0.6246,
+                    5773.0, 0.4265, 0.6256,
+                    7433.0, 0.3723, 0.6881
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.03148,
+                "transverse_neg": 0.03061
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 15000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 1.0, 2.0, 4.0, 6.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 6.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.2,
+                                1000, 0.2
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.2,
+                                1000, 0.2
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ],
+                "startup_frames": 5,
+                "convergence_frames": 6,
+                "speed": 0.15
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.562, 1.566, 1.566, 1.556, 1.533, 1.506, 1.475, 1.475, 1.475, 1.475, 1.506, 1.533, 1.555, 1.563, 1.562, 1.555,
+                            1.563, 1.564, 1.561, 1.538, 1.508, 1.482, 1.449, 1.436, 1.436, 1.449, 1.481, 1.508, 1.537, 1.557, 1.558, 1.557,
+                            1.564, 1.563, 1.554, 1.522, 1.482, 1.449, 1.421, 1.403, 1.403, 1.419, 1.449, 1.481, 1.519, 1.549, 1.557, 1.559,
+                            1.564, 1.563, 1.545, 1.506, 1.462, 1.421, 1.403, 1.378, 1.378, 1.402, 1.419, 1.459, 1.503, 1.541, 1.557, 1.559,
+                            1.564, 1.562, 1.537, 1.494, 1.447, 1.404, 1.378, 1.364, 1.364, 1.377, 1.402, 1.444, 1.491, 1.532, 1.556, 1.559,
+                            1.564, 1.559, 1.532, 1.487, 1.438, 1.395, 1.365, 1.359, 1.359, 1.364, 1.393, 1.436, 1.484, 1.527, 1.555, 1.558,
+                            1.564, 1.559, 1.532, 1.487, 1.438, 1.395, 1.365, 1.356, 1.356, 1.364, 1.393, 1.436, 1.484, 1.527, 1.554, 1.557,
+                            1.564, 1.561, 1.536, 1.492, 1.444, 1.402, 1.374, 1.364, 1.363, 1.373, 1.401, 1.442, 1.489, 1.531, 1.554, 1.557,
+                            1.564, 1.563, 1.544, 1.504, 1.458, 1.418, 1.397, 1.374, 1.374, 1.395, 1.416, 1.456, 1.501, 1.538, 1.556, 1.557,
+                            1.564, 1.562, 1.551, 1.518, 1.477, 1.441, 1.418, 1.397, 1.397, 1.416, 1.438, 1.474, 1.514, 1.546, 1.556, 1.556,
+                            1.562, 1.562, 1.558, 1.534, 1.499, 1.476, 1.441, 1.426, 1.426, 1.438, 1.473, 1.496, 1.531, 1.552, 1.556, 1.555,
+                            1.561, 1.564, 1.564, 1.552, 1.525, 1.497, 1.466, 1.461, 1.461, 1.464, 1.495, 1.523, 1.548, 1.556, 1.556, 1.552
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            2.609, 2.616, 2.617, 2.607, 2.573, 2.527, 2.483, 2.481, 2.481, 2.483, 2.529, 2.573, 2.604, 2.613, 2.613, 2.604,
+                            2.609, 2.615, 2.608, 2.576, 2.533, 2.489, 2.439, 2.418, 2.418, 2.439, 2.491, 2.532, 2.577, 2.605, 2.609, 2.607,
+                            2.611, 2.611, 2.597, 2.551, 2.489, 2.439, 2.391, 2.364, 2.364, 2.391, 2.439, 2.491, 2.551, 2.592, 2.607, 2.609,
+                            2.612, 2.608, 2.583, 2.526, 2.457, 2.391, 2.362, 2.318, 2.318, 2.362, 2.391, 2.458, 2.526, 2.581, 2.607, 2.611,
+                            2.612, 2.604, 2.571, 2.507, 2.435, 2.362, 2.317, 2.293, 2.294, 2.318, 2.363, 2.434, 2.508, 2.568, 2.604, 2.612,
+                            2.611, 2.602, 2.564, 2.496, 2.419, 2.349, 2.293, 2.284, 2.284, 2.294, 2.347, 2.421, 2.497, 2.562, 2.603, 2.611,
+                            2.609, 2.601, 2.564, 2.496, 2.419, 2.349, 2.293, 2.278, 2.278, 2.294, 2.347, 2.421, 2.497, 2.562, 2.602, 2.609,
+                            2.609, 2.602, 2.568, 2.503, 2.429, 2.361, 2.311, 2.292, 2.292, 2.309, 2.357, 2.429, 2.504, 2.567, 2.602, 2.609,
+                            2.606, 2.604, 2.579, 2.519, 2.449, 2.384, 2.348, 2.311, 2.311, 2.346, 2.383, 2.449, 2.521, 2.577, 2.604, 2.608,
+                            2.604, 2.603, 2.586, 2.537, 2.474, 2.418, 2.384, 2.348, 2.348, 2.383, 2.417, 2.476, 2.538, 2.586, 2.601, 2.603,
+                            2.603, 2.605, 2.596, 2.561, 2.508, 2.474, 2.418, 2.396, 2.396, 2.417, 2.474, 2.511, 2.562, 2.596, 2.603, 2.602,
+                            2.601, 2.607, 2.606, 2.589, 2.549, 2.507, 2.456, 2.454, 2.454, 2.458, 2.508, 2.554, 2.594, 2.605, 2.605, 2.602
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            3.221, 3.226, 3.231, 3.236, 3.239, 3.243, 3.245, 3.247, 3.249, 3.253, 3.255, 3.254, 3.253, 3.242, 3.235, 3.226,
+                            3.225, 3.231, 3.235, 3.238, 3.241, 3.244, 3.246, 3.247, 3.249, 3.254, 3.256, 3.255, 3.252, 3.248, 3.241, 3.232,
+                            3.226, 3.234, 3.239, 3.243, 3.243, 3.245, 3.247, 3.248, 3.251, 3.255, 3.256, 3.256, 3.254, 3.249, 3.244, 3.236,
+                            3.232, 3.238, 3.245, 3.245, 3.246, 3.247, 3.248, 3.251, 3.251, 3.256, 3.257, 3.257, 3.256, 3.254, 3.249, 3.239,
+                            3.232, 3.243, 3.246, 3.246, 3.246, 3.247, 3.248, 3.251, 3.253, 3.257, 3.258, 3.258, 3.257, 3.256, 3.254, 3.239,
+                            3.232, 3.242, 3.246, 3.247, 3.246, 3.246, 3.248, 3.251, 3.252, 3.253, 3.256, 3.255, 3.255, 3.254, 3.251, 3.239,
+                            3.233, 3.241, 3.244, 3.245, 3.244, 3.245, 3.246, 3.249, 3.251, 3.252, 3.253, 3.252, 3.252, 3.252, 3.249, 3.238,
+                            3.238, 3.241, 3.246, 3.246, 3.245, 3.245, 3.247, 3.249, 3.251, 3.252, 3.253, 3.253, 3.252, 3.252, 3.249, 3.239,
+                            3.235, 3.241, 3.245, 3.245, 3.245, 3.245, 3.246, 3.247, 3.251, 3.254, 3.253, 3.255, 3.256, 3.255, 3.251, 3.241,
+                            3.226, 3.235, 3.241, 3.241, 3.241, 3.241, 3.243, 3.245, 3.246, 3.252, 3.253, 3.254, 3.256, 3.254, 3.241, 3.237,
+                            3.205, 3.213, 3.213, 3.214, 3.214, 3.214, 3.214, 3.213, 3.213, 3.216, 3.218, 3.216, 3.214, 3.213, 3.211, 3.208,
+                            3.205, 3.205, 3.212, 3.212, 3.212, 3.213, 3.211, 3.211, 3.211, 3.213, 3.216, 3.214, 3.213, 3.211, 3.208, 3.196
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.645, 1.646, 1.649, 1.653, 1.654, 1.657, 1.659, 1.661, 1.663, 1.662, 1.661, 1.659, 1.656, 1.651, 1.645, 1.642,
+                            1.646, 1.649, 1.652, 1.654, 1.656, 1.659, 1.662, 1.663, 1.664, 1.664, 1.662, 1.661, 1.657, 1.653, 1.649, 1.644,
+                            1.648, 1.652, 1.654, 1.656, 1.658, 1.662, 1.665, 1.668, 1.668, 1.668, 1.665, 1.662, 1.658, 1.655, 1.652, 1.646,
+                            1.649, 1.653, 1.656, 1.658, 1.661, 1.665, 1.667, 1.671, 1.673, 1.671, 1.668, 1.663, 1.659, 1.656, 1.654, 1.647,
+                            1.649, 1.655, 1.657, 1.659, 1.661, 1.666, 1.671, 1.674, 1.675, 1.673, 1.671, 1.664, 1.659, 1.656, 1.654, 1.648,
+                            1.649, 1.654, 1.656, 1.659, 1.661, 1.666, 1.673, 1.676, 1.676, 1.675, 1.671, 1.664, 1.659, 1.656, 1.654, 1.648,
+                            1.649, 1.654, 1.656, 1.658, 1.659, 1.665, 1.672, 1.675, 1.675, 1.674, 1.668, 1.662, 1.658, 1.655, 1.654, 1.646,
+                            1.652, 1.655, 1.657, 1.659, 1.661, 1.665, 1.671, 1.673, 1.673, 1.672, 1.668, 1.662, 1.658, 1.655, 1.654, 1.647,
+                            1.652, 1.655, 1.657, 1.659, 1.661, 1.664, 1.667, 1.671, 1.672, 1.668, 1.666, 1.662, 1.659, 1.656, 1.654, 1.647,
+                            1.647, 1.652, 1.655, 1.656, 1.657, 1.661, 1.664, 1.665, 1.665, 1.665, 1.663, 1.661, 1.657, 1.655, 1.647, 1.647,
+                            1.639, 1.642, 1.644, 1.645, 1.646, 1.648, 1.648, 1.648, 1.649, 1.649, 1.649, 1.646, 1.645, 1.642, 1.639, 1.636,
+                            1.639, 1.641, 1.642, 1.644, 1.645, 1.646, 1.647, 1.647, 1.648, 1.648, 1.647, 1.645, 1.642, 1.639, 1.636, 1.633
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    2.644, 2.396, 2.077, 1.863, 1.682, 1.535, 1.392, 1.382, 1.382, 1.382, 1.515, 1.657, 1.826, 2.035, 2.351, 2.604,
+                    2.497, 2.229, 1.947, 1.733, 1.539, 1.424, 1.296, 1.249, 1.249, 1.285, 1.401, 1.519, 1.699, 1.908, 2.183, 2.456,
+                    2.389, 2.109, 1.848, 1.622, 1.424, 1.296, 1.201, 1.146, 1.146, 1.188, 1.285, 1.401, 1.591, 1.811, 2.065, 2.347,
+                    2.317, 2.026, 1.771, 1.535, 1.339, 1.201, 1.145, 1.069, 1.069, 1.134, 1.188, 1.318, 1.505, 1.734, 1.983, 2.273,
+                    2.276, 1.972, 1.715, 1.474, 1.281, 1.148, 1.069, 1.033, 1.024, 1.065, 1.134, 1.262, 1.446, 1.679, 1.929, 2.233,
+                    2.268, 1.941, 1.682, 1.441, 1.251, 1.119, 1.033, 1.013, 1.013, 1.024, 1.105, 1.231, 1.415, 1.649, 1.898, 2.227,
+                    2.268, 1.941, 1.682, 1.441, 1.251, 1.119, 1.033, 1.001, 1.001, 1.024, 1.105, 1.231, 1.415, 1.649, 1.898, 2.227,
+                    2.268, 1.951, 1.694, 1.456, 1.265, 1.131, 1.044, 1.026, 1.019, 1.039, 1.118, 1.246, 1.429, 1.663, 1.912, 2.227,
+                    2.291, 1.992, 1.738, 1.505, 1.311, 1.175, 1.108, 1.044, 1.041, 1.106, 1.161, 1.292, 1.478, 1.707, 1.955, 2.252,
+                    2.347, 2.058, 1.803, 1.581, 1.384, 1.245, 1.175, 1.108, 1.108, 1.161, 1.239, 1.364, 1.551, 1.773, 2.023, 2.311,
+                    2.438, 2.156, 1.884, 1.674, 1.484, 1.373, 1.245, 1.199, 1.199, 1.239, 1.363, 1.463, 1.647, 1.858, 2.123, 2.406,
+                    2.563, 2.305, 1.998, 1.792, 1.615, 1.472, 1.339, 1.322, 1.322, 1.326, 1.456, 1.593, 1.767, 1.973, 2.273, 2.532
+                ],
+                "sigma": 0.00178,
+                "sigma_Cb": 0.00217
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2498,
+                        "ccm":
+                        [
+                            1.14912, 0.28638, -0.43551,
+                            -0.49691, 1.60391, -0.10701,
+                            -0.10513, -1.09534, 2.20047
+                        ]
+                    },
+                    {
+                        "ct": 2821,
+                        "ccm":
+                        [
+                            1.18251, 0.15501, -0.33752,
+                            -0.44304, 1.58495, -0.14191,
+                            -0.05077, -0.96422, 2.01498
+                        ]
+                    },
+                    {
+                        "ct": 2925,
+                        "ccm":
+                        [
+                            1.18668, 0.00195, -0.18864,
+                            -0.41617, 1.50514, -0.08897,
+                            -0.02675, -0.91143, 1.93818
+                        ]
+                    },
+                    {
+                        "ct": 2926,
+                        "ccm":
+                        [
+                            1.50948, -0.44421, -0.06527,
+                            -0.37241, 1.41726, -0.04486,
+                            0.07098, -0.84694, 1.77596
+                        ]
+                    },
+                    {
+                        "ct": 2951,
+                        "ccm":
+                        [
+                            1.52743, -0.47333, -0.05411,
+                            -0.36485, 1.40764, -0.04279,
+                            0.08672, -0.90479, 1.81807
+                        ]
+                    },
+                    {
+                        "ct": 2954,
+                        "ccm":
+                        [
+                            1.51683, -0.46841, -0.04841,
+                            -0.36288, 1.39914, -0.03625,
+                            0.06421, -0.82034, 1.75613
+                        ]
+                    },
+                    {
+                        "ct": 3578,
+                        "ccm":
+                        [
+                            1.59888, -0.59105, -0.00784,
+                            -0.29366, 1.32037, -0.02671,
+                            0.06627, -0.76465, 1.69838
+                        ]
+                    },
+                    {
+                        "ct": 3717,
+                        "ccm":
+                        [
+                            1.59063, -0.58059, -0.01003,
+                            -0.29583, 1.32715, -0.03132,
+                            0.03613, -0.67431, 1.63817
+                        ]
+                    },
+                    {
+                        "ct": 3784,
+                        "ccm":
+                        [
+                            1.59379, -0.58861, -0.00517,
+                            -0.29178, 1.33292, -0.04115,
+                            0.03541, -0.66162, 1.62622
+                        ]
+                    },
+                    {
+                        "ct": 4485,
+                        "ccm":
+                        [
+                            1.40761, -0.34561, -0.06201,
+                            -0.32388, 1.57221, -0.24832,
+                            -0.01014, -0.63427, 1.64441
+                        ]
+                    },
+                    {
+                        "ct": 4615,
+                        "ccm":
+                        [
+                            1.41537, -0.35832, -0.05705,
+                            -0.31429, 1.56019, -0.24591,
+                            -0.01761, -0.61859, 1.63621
+                        ]
+                    },
+                    {
+                        "ct": 4671,
+                        "ccm":
+                        [
+                            1.42941, -0.38178, -0.04764,
+                            -0.31421, 1.55925, -0.24504,
+                            -0.01141, -0.62987, 1.64129
+                        ]
+                    },
+                    {
+                        "ct": 5753,
+                        "ccm":
+                        [
+                            1.64549, -0.63329, -0.01221,
+                            -0.22431, 1.36423, -0.13992,
+                            -0.00831, -0.55373, 1.56204
+                        ]
+                    },
+                    {
+                        "ct": 5773,
+                        "ccm":
+                        [
+                            1.63668, -0.63557, -0.00111,
+                            -0.21919, 1.36234, -0.14315,
+                            -0.00399, -0.57428, 1.57827
+                        ]
+                    },
+                    {
+                        "ct": 7433,
+                        "ccm":
+                        [
+                            1.36007, -0.09277, -0.26729,
+                            -0.36886, 2.09249, -0.72363,
+                            -0.12573, -0.76761, 1.89334
+                        ]
+                    },
+                    {
+                        "ct": 55792,
+                        "ccm":
+                        [
+                            1.65091, -0.63689, -0.01401,
+                            -0.22277, 1.35752, -0.13475,
+                            -0.00943, -0.55091, 1.56033
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        },
+        {
+            "rpi.af":
+            {
+                "ranges":
+                {
+                    "normal":
+                    {
+                        "min": 0.0,
+                        "max": 12.0,
+                        "default": 1.0
+                    },
+                    "macro":
+                    {
+                        "min": 3.0,
+                        "max": 15.0,
+                        "default": 4.0
+                    }
+                },
+                "speeds":
+                {
+                    "normal":
+                    {
+                        "step_coarse": 1.0,
+                        "step_fine": 0.25,
+                        "contrast_ratio": 0.75,
+                        "pdaf_gain": -0.02,
+                        "pdaf_squelch": 0.125,
+                        "max_slew": 2.0,
+                        "pdaf_frames": 20,
+                        "dropout_frames": 6,
+                        "step_frames": 4
+                    }
+                },
+                "conf_epsilon": 8,
+                "conf_thresh": 16,
+                "conf_clip": 512,
+                "skip_frames": 5,
+                "map": [ 0.0, 445, 15.0, 925 ]
+            }
+        }
+    ]
+}
diff --git a/src/ipa/rpi/vc4/data/imx708_wide.json b/src/ipa/rpi/vc4/data/imx708_wide.json
new file mode 100644
index 00000000..b772efee
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx708_wide.json
@@ -0,0 +1,459 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 9989,
+                "reference_gain": 1.23,
+                "reference_aperture": 1.0,
+                "reference_lux": 980,
+                "reference_Y": 8345
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 16.0,
+                "reference_slope": 4.0
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 215,
+                "slope": 0.00287
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2750.0, 0.7881, 0.2849,
+                    2940.0, 0.7559, 0.3103,
+                    3650.0, 0.6291, 0.4206,
+                    4625.0, 0.5336, 0.5161,
+                    5715.0, 0.4668, 0.5898
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.01165,
+                "transverse_neg": 0.01601
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 15000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 1.0, 2.0, 4.0, 6.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 6.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.2,
+                                1000, 0.2
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.2,
+                                1000, 0.2
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ],
+                "startup_frames": 5,
+                "convergence_frames": 6,
+                "speed": 0.15
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.529, 1.526, 1.522, 1.506, 1.489, 1.473, 1.458, 1.456, 1.456, 1.458, 1.474, 1.493, 1.513, 1.531, 1.541, 1.544,
+                            1.527, 1.523, 1.511, 1.491, 1.474, 1.459, 1.445, 1.441, 1.441, 1.446, 1.461, 1.479, 1.499, 1.521, 1.536, 1.541,
+                            1.524, 1.515, 1.498, 1.477, 1.459, 1.444, 1.431, 1.426, 1.426, 1.435, 1.446, 1.466, 1.487, 1.507, 1.528, 1.538,
+                            1.522, 1.512, 1.491, 1.468, 1.447, 1.431, 1.423, 1.417, 1.418, 1.425, 1.435, 1.455, 1.479, 1.499, 1.523, 1.537,
+                            1.522, 1.509, 1.485, 1.463, 1.441, 1.423, 1.416, 1.413, 1.415, 1.418, 1.429, 1.449, 1.473, 1.495, 1.521, 1.538,
+                            1.522, 1.508, 1.483, 1.461, 1.438, 1.421, 1.413, 1.412, 1.412, 1.415, 1.428, 1.447, 1.471, 1.493, 1.519, 1.538,
+                            1.522, 1.509, 1.484, 1.462, 1.439, 1.421, 1.414, 1.411, 1.412, 1.416, 1.428, 1.447, 1.471, 1.493, 1.519, 1.537,
+                            1.523, 1.511, 1.487, 1.465, 1.443, 1.424, 1.417, 1.413, 1.415, 1.419, 1.429, 1.451, 1.473, 1.494, 1.519, 1.536,
+                            1.524, 1.514, 1.493, 1.471, 1.451, 1.434, 1.424, 1.419, 1.419, 1.428, 1.437, 1.457, 1.477, 1.498, 1.521, 1.538,
+                            1.527, 1.521, 1.503, 1.481, 1.462, 1.449, 1.434, 1.429, 1.429, 1.437, 1.451, 1.469, 1.488, 1.508, 1.527, 1.539,
+                            1.529, 1.527, 1.515, 1.495, 1.477, 1.462, 1.449, 1.444, 1.444, 1.451, 1.467, 1.481, 1.499, 1.519, 1.535, 1.543,
+                            1.534, 1.531, 1.527, 1.512, 1.492, 1.476, 1.463, 1.461, 1.461, 1.464, 1.479, 1.495, 1.515, 1.533, 1.543, 1.546
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            2.603, 2.599, 2.591, 2.567, 2.539, 2.515, 2.489, 2.489, 2.489, 2.491, 2.516, 2.543, 2.574, 2.597, 2.614, 2.617,
+                            2.596, 2.591, 2.571, 2.542, 2.516, 2.489, 2.464, 2.458, 2.458, 2.469, 2.492, 2.518, 2.547, 2.576, 2.602, 2.614,
+                            2.591, 2.576, 2.546, 2.519, 2.489, 2.464, 2.437, 2.427, 2.427, 2.441, 2.467, 2.492, 2.525, 2.553, 2.586, 2.605,
+                            2.588, 2.568, 2.534, 2.503, 2.472, 2.437, 2.423, 2.409, 2.411, 2.425, 2.441, 2.475, 2.513, 2.541, 2.577, 2.602,
+                            2.588, 2.565, 2.527, 2.494, 2.461, 2.425, 2.409, 2.399, 2.403, 2.409, 2.431, 2.466, 2.503, 2.534, 2.571, 2.601,
+                            2.586, 2.561, 2.525, 2.491, 2.454, 2.418, 2.399, 2.396, 2.395, 2.402, 2.424, 2.461, 2.501, 2.531, 2.567, 2.599,
+                            2.583, 2.559, 2.525, 2.491, 2.454, 2.418, 2.398, 2.393, 2.393, 2.401, 2.423, 2.459, 2.498, 2.531, 2.566, 2.597,
+                            2.583, 2.559, 2.526, 2.494, 2.458, 2.421, 2.404, 2.397, 2.399, 2.404, 2.426, 2.461, 2.501, 2.531, 2.566, 2.596,
+                            2.583, 2.563, 2.531, 2.501, 2.469, 2.435, 2.419, 2.405, 2.404, 2.422, 2.435, 2.471, 2.505, 2.537, 2.572, 2.596,
+                            2.585, 2.571, 2.539, 2.516, 2.486, 2.458, 2.435, 2.424, 2.424, 2.435, 2.459, 2.489, 2.521, 2.546, 2.579, 2.601,
+                            2.589, 2.578, 2.557, 2.532, 2.506, 2.483, 2.458, 2.449, 2.449, 2.459, 2.485, 2.507, 2.535, 2.563, 2.591, 2.605,
+                            2.589, 2.586, 2.575, 2.551, 2.525, 2.503, 2.481, 2.476, 2.476, 2.481, 2.504, 2.526, 2.555, 2.583, 2.604, 2.611
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            3.311, 3.339, 3.369, 3.374, 3.371, 3.363, 3.356, 3.353, 3.353, 3.353, 3.357, 3.362, 3.362, 3.356, 3.328, 3.311,
+                            3.321, 3.354, 3.374, 3.374, 3.368, 3.359, 3.352, 3.349, 3.347, 3.347, 3.349, 3.357, 3.361, 3.359, 3.343, 3.324,
+                            3.334, 3.368, 3.375, 3.374, 3.365, 3.356, 3.349, 3.347, 3.346, 3.346, 3.347, 3.349, 3.358, 3.361, 3.357, 3.336,
+                            3.346, 3.378, 3.378, 3.369, 3.363, 3.358, 3.351, 3.348, 3.347, 3.346, 3.347, 3.348, 3.354, 3.364, 3.363, 3.345,
+                            3.351, 3.381, 3.381, 3.368, 3.361, 3.357, 3.349, 3.347, 3.347, 3.345, 3.345, 3.347, 3.353, 3.364, 3.364, 3.347,
+                            3.353, 3.379, 3.379, 3.366, 3.359, 3.351, 3.348, 3.343, 3.342, 3.342, 3.343, 3.345, 3.351, 3.363, 3.363, 3.347,
+                            3.353, 3.376, 3.376, 3.363, 3.351, 3.347, 3.343, 3.338, 3.336, 3.338, 3.339, 3.343, 3.351, 3.361, 3.361, 3.347,
+                            3.351, 3.374, 3.374, 3.359, 3.351, 3.345, 3.338, 3.334, 3.333, 3.334, 3.336, 3.339, 3.347, 3.358, 3.358, 3.345,
+                            3.346, 3.368, 3.368, 3.359, 3.349, 3.343, 3.336, 3.332, 3.327, 3.331, 3.333, 3.337, 3.346, 3.356, 3.356, 3.341,
+                            3.336, 3.362, 3.364, 3.359, 3.351, 3.342, 3.334, 3.324, 3.324, 3.325, 3.329, 3.336, 3.346, 3.351, 3.351, 3.333,
+                            3.324, 3.349, 3.359, 3.358, 3.352, 3.341, 3.329, 3.323, 3.321, 3.322, 3.326, 3.336, 3.346, 3.347, 3.339, 3.319,
+                            3.311, 3.328, 3.352, 3.354, 3.352, 3.341, 3.329, 3.321, 3.319, 3.321, 3.324, 3.338, 3.343, 3.343, 3.319, 3.312
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.634, 1.647, 1.665, 1.668, 1.668, 1.664, 1.662, 1.662, 1.661, 1.661, 1.661, 1.663, 1.663, 1.659, 1.643, 1.636,
+                            1.639, 1.656, 1.668, 1.669, 1.668, 1.666, 1.664, 1.663, 1.663, 1.661, 1.661, 1.662, 1.663, 1.662, 1.654, 1.642,
+                            1.645, 1.663, 1.669, 1.668, 1.667, 1.667, 1.667, 1.668, 1.668, 1.665, 1.662, 1.661, 1.662, 1.664, 1.661, 1.649,
+                            1.651, 1.669, 1.669, 1.667, 1.666, 1.668, 1.669, 1.672, 1.672, 1.668, 1.665, 1.661, 1.661, 1.665, 1.665, 1.655,
+                            1.654, 1.669, 1.669, 1.666, 1.666, 1.669, 1.672, 1.673, 1.673, 1.671, 1.666, 1.661, 1.661, 1.665, 1.665, 1.659,
+                            1.654, 1.669, 1.669, 1.666, 1.666, 1.669, 1.671, 1.673, 1.672, 1.669, 1.667, 1.661, 1.661, 1.665, 1.665, 1.659,
+                            1.654, 1.668, 1.668, 1.664, 1.663, 1.667, 1.669, 1.671, 1.669, 1.668, 1.665, 1.661, 1.661, 1.663, 1.663, 1.659,
+                            1.653, 1.665, 1.665, 1.661, 1.661, 1.664, 1.667, 1.668, 1.668, 1.665, 1.661, 1.658, 1.659, 1.662, 1.662, 1.657,
+                            1.651, 1.664, 1.664, 1.659, 1.659, 1.661, 1.663, 1.663, 1.662, 1.661, 1.658, 1.656, 1.657, 1.662, 1.662, 1.655,
+                            1.645, 1.661, 1.663, 1.661, 1.659, 1.659, 1.659, 1.657, 1.657, 1.656, 1.654, 1.655, 1.656, 1.661, 1.661, 1.649,
+                            1.641, 1.654, 1.661, 1.661, 1.659, 1.657, 1.655, 1.653, 1.652, 1.651, 1.652, 1.653, 1.657, 1.658, 1.655, 1.644,
+                            1.635, 1.645, 1.661, 1.661, 1.661, 1.655, 1.653, 1.649, 1.648, 1.647, 1.651, 1.653, 1.657, 1.657, 1.646, 1.638
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    3.535, 3.279, 3.049, 2.722, 2.305, 1.958, 1.657, 1.647, 1.647, 1.656, 1.953, 2.289, 2.707, 3.058, 3.325, 3.589,
+                    3.379, 3.157, 2.874, 2.421, 1.973, 1.735, 1.472, 1.388, 1.388, 1.471, 1.724, 1.963, 2.409, 2.877, 3.185, 3.416,
+                    3.288, 3.075, 2.696, 2.169, 1.735, 1.472, 1.311, 1.208, 1.208, 1.306, 1.471, 1.724, 2.159, 2.695, 3.092, 3.321,
+                    3.238, 3.001, 2.534, 1.981, 1.572, 1.311, 1.207, 1.082, 1.082, 1.204, 1.306, 1.563, 1.973, 2.529, 3.008, 3.259,
+                    3.211, 2.938, 2.414, 1.859, 1.468, 1.221, 1.082, 1.036, 1.031, 1.079, 1.217, 1.463, 1.851, 2.403, 2.931, 3.229,
+                    3.206, 2.904, 2.356, 1.802, 1.421, 1.181, 1.037, 1.002, 1.002, 1.032, 1.175, 1.414, 1.793, 2.343, 2.899, 3.223,
+                    3.206, 2.904, 2.356, 1.802, 1.421, 1.181, 1.037, 1.005, 1.005, 1.032, 1.175, 1.414, 1.793, 2.343, 2.899, 3.223,
+                    3.211, 2.936, 2.417, 1.858, 1.468, 1.222, 1.083, 1.037, 1.032, 1.083, 1.218, 1.463, 1.848, 2.403, 2.932, 3.226,
+                    3.234, 2.997, 2.536, 1.979, 1.569, 1.311, 1.206, 1.084, 1.084, 1.204, 1.305, 1.565, 1.966, 2.524, 2.996, 3.251,
+                    3.282, 3.069, 2.697, 2.166, 1.731, 1.471, 1.311, 1.207, 1.207, 1.305, 1.466, 1.729, 2.158, 2.689, 3.077, 3.304,
+                    3.369, 3.146, 2.873, 2.415, 1.964, 1.722, 1.471, 1.382, 1.382, 1.466, 1.722, 1.964, 2.408, 2.871, 3.167, 3.401,
+                    3.524, 3.253, 3.025, 2.691, 2.275, 1.939, 1.657, 1.628, 1.628, 1.654, 1.936, 2.275, 2.687, 3.029, 3.284, 3.574
+                ],
+                "sigma": 0.00195,
+                "sigma_Cb": 0.00241
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2750,
+                        "ccm":
+                        [
+                            1.13004, 0.36392, -0.49396,
+                            -0.45885, 1.68171, -0.22286,
+                            -0.06473, -0.86962, 1.93435
+                        ]
+                    },
+                    {
+                        "ct": 2940,
+                        "ccm":
+                        [
+                            1.29876, 0.09627, -0.39503,
+                            -0.43085, 1.60258, -0.17172,
+                            -0.02638, -0.92581, 1.95218
+                        ]
+                    },
+                    {
+                        "ct": 3650,
+                        "ccm":
+                        [
+                            1.57729, -0.29734, -0.27995,
+                            -0.42965, 1.66231, -0.23265,
+                            -0.02183, -0.62331, 1.64514
+                        ]
+                    },
+                    {
+                        "ct": 4625,
+                        "ccm":
+                        [
+                            1.52145, -0.22382, -0.29763,
+                            -0.40445, 1.82186, -0.41742,
+                            -0.05732, -0.56222, 1.61954
+                        ]
+                    },
+                    {
+                        "ct": 5715,
+                        "ccm":
+                        [
+                            1.67851, -0.39193, -0.28658,
+                            -0.37169, 1.72949, -0.35781,
+                            -0.09556, -0.41951, 1.51508
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        },
+        {
+            "rpi.af":
+            {
+                "ranges":
+                {
+                    "normal":
+                    {
+                        "min": 0.0,
+                        "max": 12.0,
+                        "default": 1.0
+                    },
+                    "macro":
+                    {
+                        "min": 4.0,
+                        "max": 32.0,
+                        "default": 6.0
+                    }
+                },
+                "speeds":
+                {
+                    "normal":
+                    {
+                        "step_coarse": 2.0,
+                        "step_fine": 0.5,
+                        "contrast_ratio": 0.75,
+                        "pdaf_gain": -0.03,
+                        "pdaf_squelch": 0.2,
+                        "max_slew": 4.0,
+                        "pdaf_frames": 20,
+                        "dropout_frames": 6,
+                        "step_frames": 4
+                    },
+                    "fast":
+                    {
+                        "step_coarse": 2.0,
+                        "step_fine": 0.5,
+                        "contrast_ratio": 0.75,
+                        "pdaf_gain": -0.05,
+                        "pdaf_squelch": 0.2,
+                        "max_slew": 5.0,
+                        "pdaf_frames": 16,
+                        "dropout_frames": 6,
+                        "step_frames": 4
+                    }
+                },
+                "conf_epsilon": 8,
+                "conf_thresh": 12,
+                "conf_clip": 512,
+                "skip_frames": 5,
+                "map": [ 0.0, 420, 35.0, 920 ]
+            }
+        }
+    ]
+}
diff --git a/src/ipa/rpi/vc4/data/imx708_wide_noir.json b/src/ipa/rpi/vc4/data/imx708_wide_noir.json
new file mode 100644
index 00000000..c5f6b53d
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/imx708_wide_noir.json
@@ -0,0 +1,459 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 9989,
+                "reference_gain": 1.23,
+                "reference_aperture": 1.0,
+                "reference_lux": 980,
+                "reference_Y": 8345
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 16.0,
+                "reference_slope": 4.0
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 215,
+                "slope": 0.00287
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 0,
+                "ct_curve":
+                [
+                    2750.0, 0.7881, 0.2849,
+                    2940.0, 0.7559, 0.3103,
+                    3650.0, 0.6291, 0.4206,
+                    4625.0, 0.5336, 0.5161,
+                    5715.0, 0.4668, 0.5898
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.01165,
+                "transverse_neg": 0.01601
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 15000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 1.0, 2.0, 4.0, 6.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 6.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.2,
+                                1000, 0.2
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.2,
+                                1000, 0.2
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ],
+                "startup_frames": 5,
+                "convergence_frames": 6,
+                "speed": 0.15
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.529, 1.526, 1.522, 1.506, 1.489, 1.473, 1.458, 1.456, 1.456, 1.458, 1.474, 1.493, 1.513, 1.531, 1.541, 1.544,
+                            1.527, 1.523, 1.511, 1.491, 1.474, 1.459, 1.445, 1.441, 1.441, 1.446, 1.461, 1.479, 1.499, 1.521, 1.536, 1.541,
+                            1.524, 1.515, 1.498, 1.477, 1.459, 1.444, 1.431, 1.426, 1.426, 1.435, 1.446, 1.466, 1.487, 1.507, 1.528, 1.538,
+                            1.522, 1.512, 1.491, 1.468, 1.447, 1.431, 1.423, 1.417, 1.418, 1.425, 1.435, 1.455, 1.479, 1.499, 1.523, 1.537,
+                            1.522, 1.509, 1.485, 1.463, 1.441, 1.423, 1.416, 1.413, 1.415, 1.418, 1.429, 1.449, 1.473, 1.495, 1.521, 1.538,
+                            1.522, 1.508, 1.483, 1.461, 1.438, 1.421, 1.413, 1.412, 1.412, 1.415, 1.428, 1.447, 1.471, 1.493, 1.519, 1.538,
+                            1.522, 1.509, 1.484, 1.462, 1.439, 1.421, 1.414, 1.411, 1.412, 1.416, 1.428, 1.447, 1.471, 1.493, 1.519, 1.537,
+                            1.523, 1.511, 1.487, 1.465, 1.443, 1.424, 1.417, 1.413, 1.415, 1.419, 1.429, 1.451, 1.473, 1.494, 1.519, 1.536,
+                            1.524, 1.514, 1.493, 1.471, 1.451, 1.434, 1.424, 1.419, 1.419, 1.428, 1.437, 1.457, 1.477, 1.498, 1.521, 1.538,
+                            1.527, 1.521, 1.503, 1.481, 1.462, 1.449, 1.434, 1.429, 1.429, 1.437, 1.451, 1.469, 1.488, 1.508, 1.527, 1.539,
+                            1.529, 1.527, 1.515, 1.495, 1.477, 1.462, 1.449, 1.444, 1.444, 1.451, 1.467, 1.481, 1.499, 1.519, 1.535, 1.543,
+                            1.534, 1.531, 1.527, 1.512, 1.492, 1.476, 1.463, 1.461, 1.461, 1.464, 1.479, 1.495, 1.515, 1.533, 1.543, 1.546
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            2.603, 2.599, 2.591, 2.567, 2.539, 2.515, 2.489, 2.489, 2.489, 2.491, 2.516, 2.543, 2.574, 2.597, 2.614, 2.617,
+                            2.596, 2.591, 2.571, 2.542, 2.516, 2.489, 2.464, 2.458, 2.458, 2.469, 2.492, 2.518, 2.547, 2.576, 2.602, 2.614,
+                            2.591, 2.576, 2.546, 2.519, 2.489, 2.464, 2.437, 2.427, 2.427, 2.441, 2.467, 2.492, 2.525, 2.553, 2.586, 2.605,
+                            2.588, 2.568, 2.534, 2.503, 2.472, 2.437, 2.423, 2.409, 2.411, 2.425, 2.441, 2.475, 2.513, 2.541, 2.577, 2.602,
+                            2.588, 2.565, 2.527, 2.494, 2.461, 2.425, 2.409, 2.399, 2.403, 2.409, 2.431, 2.466, 2.503, 2.534, 2.571, 2.601,
+                            2.586, 2.561, 2.525, 2.491, 2.454, 2.418, 2.399, 2.396, 2.395, 2.402, 2.424, 2.461, 2.501, 2.531, 2.567, 2.599,
+                            2.583, 2.559, 2.525, 2.491, 2.454, 2.418, 2.398, 2.393, 2.393, 2.401, 2.423, 2.459, 2.498, 2.531, 2.566, 2.597,
+                            2.583, 2.559, 2.526, 2.494, 2.458, 2.421, 2.404, 2.397, 2.399, 2.404, 2.426, 2.461, 2.501, 2.531, 2.566, 2.596,
+                            2.583, 2.563, 2.531, 2.501, 2.469, 2.435, 2.419, 2.405, 2.404, 2.422, 2.435, 2.471, 2.505, 2.537, 2.572, 2.596,
+                            2.585, 2.571, 2.539, 2.516, 2.486, 2.458, 2.435, 2.424, 2.424, 2.435, 2.459, 2.489, 2.521, 2.546, 2.579, 2.601,
+                            2.589, 2.578, 2.557, 2.532, 2.506, 2.483, 2.458, 2.449, 2.449, 2.459, 2.485, 2.507, 2.535, 2.563, 2.591, 2.605,
+                            2.589, 2.586, 2.575, 2.551, 2.525, 2.503, 2.481, 2.476, 2.476, 2.481, 2.504, 2.526, 2.555, 2.583, 2.604, 2.611
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            3.311, 3.339, 3.369, 3.374, 3.371, 3.363, 3.356, 3.353, 3.353, 3.353, 3.357, 3.362, 3.362, 3.356, 3.328, 3.311,
+                            3.321, 3.354, 3.374, 3.374, 3.368, 3.359, 3.352, 3.349, 3.347, 3.347, 3.349, 3.357, 3.361, 3.359, 3.343, 3.324,
+                            3.334, 3.368, 3.375, 3.374, 3.365, 3.356, 3.349, 3.347, 3.346, 3.346, 3.347, 3.349, 3.358, 3.361, 3.357, 3.336,
+                            3.346, 3.378, 3.378, 3.369, 3.363, 3.358, 3.351, 3.348, 3.347, 3.346, 3.347, 3.348, 3.354, 3.364, 3.363, 3.345,
+                            3.351, 3.381, 3.381, 3.368, 3.361, 3.357, 3.349, 3.347, 3.347, 3.345, 3.345, 3.347, 3.353, 3.364, 3.364, 3.347,
+                            3.353, 3.379, 3.379, 3.366, 3.359, 3.351, 3.348, 3.343, 3.342, 3.342, 3.343, 3.345, 3.351, 3.363, 3.363, 3.347,
+                            3.353, 3.376, 3.376, 3.363, 3.351, 3.347, 3.343, 3.338, 3.336, 3.338, 3.339, 3.343, 3.351, 3.361, 3.361, 3.347,
+                            3.351, 3.374, 3.374, 3.359, 3.351, 3.345, 3.338, 3.334, 3.333, 3.334, 3.336, 3.339, 3.347, 3.358, 3.358, 3.345,
+                            3.346, 3.368, 3.368, 3.359, 3.349, 3.343, 3.336, 3.332, 3.327, 3.331, 3.333, 3.337, 3.346, 3.356, 3.356, 3.341,
+                            3.336, 3.362, 3.364, 3.359, 3.351, 3.342, 3.334, 3.324, 3.324, 3.325, 3.329, 3.336, 3.346, 3.351, 3.351, 3.333,
+                            3.324, 3.349, 3.359, 3.358, 3.352, 3.341, 3.329, 3.323, 3.321, 3.322, 3.326, 3.336, 3.346, 3.347, 3.339, 3.319,
+                            3.311, 3.328, 3.352, 3.354, 3.352, 3.341, 3.329, 3.321, 3.319, 3.321, 3.324, 3.338, 3.343, 3.343, 3.319, 3.312
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.634, 1.647, 1.665, 1.668, 1.668, 1.664, 1.662, 1.662, 1.661, 1.661, 1.661, 1.663, 1.663, 1.659, 1.643, 1.636,
+                            1.639, 1.656, 1.668, 1.669, 1.668, 1.666, 1.664, 1.663, 1.663, 1.661, 1.661, 1.662, 1.663, 1.662, 1.654, 1.642,
+                            1.645, 1.663, 1.669, 1.668, 1.667, 1.667, 1.667, 1.668, 1.668, 1.665, 1.662, 1.661, 1.662, 1.664, 1.661, 1.649,
+                            1.651, 1.669, 1.669, 1.667, 1.666, 1.668, 1.669, 1.672, 1.672, 1.668, 1.665, 1.661, 1.661, 1.665, 1.665, 1.655,
+                            1.654, 1.669, 1.669, 1.666, 1.666, 1.669, 1.672, 1.673, 1.673, 1.671, 1.666, 1.661, 1.661, 1.665, 1.665, 1.659,
+                            1.654, 1.669, 1.669, 1.666, 1.666, 1.669, 1.671, 1.673, 1.672, 1.669, 1.667, 1.661, 1.661, 1.665, 1.665, 1.659,
+                            1.654, 1.668, 1.668, 1.664, 1.663, 1.667, 1.669, 1.671, 1.669, 1.668, 1.665, 1.661, 1.661, 1.663, 1.663, 1.659,
+                            1.653, 1.665, 1.665, 1.661, 1.661, 1.664, 1.667, 1.668, 1.668, 1.665, 1.661, 1.658, 1.659, 1.662, 1.662, 1.657,
+                            1.651, 1.664, 1.664, 1.659, 1.659, 1.661, 1.663, 1.663, 1.662, 1.661, 1.658, 1.656, 1.657, 1.662, 1.662, 1.655,
+                            1.645, 1.661, 1.663, 1.661, 1.659, 1.659, 1.659, 1.657, 1.657, 1.656, 1.654, 1.655, 1.656, 1.661, 1.661, 1.649,
+                            1.641, 1.654, 1.661, 1.661, 1.659, 1.657, 1.655, 1.653, 1.652, 1.651, 1.652, 1.653, 1.657, 1.658, 1.655, 1.644,
+                            1.635, 1.645, 1.661, 1.661, 1.661, 1.655, 1.653, 1.649, 1.648, 1.647, 1.651, 1.653, 1.657, 1.657, 1.646, 1.638
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    3.535, 3.279, 3.049, 2.722, 2.305, 1.958, 1.657, 1.647, 1.647, 1.656, 1.953, 2.289, 2.707, 3.058, 3.325, 3.589,
+                    3.379, 3.157, 2.874, 2.421, 1.973, 1.735, 1.472, 1.388, 1.388, 1.471, 1.724, 1.963, 2.409, 2.877, 3.185, 3.416,
+                    3.288, 3.075, 2.696, 2.169, 1.735, 1.472, 1.311, 1.208, 1.208, 1.306, 1.471, 1.724, 2.159, 2.695, 3.092, 3.321,
+                    3.238, 3.001, 2.534, 1.981, 1.572, 1.311, 1.207, 1.082, 1.082, 1.204, 1.306, 1.563, 1.973, 2.529, 3.008, 3.259,
+                    3.211, 2.938, 2.414, 1.859, 1.468, 1.221, 1.082, 1.036, 1.031, 1.079, 1.217, 1.463, 1.851, 2.403, 2.931, 3.229,
+                    3.206, 2.904, 2.356, 1.802, 1.421, 1.181, 1.037, 1.002, 1.002, 1.032, 1.175, 1.414, 1.793, 2.343, 2.899, 3.223,
+                    3.206, 2.904, 2.356, 1.802, 1.421, 1.181, 1.037, 1.005, 1.005, 1.032, 1.175, 1.414, 1.793, 2.343, 2.899, 3.223,
+                    3.211, 2.936, 2.417, 1.858, 1.468, 1.222, 1.083, 1.037, 1.032, 1.083, 1.218, 1.463, 1.848, 2.403, 2.932, 3.226,
+                    3.234, 2.997, 2.536, 1.979, 1.569, 1.311, 1.206, 1.084, 1.084, 1.204, 1.305, 1.565, 1.966, 2.524, 2.996, 3.251,
+                    3.282, 3.069, 2.697, 2.166, 1.731, 1.471, 1.311, 1.207, 1.207, 1.305, 1.466, 1.729, 2.158, 2.689, 3.077, 3.304,
+                    3.369, 3.146, 2.873, 2.415, 1.964, 1.722, 1.471, 1.382, 1.382, 1.466, 1.722, 1.964, 2.408, 2.871, 3.167, 3.401,
+                    3.524, 3.253, 3.025, 2.691, 2.275, 1.939, 1.657, 1.628, 1.628, 1.654, 1.936, 2.275, 2.687, 3.029, 3.284, 3.574
+                ],
+                "sigma": 0.00195,
+                "sigma_Cb": 0.00241
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2750,
+                        "ccm":
+                        [
+                            1.13004, 0.36392, -0.49396,
+                            -0.45885, 1.68171, -0.22286,
+                            -0.06473, -0.86962, 1.93435
+                        ]
+                    },
+                    {
+                        "ct": 2940,
+                        "ccm":
+                        [
+                            1.29876, 0.09627, -0.39503,
+                            -0.43085, 1.60258, -0.17172,
+                            -0.02638, -0.92581, 1.95218
+                        ]
+                    },
+                    {
+                        "ct": 3650,
+                        "ccm":
+                        [
+                            1.57729, -0.29734, -0.27995,
+                            -0.42965, 1.66231, -0.23265,
+                            -0.02183, -0.62331, 1.64514
+                        ]
+                    },
+                    {
+                        "ct": 4625,
+                        "ccm":
+                        [
+                            1.52145, -0.22382, -0.29763,
+                            -0.40445, 1.82186, -0.41742,
+                            -0.05732, -0.56222, 1.61954
+                        ]
+                    },
+                    {
+                        "ct": 5715,
+                        "ccm":
+                        [
+                            1.67851, -0.39193, -0.28658,
+                            -0.37169, 1.72949, -0.35781,
+                            -0.09556, -0.41951, 1.51508
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        },
+        {
+            "rpi.af":
+            {
+                "ranges":
+                {
+                    "normal":
+                    {
+                        "min": 0.0,
+                        "max": 12.0,
+                        "default": 1.0
+                    },
+                    "macro":
+                    {
+                        "min": 4.0,
+                        "max": 32.0,
+                        "default": 6.0
+                    }
+                },
+                "speeds":
+                {
+                    "normal":
+                    {
+                        "step_coarse": 2.0,
+                        "step_fine": 0.5,
+                        "contrast_ratio": 0.75,
+                        "pdaf_gain": -0.03,
+                        "pdaf_squelch": 0.2,
+                        "max_slew": 4.0,
+                        "pdaf_frames": 20,
+                        "dropout_frames": 6,
+                        "step_frames": 4
+                    },
+                    "fast":
+                    {
+                        "step_coarse": 2.0,
+                        "step_fine": 0.5,
+                        "contrast_ratio": 0.75,
+                        "pdaf_gain": -0.05,
+                        "pdaf_squelch": 0.2,
+                        "max_slew": 5.0,
+                        "pdaf_frames": 16,
+                        "dropout_frames": 6,
+                        "step_frames": 4
+                    }
+                },
+                "conf_epsilon": 8,
+                "conf_thresh": 12,
+                "conf_clip": 512,
+                "skip_frames": 5,
+                "map": [ 0.0, 420, 35.0, 920 ]
+            }
+        }
+    ]
+}
diff --git a/src/ipa/rpi/vc4/data/meson.build b/src/ipa/rpi/vc4/data/meson.build
new file mode 100644
index 00000000..bcf5658b
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/meson.build
@@ -0,0 +1,26 @@
+# SPDX-License-Identifier: CC0-1.0
+
+conf_files = files([
+    'imx219.json',
+    'imx219_noir.json',
+    'imx290.json',
+    'imx296.json',
+    'imx296_mono.json',
+    'imx378.json',
+    'imx477.json',
+    'imx477_noir.json',
+    'imx477_scientific.json',
+    'imx519.json',
+    'imx708.json',
+    'imx708_noir.json',
+    'imx708_wide.json',
+    'imx708_wide_noir.json',
+    'ov5647.json',
+    'ov5647_noir.json',
+    'ov9281_mono.json',
+    'se327m12.json',
+    'uncalibrated.json',
+])
+
+install_data(conf_files,
+             install_dir : ipa_data_dir / 'rpi' / 'vc4')
diff --git a/src/ipa/rpi/vc4/data/ov5647.json b/src/ipa/rpi/vc4/data/ov5647.json
new file mode 100644
index 00000000..d770e44f
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/ov5647.json
@@ -0,0 +1,487 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 1024
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 21663,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 987,
+                "reference_Y": 8961
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 4.25
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 401,
+                "slope": 0.05619
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2500.0, 1.0289, 0.4503,
+                    2803.0, 0.9428, 0.5108,
+                    2914.0, 0.9406, 0.5127,
+                    3605.0, 0.8261, 0.6249,
+                    4540.0, 0.7331, 0.7533,
+                    5699.0, 0.6715, 0.8627,
+                    8625.0, 0.6081, 1.0012
+                ],
+                "sensitivity_r": 1.05,
+                "sensitivity_b": 1.05,
+                "transverse_pos": 0.0321,
+                "transverse_neg": 0.04313
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 66666 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 33333 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ],
+                    "shadows": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.5,
+                            "y_target":
+                            [
+                                0, 0.17,
+                                1000, 0.17
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ],
+                "base_ev": 1.25
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.105, 1.103, 1.093, 1.083, 1.071, 1.065, 1.065, 1.065, 1.066, 1.069, 1.072, 1.077, 1.084, 1.089, 1.093, 1.093,
+                            1.103, 1.096, 1.084, 1.072, 1.059, 1.051, 1.047, 1.047, 1.051, 1.053, 1.059, 1.067, 1.075, 1.082, 1.085, 1.086,
+                            1.096, 1.084, 1.072, 1.059, 1.051, 1.045, 1.039, 1.038, 1.039, 1.045, 1.049, 1.057, 1.063, 1.072, 1.081, 1.082,
+                            1.092, 1.075, 1.061, 1.052, 1.045, 1.039, 1.036, 1.035, 1.035, 1.039, 1.044, 1.049, 1.056, 1.063, 1.072, 1.081,
+                            1.092, 1.073, 1.058, 1.048, 1.043, 1.038, 1.035, 1.033, 1.033, 1.035, 1.039, 1.044, 1.051, 1.057, 1.069, 1.078,
+                            1.091, 1.068, 1.054, 1.045, 1.041, 1.038, 1.035, 1.032, 1.032, 1.032, 1.036, 1.041, 1.045, 1.055, 1.069, 1.078,
+                            1.091, 1.068, 1.052, 1.043, 1.041, 1.038, 1.035, 1.032, 1.031, 1.032, 1.034, 1.036, 1.043, 1.055, 1.069, 1.078,
+                            1.092, 1.068, 1.052, 1.047, 1.042, 1.041, 1.038, 1.035, 1.032, 1.032, 1.035, 1.039, 1.043, 1.055, 1.071, 1.079,
+                            1.092, 1.073, 1.057, 1.051, 1.047, 1.047, 1.044, 1.041, 1.038, 1.038, 1.039, 1.043, 1.051, 1.059, 1.076, 1.083,
+                            1.092, 1.081, 1.068, 1.058, 1.056, 1.056, 1.053, 1.052, 1.049, 1.048, 1.048, 1.051, 1.059, 1.066, 1.083, 1.085,
+                            1.091, 1.087, 1.081, 1.068, 1.065, 1.064, 1.062, 1.062, 1.061, 1.056, 1.056, 1.056, 1.064, 1.069, 1.084, 1.089,
+                            1.091, 1.089, 1.085, 1.079, 1.069, 1.068, 1.067, 1.067, 1.067, 1.063, 1.061, 1.063, 1.068, 1.069, 1.081, 1.092
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.486, 1.484, 1.468, 1.449, 1.427, 1.403, 1.399, 1.399, 1.399, 1.404, 1.413, 1.433, 1.454, 1.473, 1.482, 1.488,
+                            1.484, 1.472, 1.454, 1.431, 1.405, 1.381, 1.365, 1.365, 1.367, 1.373, 1.392, 1.411, 1.438, 1.458, 1.476, 1.481,
+                            1.476, 1.458, 1.433, 1.405, 1.381, 1.361, 1.339, 1.334, 1.334, 1.346, 1.362, 1.391, 1.411, 1.438, 1.462, 1.474,
+                            1.471, 1.443, 1.417, 1.388, 1.361, 1.339, 1.321, 1.313, 1.313, 1.327, 1.346, 1.362, 1.391, 1.422, 1.453, 1.473,
+                            1.469, 1.439, 1.408, 1.377, 1.349, 1.321, 1.312, 1.299, 1.299, 1.311, 1.327, 1.348, 1.378, 1.415, 1.446, 1.468,
+                            1.468, 1.434, 1.402, 1.371, 1.341, 1.316, 1.299, 1.296, 1.295, 1.299, 1.314, 1.338, 1.371, 1.408, 1.441, 1.466,
+                            1.468, 1.434, 1.401, 1.371, 1.341, 1.316, 1.301, 1.296, 1.295, 1.297, 1.314, 1.338, 1.369, 1.408, 1.441, 1.465,
+                            1.469, 1.436, 1.401, 1.374, 1.348, 1.332, 1.315, 1.301, 1.301, 1.313, 1.324, 1.342, 1.372, 1.409, 1.442, 1.465,
+                            1.471, 1.444, 1.413, 1.388, 1.371, 1.348, 1.332, 1.323, 1.323, 1.324, 1.342, 1.362, 1.386, 1.418, 1.449, 1.467,
+                            1.473, 1.454, 1.431, 1.407, 1.388, 1.371, 1.359, 1.352, 1.351, 1.351, 1.362, 1.383, 1.404, 1.433, 1.462, 1.472,
+                            1.474, 1.461, 1.447, 1.424, 1.407, 1.394, 1.385, 1.381, 1.379, 1.381, 1.383, 1.401, 1.419, 1.444, 1.466, 1.481,
+                            1.474, 1.464, 1.455, 1.442, 1.421, 1.408, 1.403, 1.403, 1.403, 1.399, 1.402, 1.415, 1.432, 1.446, 1.467, 1.483
+                        ]
+                    },
+                    {
+                        "ct": 6500,
+                        "table":
+                        [
+                            1.567, 1.565, 1.555, 1.541, 1.525, 1.518, 1.518, 1.518, 1.521, 1.527, 1.532, 1.541, 1.551, 1.559, 1.567, 1.569,
+                            1.565, 1.557, 1.542, 1.527, 1.519, 1.515, 1.511, 1.516, 1.519, 1.524, 1.528, 1.533, 1.542, 1.553, 1.559, 1.562,
+                            1.561, 1.546, 1.532, 1.521, 1.518, 1.515, 1.511, 1.516, 1.519, 1.524, 1.528, 1.529, 1.533, 1.542, 1.554, 1.559,
+                            1.561, 1.539, 1.526, 1.524, 1.521, 1.521, 1.522, 1.524, 1.525, 1.531, 1.529, 1.529, 1.531, 1.538, 1.549, 1.558,
+                            1.559, 1.538, 1.526, 1.525, 1.524, 1.528, 1.534, 1.536, 1.536, 1.536, 1.532, 1.529, 1.531, 1.537, 1.548, 1.556,
+                            1.561, 1.537, 1.525, 1.524, 1.526, 1.532, 1.537, 1.539, 1.538, 1.537, 1.532, 1.529, 1.529, 1.537, 1.546, 1.556,
+                            1.561, 1.536, 1.524, 1.522, 1.525, 1.532, 1.538, 1.538, 1.537, 1.533, 1.528, 1.526, 1.527, 1.536, 1.546, 1.555,
+                            1.561, 1.537, 1.522, 1.521, 1.524, 1.531, 1.536, 1.537, 1.534, 1.529, 1.526, 1.522, 1.523, 1.534, 1.547, 1.555,
+                            1.561, 1.538, 1.524, 1.522, 1.526, 1.531, 1.535, 1.535, 1.534, 1.527, 1.524, 1.522, 1.522, 1.535, 1.549, 1.556,
+                            1.558, 1.543, 1.532, 1.526, 1.526, 1.529, 1.534, 1.535, 1.533, 1.526, 1.523, 1.522, 1.524, 1.537, 1.552, 1.557,
+                            1.555, 1.546, 1.541, 1.528, 1.527, 1.528, 1.531, 1.533, 1.531, 1.527, 1.522, 1.522, 1.526, 1.536, 1.552, 1.561,
+                            1.555, 1.547, 1.542, 1.538, 1.526, 1.526, 1.529, 1.531, 1.529, 1.528, 1.519, 1.519, 1.527, 1.531, 1.543, 1.561
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.684, 1.688, 1.691, 1.697, 1.709, 1.722, 1.735, 1.745, 1.747, 1.745, 1.731, 1.719, 1.709, 1.705, 1.699, 1.699,
+                            1.684, 1.689, 1.694, 1.708, 1.721, 1.735, 1.747, 1.762, 1.762, 1.758, 1.745, 1.727, 1.716, 1.707, 1.701, 1.699,
+                            1.684, 1.691, 1.704, 1.719, 1.734, 1.755, 1.772, 1.786, 1.789, 1.788, 1.762, 1.745, 1.724, 1.709, 1.702, 1.698,
+                            1.682, 1.694, 1.709, 1.729, 1.755, 1.773, 1.798, 1.815, 1.817, 1.808, 1.788, 1.762, 1.733, 1.714, 1.704, 1.699,
+                            1.682, 1.693, 1.713, 1.742, 1.772, 1.798, 1.815, 1.829, 1.831, 1.821, 1.807, 1.773, 1.742, 1.716, 1.703, 1.699,
+                            1.681, 1.693, 1.713, 1.742, 1.772, 1.799, 1.828, 1.839, 1.839, 1.828, 1.807, 1.774, 1.742, 1.715, 1.699, 1.695,
+                            1.679, 1.691, 1.712, 1.739, 1.771, 1.798, 1.825, 1.829, 1.831, 1.818, 1.801, 1.774, 1.738, 1.712, 1.695, 1.691,
+                            1.676, 1.685, 1.703, 1.727, 1.761, 1.784, 1.801, 1.817, 1.817, 1.801, 1.779, 1.761, 1.729, 1.706, 1.691, 1.684,
+                            1.669, 1.678, 1.692, 1.714, 1.741, 1.764, 1.784, 1.795, 1.795, 1.779, 1.761, 1.738, 1.713, 1.696, 1.683, 1.679,
+                            1.664, 1.671, 1.679, 1.693, 1.716, 1.741, 1.762, 1.769, 1.769, 1.753, 1.738, 1.713, 1.701, 1.687, 1.681, 1.676,
+                            1.661, 1.664, 1.671, 1.679, 1.693, 1.714, 1.732, 1.739, 1.739, 1.729, 1.708, 1.701, 1.685, 1.679, 1.676, 1.677,
+                            1.659, 1.661, 1.664, 1.671, 1.679, 1.693, 1.712, 1.714, 1.714, 1.708, 1.701, 1.687, 1.679, 1.672, 1.673, 1.677
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.177, 1.183, 1.187, 1.191, 1.197, 1.206, 1.213, 1.215, 1.215, 1.215, 1.211, 1.204, 1.196, 1.191, 1.183, 1.182,
+                            1.179, 1.185, 1.191, 1.196, 1.206, 1.217, 1.224, 1.229, 1.229, 1.226, 1.221, 1.212, 1.202, 1.195, 1.188, 1.182,
+                            1.183, 1.191, 1.196, 1.206, 1.217, 1.229, 1.239, 1.245, 1.245, 1.245, 1.233, 1.221, 1.212, 1.199, 1.193, 1.187,
+                            1.183, 1.192, 1.201, 1.212, 1.229, 1.241, 1.252, 1.259, 1.259, 1.257, 1.245, 1.233, 1.217, 1.201, 1.194, 1.192,
+                            1.183, 1.192, 1.202, 1.219, 1.238, 1.252, 1.261, 1.269, 1.268, 1.261, 1.257, 1.241, 1.223, 1.204, 1.194, 1.191,
+                            1.182, 1.192, 1.202, 1.219, 1.239, 1.255, 1.266, 1.271, 1.271, 1.265, 1.258, 1.242, 1.223, 1.205, 1.192, 1.191,
+                            1.181, 1.189, 1.199, 1.218, 1.239, 1.254, 1.262, 1.268, 1.268, 1.258, 1.253, 1.241, 1.221, 1.204, 1.191, 1.187,
+                            1.179, 1.184, 1.193, 1.211, 1.232, 1.243, 1.254, 1.257, 1.256, 1.253, 1.242, 1.232, 1.216, 1.199, 1.187, 1.183,
+                            1.174, 1.179, 1.187, 1.202, 1.218, 1.232, 1.243, 1.246, 1.246, 1.239, 1.232, 1.218, 1.207, 1.191, 1.183, 1.179,
+                            1.169, 1.175, 1.181, 1.189, 1.202, 1.218, 1.229, 1.232, 1.232, 1.224, 1.218, 1.207, 1.199, 1.185, 1.181, 1.174,
+                            1.164, 1.168, 1.175, 1.179, 1.189, 1.201, 1.209, 1.213, 1.213, 1.209, 1.201, 1.198, 1.186, 1.181, 1.174, 1.173,
+                            1.161, 1.166, 1.171, 1.175, 1.179, 1.189, 1.197, 1.198, 1.198, 1.197, 1.196, 1.186, 1.182, 1.175, 1.173, 1.173
+                        ]
+                    },
+                    {
+                        "ct": 6500,
+                        "table":
+                        [
+                            1.166, 1.171, 1.173, 1.178, 1.187, 1.193, 1.201, 1.205, 1.205, 1.205, 1.199, 1.191, 1.184, 1.179, 1.174, 1.171,
+                            1.166, 1.172, 1.176, 1.184, 1.195, 1.202, 1.209, 1.216, 1.216, 1.213, 1.208, 1.201, 1.189, 1.182, 1.176, 1.171,
+                            1.166, 1.173, 1.183, 1.195, 1.202, 1.214, 1.221, 1.228, 1.229, 1.228, 1.221, 1.209, 1.201, 1.186, 1.179, 1.174,
+                            1.165, 1.174, 1.187, 1.201, 1.214, 1.223, 1.235, 1.241, 1.242, 1.241, 1.229, 1.221, 1.205, 1.188, 1.181, 1.177,
+                            1.165, 1.174, 1.189, 1.207, 1.223, 1.235, 1.242, 1.253, 1.252, 1.245, 1.241, 1.228, 1.211, 1.189, 1.181, 1.178,
+                            1.164, 1.173, 1.189, 1.207, 1.224, 1.238, 1.249, 1.255, 1.255, 1.249, 1.242, 1.228, 1.211, 1.191, 1.179, 1.176,
+                            1.163, 1.172, 1.187, 1.207, 1.223, 1.237, 1.245, 1.253, 1.252, 1.243, 1.237, 1.228, 1.207, 1.188, 1.176, 1.173,
+                            1.159, 1.167, 1.179, 1.199, 1.217, 1.227, 1.237, 1.241, 1.241, 1.237, 1.228, 1.217, 1.201, 1.184, 1.174, 1.169,
+                            1.156, 1.164, 1.172, 1.189, 1.205, 1.217, 1.226, 1.229, 1.229, 1.222, 1.217, 1.204, 1.192, 1.177, 1.171, 1.166,
+                            1.154, 1.159, 1.166, 1.177, 1.189, 1.205, 1.213, 1.216, 1.216, 1.209, 1.204, 1.192, 1.183, 1.172, 1.168, 1.162,
+                            1.152, 1.155, 1.161, 1.166, 1.177, 1.188, 1.195, 1.198, 1.199, 1.196, 1.187, 1.183, 1.173, 1.168, 1.163, 1.162,
+                            1.151, 1.154, 1.158, 1.162, 1.168, 1.177, 1.183, 1.184, 1.184, 1.184, 1.182, 1.172, 1.168, 1.165, 1.162, 1.161
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    2.236, 2.111, 1.912, 1.741, 1.579, 1.451, 1.379, 1.349, 1.349, 1.361, 1.411, 1.505, 1.644, 1.816, 2.034, 2.159,
+                    2.139, 1.994, 1.796, 1.625, 1.467, 1.361, 1.285, 1.248, 1.239, 1.265, 1.321, 1.408, 1.536, 1.703, 1.903, 2.087,
+                    2.047, 1.898, 1.694, 1.511, 1.373, 1.254, 1.186, 1.152, 1.142, 1.166, 1.226, 1.309, 1.441, 1.598, 1.799, 1.978,
+                    1.999, 1.824, 1.615, 1.429, 1.281, 1.179, 1.113, 1.077, 1.071, 1.096, 1.153, 1.239, 1.357, 1.525, 1.726, 1.915,
+                    1.976, 1.773, 1.563, 1.374, 1.222, 1.119, 1.064, 1.032, 1.031, 1.049, 1.099, 1.188, 1.309, 1.478, 1.681, 1.893,
+                    1.973, 1.756, 1.542, 1.351, 1.196, 1.088, 1.028, 1.011, 1.004, 1.029, 1.077, 1.169, 1.295, 1.459, 1.663, 1.891,
+                    1.973, 1.761, 1.541, 1.349, 1.193, 1.087, 1.031, 1.006, 1.006, 1.023, 1.075, 1.169, 1.298, 1.463, 1.667, 1.891,
+                    1.982, 1.789, 1.568, 1.373, 1.213, 1.111, 1.051, 1.029, 1.024, 1.053, 1.106, 1.199, 1.329, 1.495, 1.692, 1.903,
+                    2.015, 1.838, 1.621, 1.426, 1.268, 1.159, 1.101, 1.066, 1.068, 1.099, 1.166, 1.259, 1.387, 1.553, 1.751, 1.937,
+                    2.076, 1.911, 1.692, 1.507, 1.346, 1.236, 1.169, 1.136, 1.139, 1.174, 1.242, 1.349, 1.475, 1.641, 1.833, 2.004,
+                    2.193, 2.011, 1.798, 1.604, 1.444, 1.339, 1.265, 1.235, 1.237, 1.273, 1.351, 1.461, 1.598, 1.758, 1.956, 2.125,
+                    2.263, 2.154, 1.916, 1.711, 1.549, 1.432, 1.372, 1.356, 1.356, 1.383, 1.455, 1.578, 1.726, 1.914, 2.119, 2.211
+                ],
+                "sigma": 0.006,
+                "sigma_Cb": 0.00208
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2500,
+                        "ccm":
+                        [
+                            1.70741, -0.05307, -0.65433,
+                            -0.62822, 1.68836, -0.06014,
+                            -0.04452, -1.87628, 2.92079
+                        ]
+                    },
+                    {
+                        "ct": 2803,
+                        "ccm":
+                        [
+                            1.74383, -0.18731, -0.55652,
+                            -0.56491, 1.67772, -0.11281,
+                            -0.01522, -1.60635, 2.62157
+                        ]
+                    },
+                    {
+                        "ct": 2912,
+                        "ccm":
+                        [
+                            1.75215, -0.22221, -0.52995,
+                            -0.54568, 1.63522, -0.08954,
+                            0.02633, -1.56997, 2.54364
+                        ]
+                    },
+                    {
+                        "ct": 2914,
+                        "ccm":
+                        [
+                            1.72423, -0.28939, -0.43484,
+                            -0.55188, 1.62925, -0.07737,
+                            0.01959, -1.28661, 2.26702
+                        ]
+                    },
+                    {
+                        "ct": 3605,
+                        "ccm":
+                        [
+                            1.80381, -0.43646, -0.36735,
+                            -0.46505, 1.56814, -0.10309,
+                            0.00929, -1.00424, 1.99495
+                        ]
+                    },
+                    {
+                        "ct": 4540,
+                        "ccm":
+                        [
+                            1.85263, -0.46545, -0.38719,
+                            -0.44136, 1.68443, -0.24307,
+                            0.04108, -0.85599, 1.81491
+                        ]
+                    },
+                    {
+                        "ct": 5699,
+                        "ccm":
+                        [
+                            1.98595, -0.63542, -0.35054,
+                            -0.34623, 1.54146, -0.19522,
+                            0.00411, -0.70936, 1.70525
+                        ]
+                    },
+                    {
+                        "ct": 8625,
+                        "ccm":
+                        [
+                            2.21637, -0.56663, -0.64974,
+                            -0.41133, 1.96625, -0.55492,
+                            -0.02307, -0.83529, 1.85837
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/ov5647_noir.json b/src/ipa/rpi/vc4/data/ov5647_noir.json
new file mode 100644
index 00000000..a6c6722f
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/ov5647_noir.json
@@ -0,0 +1,403 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 1024
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 21663,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 987,
+                "reference_Y": 8961
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 4.25
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 401,
+                "slope": 0.05619
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "bayes": 0
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 66666 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 33333 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ],
+                    "shadows": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.5,
+                            "y_target":
+                            [
+                                0, 0.17,
+                                1000, 0.17
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ],
+                "base_ev": 1.25
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.105, 1.103, 1.093, 1.083, 1.071, 1.065, 1.065, 1.065, 1.066, 1.069, 1.072, 1.077, 1.084, 1.089, 1.093, 1.093,
+                            1.103, 1.096, 1.084, 1.072, 1.059, 1.051, 1.047, 1.047, 1.051, 1.053, 1.059, 1.067, 1.075, 1.082, 1.085, 1.086,
+                            1.096, 1.084, 1.072, 1.059, 1.051, 1.045, 1.039, 1.038, 1.039, 1.045, 1.049, 1.057, 1.063, 1.072, 1.081, 1.082,
+                            1.092, 1.075, 1.061, 1.052, 1.045, 1.039, 1.036, 1.035, 1.035, 1.039, 1.044, 1.049, 1.056, 1.063, 1.072, 1.081,
+                            1.092, 1.073, 1.058, 1.048, 1.043, 1.038, 1.035, 1.033, 1.033, 1.035, 1.039, 1.044, 1.051, 1.057, 1.069, 1.078,
+                            1.091, 1.068, 1.054, 1.045, 1.041, 1.038, 1.035, 1.032, 1.032, 1.032, 1.036, 1.041, 1.045, 1.055, 1.069, 1.078,
+                            1.091, 1.068, 1.052, 1.043, 1.041, 1.038, 1.035, 1.032, 1.031, 1.032, 1.034, 1.036, 1.043, 1.055, 1.069, 1.078,
+                            1.092, 1.068, 1.052, 1.047, 1.042, 1.041, 1.038, 1.035, 1.032, 1.032, 1.035, 1.039, 1.043, 1.055, 1.071, 1.079,
+                            1.092, 1.073, 1.057, 1.051, 1.047, 1.047, 1.044, 1.041, 1.038, 1.038, 1.039, 1.043, 1.051, 1.059, 1.076, 1.083,
+                            1.092, 1.081, 1.068, 1.058, 1.056, 1.056, 1.053, 1.052, 1.049, 1.048, 1.048, 1.051, 1.059, 1.066, 1.083, 1.085,
+                            1.091, 1.087, 1.081, 1.068, 1.065, 1.064, 1.062, 1.062, 1.061, 1.056, 1.056, 1.056, 1.064, 1.069, 1.084, 1.089,
+                            1.091, 1.089, 1.085, 1.079, 1.069, 1.068, 1.067, 1.067, 1.067, 1.063, 1.061, 1.063, 1.068, 1.069, 1.081, 1.092
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.486, 1.484, 1.468, 1.449, 1.427, 1.403, 1.399, 1.399, 1.399, 1.404, 1.413, 1.433, 1.454, 1.473, 1.482, 1.488,
+                            1.484, 1.472, 1.454, 1.431, 1.405, 1.381, 1.365, 1.365, 1.367, 1.373, 1.392, 1.411, 1.438, 1.458, 1.476, 1.481,
+                            1.476, 1.458, 1.433, 1.405, 1.381, 1.361, 1.339, 1.334, 1.334, 1.346, 1.362, 1.391, 1.411, 1.438, 1.462, 1.474,
+                            1.471, 1.443, 1.417, 1.388, 1.361, 1.339, 1.321, 1.313, 1.313, 1.327, 1.346, 1.362, 1.391, 1.422, 1.453, 1.473,
+                            1.469, 1.439, 1.408, 1.377, 1.349, 1.321, 1.312, 1.299, 1.299, 1.311, 1.327, 1.348, 1.378, 1.415, 1.446, 1.468,
+                            1.468, 1.434, 1.402, 1.371, 1.341, 1.316, 1.299, 1.296, 1.295, 1.299, 1.314, 1.338, 1.371, 1.408, 1.441, 1.466,
+                            1.468, 1.434, 1.401, 1.371, 1.341, 1.316, 1.301, 1.296, 1.295, 1.297, 1.314, 1.338, 1.369, 1.408, 1.441, 1.465,
+                            1.469, 1.436, 1.401, 1.374, 1.348, 1.332, 1.315, 1.301, 1.301, 1.313, 1.324, 1.342, 1.372, 1.409, 1.442, 1.465,
+                            1.471, 1.444, 1.413, 1.388, 1.371, 1.348, 1.332, 1.323, 1.323, 1.324, 1.342, 1.362, 1.386, 1.418, 1.449, 1.467,
+                            1.473, 1.454, 1.431, 1.407, 1.388, 1.371, 1.359, 1.352, 1.351, 1.351, 1.362, 1.383, 1.404, 1.433, 1.462, 1.472,
+                            1.474, 1.461, 1.447, 1.424, 1.407, 1.394, 1.385, 1.381, 1.379, 1.381, 1.383, 1.401, 1.419, 1.444, 1.466, 1.481,
+                            1.474, 1.464, 1.455, 1.442, 1.421, 1.408, 1.403, 1.403, 1.403, 1.399, 1.402, 1.415, 1.432, 1.446, 1.467, 1.483
+                        ]
+                    },
+                    {
+                        "ct": 6500,
+                        "table":
+                        [
+                            1.567, 1.565, 1.555, 1.541, 1.525, 1.518, 1.518, 1.518, 1.521, 1.527, 1.532, 1.541, 1.551, 1.559, 1.567, 1.569,
+                            1.565, 1.557, 1.542, 1.527, 1.519, 1.515, 1.511, 1.516, 1.519, 1.524, 1.528, 1.533, 1.542, 1.553, 1.559, 1.562,
+                            1.561, 1.546, 1.532, 1.521, 1.518, 1.515, 1.511, 1.516, 1.519, 1.524, 1.528, 1.529, 1.533, 1.542, 1.554, 1.559,
+                            1.561, 1.539, 1.526, 1.524, 1.521, 1.521, 1.522, 1.524, 1.525, 1.531, 1.529, 1.529, 1.531, 1.538, 1.549, 1.558,
+                            1.559, 1.538, 1.526, 1.525, 1.524, 1.528, 1.534, 1.536, 1.536, 1.536, 1.532, 1.529, 1.531, 1.537, 1.548, 1.556,
+                            1.561, 1.537, 1.525, 1.524, 1.526, 1.532, 1.537, 1.539, 1.538, 1.537, 1.532, 1.529, 1.529, 1.537, 1.546, 1.556,
+                            1.561, 1.536, 1.524, 1.522, 1.525, 1.532, 1.538, 1.538, 1.537, 1.533, 1.528, 1.526, 1.527, 1.536, 1.546, 1.555,
+                            1.561, 1.537, 1.522, 1.521, 1.524, 1.531, 1.536, 1.537, 1.534, 1.529, 1.526, 1.522, 1.523, 1.534, 1.547, 1.555,
+                            1.561, 1.538, 1.524, 1.522, 1.526, 1.531, 1.535, 1.535, 1.534, 1.527, 1.524, 1.522, 1.522, 1.535, 1.549, 1.556,
+                            1.558, 1.543, 1.532, 1.526, 1.526, 1.529, 1.534, 1.535, 1.533, 1.526, 1.523, 1.522, 1.524, 1.537, 1.552, 1.557,
+                            1.555, 1.546, 1.541, 1.528, 1.527, 1.528, 1.531, 1.533, 1.531, 1.527, 1.522, 1.522, 1.526, 1.536, 1.552, 1.561,
+                            1.555, 1.547, 1.542, 1.538, 1.526, 1.526, 1.529, 1.531, 1.529, 1.528, 1.519, 1.519, 1.527, 1.531, 1.543, 1.561
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 3000,
+                        "table":
+                        [
+                            1.684, 1.688, 1.691, 1.697, 1.709, 1.722, 1.735, 1.745, 1.747, 1.745, 1.731, 1.719, 1.709, 1.705, 1.699, 1.699,
+                            1.684, 1.689, 1.694, 1.708, 1.721, 1.735, 1.747, 1.762, 1.762, 1.758, 1.745, 1.727, 1.716, 1.707, 1.701, 1.699,
+                            1.684, 1.691, 1.704, 1.719, 1.734, 1.755, 1.772, 1.786, 1.789, 1.788, 1.762, 1.745, 1.724, 1.709, 1.702, 1.698,
+                            1.682, 1.694, 1.709, 1.729, 1.755, 1.773, 1.798, 1.815, 1.817, 1.808, 1.788, 1.762, 1.733, 1.714, 1.704, 1.699,
+                            1.682, 1.693, 1.713, 1.742, 1.772, 1.798, 1.815, 1.829, 1.831, 1.821, 1.807, 1.773, 1.742, 1.716, 1.703, 1.699,
+                            1.681, 1.693, 1.713, 1.742, 1.772, 1.799, 1.828, 1.839, 1.839, 1.828, 1.807, 1.774, 1.742, 1.715, 1.699, 1.695,
+                            1.679, 1.691, 1.712, 1.739, 1.771, 1.798, 1.825, 1.829, 1.831, 1.818, 1.801, 1.774, 1.738, 1.712, 1.695, 1.691,
+                            1.676, 1.685, 1.703, 1.727, 1.761, 1.784, 1.801, 1.817, 1.817, 1.801, 1.779, 1.761, 1.729, 1.706, 1.691, 1.684,
+                            1.669, 1.678, 1.692, 1.714, 1.741, 1.764, 1.784, 1.795, 1.795, 1.779, 1.761, 1.738, 1.713, 1.696, 1.683, 1.679,
+                            1.664, 1.671, 1.679, 1.693, 1.716, 1.741, 1.762, 1.769, 1.769, 1.753, 1.738, 1.713, 1.701, 1.687, 1.681, 1.676,
+                            1.661, 1.664, 1.671, 1.679, 1.693, 1.714, 1.732, 1.739, 1.739, 1.729, 1.708, 1.701, 1.685, 1.679, 1.676, 1.677,
+                            1.659, 1.661, 1.664, 1.671, 1.679, 1.693, 1.712, 1.714, 1.714, 1.708, 1.701, 1.687, 1.679, 1.672, 1.673, 1.677
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.177, 1.183, 1.187, 1.191, 1.197, 1.206, 1.213, 1.215, 1.215, 1.215, 1.211, 1.204, 1.196, 1.191, 1.183, 1.182,
+                            1.179, 1.185, 1.191, 1.196, 1.206, 1.217, 1.224, 1.229, 1.229, 1.226, 1.221, 1.212, 1.202, 1.195, 1.188, 1.182,
+                            1.183, 1.191, 1.196, 1.206, 1.217, 1.229, 1.239, 1.245, 1.245, 1.245, 1.233, 1.221, 1.212, 1.199, 1.193, 1.187,
+                            1.183, 1.192, 1.201, 1.212, 1.229, 1.241, 1.252, 1.259, 1.259, 1.257, 1.245, 1.233, 1.217, 1.201, 1.194, 1.192,
+                            1.183, 1.192, 1.202, 1.219, 1.238, 1.252, 1.261, 1.269, 1.268, 1.261, 1.257, 1.241, 1.223, 1.204, 1.194, 1.191,
+                            1.182, 1.192, 1.202, 1.219, 1.239, 1.255, 1.266, 1.271, 1.271, 1.265, 1.258, 1.242, 1.223, 1.205, 1.192, 1.191,
+                            1.181, 1.189, 1.199, 1.218, 1.239, 1.254, 1.262, 1.268, 1.268, 1.258, 1.253, 1.241, 1.221, 1.204, 1.191, 1.187,
+                            1.179, 1.184, 1.193, 1.211, 1.232, 1.243, 1.254, 1.257, 1.256, 1.253, 1.242, 1.232, 1.216, 1.199, 1.187, 1.183,
+                            1.174, 1.179, 1.187, 1.202, 1.218, 1.232, 1.243, 1.246, 1.246, 1.239, 1.232, 1.218, 1.207, 1.191, 1.183, 1.179,
+                            1.169, 1.175, 1.181, 1.189, 1.202, 1.218, 1.229, 1.232, 1.232, 1.224, 1.218, 1.207, 1.199, 1.185, 1.181, 1.174,
+                            1.164, 1.168, 1.175, 1.179, 1.189, 1.201, 1.209, 1.213, 1.213, 1.209, 1.201, 1.198, 1.186, 1.181, 1.174, 1.173,
+                            1.161, 1.166, 1.171, 1.175, 1.179, 1.189, 1.197, 1.198, 1.198, 1.197, 1.196, 1.186, 1.182, 1.175, 1.173, 1.173
+                        ]
+                    },
+                    {
+                        "ct": 6500,
+                        "table":
+                        [
+                            1.166, 1.171, 1.173, 1.178, 1.187, 1.193, 1.201, 1.205, 1.205, 1.205, 1.199, 1.191, 1.184, 1.179, 1.174, 1.171,
+                            1.166, 1.172, 1.176, 1.184, 1.195, 1.202, 1.209, 1.216, 1.216, 1.213, 1.208, 1.201, 1.189, 1.182, 1.176, 1.171,
+                            1.166, 1.173, 1.183, 1.195, 1.202, 1.214, 1.221, 1.228, 1.229, 1.228, 1.221, 1.209, 1.201, 1.186, 1.179, 1.174,
+                            1.165, 1.174, 1.187, 1.201, 1.214, 1.223, 1.235, 1.241, 1.242, 1.241, 1.229, 1.221, 1.205, 1.188, 1.181, 1.177,
+                            1.165, 1.174, 1.189, 1.207, 1.223, 1.235, 1.242, 1.253, 1.252, 1.245, 1.241, 1.228, 1.211, 1.189, 1.181, 1.178,
+                            1.164, 1.173, 1.189, 1.207, 1.224, 1.238, 1.249, 1.255, 1.255, 1.249, 1.242, 1.228, 1.211, 1.191, 1.179, 1.176,
+                            1.163, 1.172, 1.187, 1.207, 1.223, 1.237, 1.245, 1.253, 1.252, 1.243, 1.237, 1.228, 1.207, 1.188, 1.176, 1.173,
+                            1.159, 1.167, 1.179, 1.199, 1.217, 1.227, 1.237, 1.241, 1.241, 1.237, 1.228, 1.217, 1.201, 1.184, 1.174, 1.169,
+                            1.156, 1.164, 1.172, 1.189, 1.205, 1.217, 1.226, 1.229, 1.229, 1.222, 1.217, 1.204, 1.192, 1.177, 1.171, 1.166,
+                            1.154, 1.159, 1.166, 1.177, 1.189, 1.205, 1.213, 1.216, 1.216, 1.209, 1.204, 1.192, 1.183, 1.172, 1.168, 1.162,
+                            1.152, 1.155, 1.161, 1.166, 1.177, 1.188, 1.195, 1.198, 1.199, 1.196, 1.187, 1.183, 1.173, 1.168, 1.163, 1.162,
+                            1.151, 1.154, 1.158, 1.162, 1.168, 1.177, 1.183, 1.184, 1.184, 1.184, 1.182, 1.172, 1.168, 1.165, 1.162, 1.161
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    2.236, 2.111, 1.912, 1.741, 1.579, 1.451, 1.379, 1.349, 1.349, 1.361, 1.411, 1.505, 1.644, 1.816, 2.034, 2.159,
+                    2.139, 1.994, 1.796, 1.625, 1.467, 1.361, 1.285, 1.248, 1.239, 1.265, 1.321, 1.408, 1.536, 1.703, 1.903, 2.087,
+                    2.047, 1.898, 1.694, 1.511, 1.373, 1.254, 1.186, 1.152, 1.142, 1.166, 1.226, 1.309, 1.441, 1.598, 1.799, 1.978,
+                    1.999, 1.824, 1.615, 1.429, 1.281, 1.179, 1.113, 1.077, 1.071, 1.096, 1.153, 1.239, 1.357, 1.525, 1.726, 1.915,
+                    1.976, 1.773, 1.563, 1.374, 1.222, 1.119, 1.064, 1.032, 1.031, 1.049, 1.099, 1.188, 1.309, 1.478, 1.681, 1.893,
+                    1.973, 1.756, 1.542, 1.351, 1.196, 1.088, 1.028, 1.011, 1.004, 1.029, 1.077, 1.169, 1.295, 1.459, 1.663, 1.891,
+                    1.973, 1.761, 1.541, 1.349, 1.193, 1.087, 1.031, 1.006, 1.006, 1.023, 1.075, 1.169, 1.298, 1.463, 1.667, 1.891,
+                    1.982, 1.789, 1.568, 1.373, 1.213, 1.111, 1.051, 1.029, 1.024, 1.053, 1.106, 1.199, 1.329, 1.495, 1.692, 1.903,
+                    2.015, 1.838, 1.621, 1.426, 1.268, 1.159, 1.101, 1.066, 1.068, 1.099, 1.166, 1.259, 1.387, 1.553, 1.751, 1.937,
+                    2.076, 1.911, 1.692, 1.507, 1.346, 1.236, 1.169, 1.136, 1.139, 1.174, 1.242, 1.349, 1.475, 1.641, 1.833, 2.004,
+                    2.193, 2.011, 1.798, 1.604, 1.444, 1.339, 1.265, 1.235, 1.237, 1.273, 1.351, 1.461, 1.598, 1.758, 1.956, 2.125,
+                    2.263, 2.154, 1.916, 1.711, 1.549, 1.432, 1.372, 1.356, 1.356, 1.383, 1.455, 1.578, 1.726, 1.914, 2.119, 2.211
+                ],
+                "sigma": 0.006,
+                "sigma_Cb": 0.00208
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2500,
+                        "ccm":
+                        [
+                            1.70741, -0.05307, -0.65433,
+                            -0.62822, 1.68836, -0.06014,
+                            -0.04452, -1.87628, 2.92079
+                        ]
+                    },
+                    {
+                        "ct": 2803,
+                        "ccm":
+                        [
+                            1.74383, -0.18731, -0.55652,
+                            -0.56491, 1.67772, -0.11281,
+                            -0.01522, -1.60635, 2.62157
+                        ]
+                    },
+                    {
+                        "ct": 2912,
+                        "ccm":
+                        [
+                            1.75215, -0.22221, -0.52995,
+                            -0.54568, 1.63522, -0.08954,
+                            0.02633, -1.56997, 2.54364
+                        ]
+                    },
+                    {
+                        "ct": 2914,
+                        "ccm":
+                        [
+                            1.72423, -0.28939, -0.43484,
+                            -0.55188, 1.62925, -0.07737,
+                            0.01959, -1.28661, 2.26702
+                        ]
+                    },
+                    {
+                        "ct": 3605,
+                        "ccm":
+                        [
+                            1.80381, -0.43646, -0.36735,
+                            -0.46505, 1.56814, -0.10309,
+                            0.00929, -1.00424, 1.99495
+                        ]
+                    },
+                    {
+                        "ct": 4540,
+                        "ccm":
+                        [
+                            1.85263, -0.46545, -0.38719,
+                            -0.44136, 1.68443, -0.24307,
+                            0.04108, -0.85599, 1.81491
+                        ]
+                    },
+                    {
+                        "ct": 5699,
+                        "ccm":
+                        [
+                            1.98595, -0.63542, -0.35054,
+                            -0.34623, 1.54146, -0.19522,
+                            0.00411, -0.70936, 1.70525
+                        ]
+                    },
+                    {
+                        "ct": 8625,
+                        "ccm":
+                        [
+                            2.21637, -0.56663, -0.64974,
+                            -0.41133, 1.96625, -0.55492,
+                            -0.02307, -0.83529, 1.85837
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen": { }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/ov9281_mono.json b/src/ipa/rpi/vc4/data/ov9281_mono.json
new file mode 100644
index 00000000..37944c63
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/ov9281_mono.json
@@ -0,0 +1,123 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 2000,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 800,
+                "reference_Y": 20000
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.5
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 4, 4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 15000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 3.0, 4.0, 8.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.4,
+                                1000, 0.4
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "n_iter": 0,
+                "luminance_strength": 1.0,
+                "corner_strength": 1.5
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 0,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/se327m12.json b/src/ipa/rpi/vc4/data/se327m12.json
new file mode 100644
index 00000000..ee69caea
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/se327m12.json
@@ -0,0 +1,418 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 3840
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 6873,
+                "reference_gain": 1.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 800,
+                "reference_Y": 12293
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 1.986
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 207,
+                "slope": 0.00539
+            }
+        },
+        {
+            "rpi.sdn": { }
+        },
+        {
+            "rpi.awb":
+            {
+                "priors": [
+                    {
+                        "lux": 0,
+                        "prior":
+                        [
+                            2000, 1.0,
+                            3000, 0.0,
+                            13000, 0.0
+                        ]
+                    },
+                    {
+                        "lux": 800,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            6000, 2.0,
+                            13000, 2.0
+                        ]
+                    },
+                    {
+                        "lux": 1500,
+                        "prior":
+                        [
+                            2000, 0.0,
+                            4000, 1.0,
+                            6000, 6.0,
+                            6500, 7.0,
+                            7000, 1.0,
+                            13000, 1.0
+                        ]
+                    }
+                ],
+                "modes":
+                {
+                    "auto":
+                    {
+                        "lo": 2500,
+                        "hi": 8000
+                    },
+                    "incandescent":
+                    {
+                        "lo": 2500,
+                        "hi": 3000
+                    },
+                    "tungsten":
+                    {
+                        "lo": 3000,
+                        "hi": 3500
+                    },
+                    "fluorescent":
+                    {
+                        "lo": 4000,
+                        "hi": 4700
+                    },
+                    "indoor":
+                    {
+                        "lo": 3000,
+                        "hi": 5000
+                    },
+                    "daylight":
+                    {
+                        "lo": 5500,
+                        "hi": 6500
+                    },
+                    "cloudy":
+                    {
+                        "lo": 7000,
+                        "hi": 8600
+                    }
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2900.0, 0.9217, 0.3657,
+                    3600.0, 0.7876, 0.4651,
+                    4600.0, 0.6807, 0.5684,
+                    5800.0, 0.5937, 0.6724,
+                    8100.0, 0.5447, 0.7403
+                ],
+                "sensitivity_r": 1.0,
+                "sensitivity_b": 1.0,
+                "transverse_pos": 0.0162,
+                "transverse_neg": 0.0204
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    },
+                    "spot":
+                    {
+                        "weights": [ 2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
+                    },
+                    "matrix":
+                    {
+                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 6.0, 8.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.8,
+                                1000, 0.8
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.alsc":
+            {
+                "omega": 1.3,
+                "n_iter": 100,
+                "luminance_strength": 0.5,
+                "calibrations_Cr": [
+                    {
+                        "ct": 4000,
+                        "table":
+                        [
+                            1.481, 1.471, 1.449, 1.429, 1.416, 1.404, 1.394, 1.389, 1.389, 1.389, 1.392, 1.397, 1.404, 1.416, 1.429, 1.437,
+                            1.472, 1.456, 1.436, 1.418, 1.405, 1.394, 1.389, 1.384, 1.382, 1.382, 1.386, 1.388, 1.398, 1.407, 1.422, 1.429,
+                            1.465, 1.443, 1.426, 1.411, 1.397, 1.389, 1.383, 1.377, 1.377, 1.377, 1.379, 1.384, 1.388, 1.398, 1.411, 1.422,
+                            1.462, 1.441, 1.423, 1.409, 1.395, 1.385, 1.379, 1.376, 1.374, 1.374, 1.375, 1.379, 1.384, 1.394, 1.407, 1.418,
+                            1.461, 1.439, 1.421, 1.407, 1.394, 1.385, 1.381, 1.376, 1.373, 1.373, 1.373, 1.376, 1.381, 1.389, 1.403, 1.415,
+                            1.461, 1.439, 1.419, 1.404, 1.392, 1.384, 1.379, 1.376, 1.373, 1.372, 1.374, 1.375, 1.379, 1.389, 1.401, 1.413,
+                            1.461, 1.438, 1.419, 1.402, 1.389, 1.383, 1.377, 1.375, 1.373, 1.372, 1.372, 1.375, 1.381, 1.388, 1.401, 1.414,
+                            1.462, 1.438, 1.419, 1.403, 1.391, 1.381, 1.377, 1.374, 1.373, 1.373, 1.374, 1.376, 1.381, 1.389, 1.401, 1.414,
+                            1.462, 1.441, 1.423, 1.405, 1.392, 1.383, 1.377, 1.374, 1.373, 1.372, 1.373, 1.376, 1.382, 1.391, 1.402, 1.414,
+                            1.465, 1.444, 1.424, 1.407, 1.393, 1.382, 1.378, 1.373, 1.369, 1.369, 1.372, 1.375, 1.381, 1.389, 1.402, 1.417,
+                            1.469, 1.449, 1.427, 1.413, 1.396, 1.384, 1.381, 1.375, 1.371, 1.371, 1.373, 1.377, 1.385, 1.393, 1.407, 1.422,
+                            1.474, 1.456, 1.436, 1.419, 1.407, 1.391, 1.383, 1.379, 1.377, 1.377, 1.378, 1.381, 1.391, 1.404, 1.422, 1.426
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.742, 1.721, 1.689, 1.661, 1.639, 1.623, 1.613, 1.609, 1.607, 1.606, 1.609, 1.617, 1.626, 1.641, 1.665, 1.681,
+                            1.728, 1.703, 1.672, 1.645, 1.631, 1.614, 1.602, 1.599, 1.596, 1.597, 1.601, 1.608, 1.618, 1.631, 1.653, 1.671,
+                            1.713, 1.691, 1.658, 1.635, 1.618, 1.606, 1.595, 1.591, 1.588, 1.588, 1.591, 1.601, 1.608, 1.624, 1.641, 1.658,
+                            1.707, 1.681, 1.651, 1.627, 1.613, 1.599, 1.591, 1.585, 1.583, 1.584, 1.587, 1.591, 1.601, 1.615, 1.633, 1.655,
+                            1.699, 1.672, 1.644, 1.622, 1.606, 1.593, 1.586, 1.581, 1.579, 1.581, 1.583, 1.587, 1.597, 1.611, 1.631, 1.652,
+                            1.697, 1.665, 1.637, 1.617, 1.601, 1.589, 1.584, 1.579, 1.577, 1.578, 1.581, 1.585, 1.597, 1.607, 1.627, 1.652,
+                            1.697, 1.662, 1.634, 1.613, 1.599, 1.591, 1.583, 1.578, 1.576, 1.576, 1.579, 1.586, 1.597, 1.607, 1.628, 1.653,
+                            1.697, 1.662, 1.633, 1.613, 1.598, 1.589, 1.582, 1.578, 1.576, 1.577, 1.582, 1.589, 1.598, 1.611, 1.635, 1.655,
+                            1.701, 1.666, 1.636, 1.616, 1.602, 1.589, 1.583, 1.578, 1.577, 1.581, 1.583, 1.591, 1.601, 1.617, 1.639, 1.659,
+                            1.708, 1.671, 1.641, 1.618, 1.603, 1.591, 1.584, 1.581, 1.578, 1.581, 1.585, 1.594, 1.604, 1.622, 1.646, 1.666,
+                            1.714, 1.681, 1.648, 1.622, 1.608, 1.599, 1.591, 1.584, 1.583, 1.584, 1.589, 1.599, 1.614, 1.629, 1.653, 1.673,
+                            1.719, 1.691, 1.659, 1.631, 1.618, 1.606, 1.596, 1.591, 1.591, 1.593, 1.599, 1.608, 1.623, 1.642, 1.665, 1.681
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 4000,
+                        "table":
+                        [
+                            2.253, 2.267, 2.289, 2.317, 2.342, 2.359, 2.373, 2.381, 2.381, 2.378, 2.368, 2.361, 2.344, 2.337, 2.314, 2.301,
+                            2.262, 2.284, 2.314, 2.335, 2.352, 2.371, 2.383, 2.391, 2.393, 2.391, 2.381, 2.368, 2.361, 2.342, 2.322, 2.308,
+                            2.277, 2.303, 2.321, 2.346, 2.364, 2.381, 2.391, 2.395, 2.397, 2.397, 2.395, 2.381, 2.367, 2.354, 2.332, 2.321,
+                            2.277, 2.304, 2.327, 2.349, 2.369, 2.388, 2.393, 2.396, 2.396, 2.398, 2.396, 2.391, 2.376, 2.359, 2.339, 2.328,
+                            2.279, 2.311, 2.327, 2.354, 2.377, 2.389, 2.393, 2.397, 2.397, 2.398, 2.395, 2.393, 2.382, 2.363, 2.344, 2.332,
+                            2.282, 2.311, 2.329, 2.354, 2.377, 2.386, 2.396, 2.396, 2.395, 2.396, 2.397, 2.394, 2.383, 2.367, 2.346, 2.333,
+                            2.283, 2.314, 2.333, 2.353, 2.375, 2.389, 2.394, 2.395, 2.395, 2.395, 2.396, 2.394, 2.386, 2.368, 2.354, 2.336,
+                            2.287, 2.309, 2.331, 2.352, 2.373, 2.386, 2.394, 2.395, 2.395, 2.396, 2.396, 2.394, 2.384, 2.371, 2.354, 2.339,
+                            2.289, 2.307, 2.326, 2.347, 2.369, 2.385, 2.392, 2.397, 2.398, 2.398, 2.397, 2.392, 2.383, 2.367, 2.352, 2.337,
+                            2.286, 2.303, 2.322, 2.342, 2.361, 2.379, 2.389, 2.394, 2.397, 2.398, 2.396, 2.389, 2.381, 2.366, 2.346, 2.332,
+                            2.284, 2.291, 2.312, 2.329, 2.351, 2.372, 2.381, 2.389, 2.393, 2.394, 2.389, 2.385, 2.374, 2.362, 2.338, 2.325,
+                            2.283, 2.288, 2.305, 2.319, 2.339, 2.365, 2.374, 2.381, 2.384, 2.386, 2.385, 2.379, 2.368, 2.342, 2.325, 2.318
+                        ]
+                    },
+                    {
+                        "ct": 5000,
+                        "table":
+                        [
+                            1.897, 1.919, 1.941, 1.969, 1.989, 2.003, 2.014, 2.019, 2.019, 2.017, 2.014, 2.008, 1.999, 1.988, 1.968, 1.944,
+                            1.914, 1.932, 1.957, 1.982, 1.998, 2.014, 2.023, 2.029, 2.031, 2.029, 2.022, 2.014, 2.006, 1.995, 1.976, 1.955,
+                            1.925, 1.951, 1.974, 1.996, 2.013, 2.027, 2.035, 2.039, 2.039, 2.038, 2.035, 2.026, 2.015, 2.002, 1.984, 1.963,
+                            1.932, 1.958, 1.986, 2.007, 2.024, 2.034, 2.041, 2.041, 2.045, 2.045, 2.042, 2.033, 2.023, 2.009, 1.995, 1.971,
+                            1.942, 1.964, 1.994, 2.012, 2.029, 2.038, 2.043, 2.046, 2.047, 2.046, 2.045, 2.039, 2.029, 2.014, 1.997, 1.977,
+                            1.946, 1.974, 1.999, 2.015, 2.031, 2.041, 2.046, 2.047, 2.048, 2.047, 2.044, 2.041, 2.031, 2.019, 1.999, 1.978,
+                            1.948, 1.975, 2.002, 2.018, 2.031, 2.041, 2.046, 2.047, 2.048, 2.048, 2.045, 2.041, 2.029, 2.019, 1.998, 1.978,
+                            1.948, 1.973, 2.002, 2.018, 2.029, 2.042, 2.045, 2.048, 2.048, 2.048, 2.044, 2.037, 2.027, 2.014, 1.993, 1.978,
+                            1.945, 1.969, 1.998, 2.015, 2.028, 2.037, 2.045, 2.046, 2.047, 2.044, 2.039, 2.033, 2.022, 2.008, 1.989, 1.971,
+                            1.939, 1.964, 1.991, 2.011, 2.024, 2.032, 2.036, 2.042, 2.042, 2.039, 2.035, 2.024, 2.012, 1.998, 1.977, 1.964,
+                            1.932, 1.953, 1.981, 2.006, 2.016, 2.024, 2.028, 2.031, 2.034, 2.031, 2.024, 2.015, 2.005, 1.989, 1.966, 1.955,
+                            1.928, 1.944, 1.973, 1.999, 2.007, 2.016, 2.019, 2.025, 2.026, 2.025, 2.017, 2.008, 1.997, 1.975, 1.958, 1.947
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    1.877, 1.597, 1.397, 1.269, 1.191, 1.131, 1.093, 1.078, 1.071, 1.069, 1.086, 1.135, 1.221, 1.331, 1.474, 1.704,
+                    1.749, 1.506, 1.334, 1.229, 1.149, 1.088, 1.058, 1.053, 1.051, 1.046, 1.053, 1.091, 1.163, 1.259, 1.387, 1.587,
+                    1.661, 1.451, 1.295, 1.195, 1.113, 1.061, 1.049, 1.048, 1.047, 1.049, 1.049, 1.066, 1.124, 1.211, 1.333, 1.511,
+                    1.615, 1.411, 1.267, 1.165, 1.086, 1.052, 1.047, 1.047, 1.047, 1.049, 1.052, 1.056, 1.099, 1.181, 1.303, 1.471,
+                    1.576, 1.385, 1.252, 1.144, 1.068, 1.049, 1.044, 1.044, 1.045, 1.049, 1.053, 1.054, 1.083, 1.163, 1.283, 1.447,
+                    1.561, 1.373, 1.245, 1.135, 1.064, 1.049, 1.044, 1.044, 1.044, 1.046, 1.048, 1.054, 1.073, 1.153, 1.271, 1.432,
+                    1.571, 1.377, 1.242, 1.137, 1.066, 1.055, 1.052, 1.051, 1.051, 1.049, 1.047, 1.048, 1.068, 1.148, 1.271, 1.427,
+                    1.582, 1.396, 1.259, 1.156, 1.085, 1.068, 1.059, 1.054, 1.049, 1.045, 1.041, 1.043, 1.074, 1.157, 1.284, 1.444,
+                    1.623, 1.428, 1.283, 1.178, 1.105, 1.074, 1.069, 1.063, 1.056, 1.048, 1.046, 1.051, 1.094, 1.182, 1.311, 1.473,
+                    1.691, 1.471, 1.321, 1.213, 1.135, 1.088, 1.073, 1.069, 1.063, 1.059, 1.053, 1.071, 1.129, 1.222, 1.351, 1.521,
+                    1.808, 1.543, 1.371, 1.253, 1.174, 1.118, 1.085, 1.072, 1.067, 1.064, 1.071, 1.106, 1.176, 1.274, 1.398, 1.582,
+                    1.969, 1.666, 1.447, 1.316, 1.223, 1.166, 1.123, 1.094, 1.089, 1.097, 1.118, 1.163, 1.239, 1.336, 1.471, 1.681
+                ],
+                "sigma": 0.00218,
+                "sigma_Cb": 0.00194
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 1,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2900,
+                        "ccm":
+                        [
+                            1.44924, -0.12935, -0.31989,
+                            -0.65839, 1.95441, -0.29602,
+                            0.18344, -1.22282, 2.03938
+                        ]
+                    },
+                    {
+                        "ct": 3000,
+                        "ccm":
+                        [
+                            1.38736, 0.07714, -0.46451,
+                            -0.59691, 1.84335, -0.24644,
+                            0.10092, -1.30441, 2.20349
+                        ]
+                    },
+                    {
+                        "ct": 3600,
+                        "ccm":
+                        [
+                            1.51261, -0.27921, -0.23339,
+                            -0.55129, 1.83241, -0.28111,
+                            0.11649, -0.93195, 1.81546
+                        ]
+                    },
+                    {
+                        "ct": 4600,
+                        "ccm":
+                        [
+                            1.47082, -0.18523, -0.28559,
+                            -0.48923, 1.95126, -0.46203,
+                            0.07951, -0.83987, 1.76036
+                        ]
+                    },
+                    {
+                        "ct": 5800,
+                        "ccm":
+                        [
+                            1.57294, -0.36229, -0.21065,
+                            -0.42272, 1.80305, -0.38032,
+                            0.03671, -0.66862, 1.63191
+                        ]
+                    },
+                    {
+                        "ct": 8100,
+                        "ccm":
+                        [
+                            1.58803, -0.09912, -0.48891,
+                            -0.42594, 2.22303, -0.79709,
+                            -0.00621, -0.90516, 1.91137
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.sharpen":
+            {
+                "threshold": 2.0,
+                "strength": 0.5,
+                "limit": 0.5
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/uncalibrated.json b/src/ipa/rpi/vc4/data/uncalibrated.json
new file mode 100644
index 00000000..13eb3f17
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/uncalibrated.json
@@ -0,0 +1,118 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.awb":
+            {
+                "use_derivatives": 0,
+                "bayes": 0
+            }
+        },
+        {
+            "rpi.agc":
+            {
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights": [ 4, 4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 15000, 30000, 60000, 120000 ],
+                        "gain": [ 1.0, 2.0, 3.0, 4.0, 6.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.4,
+                                1000, 0.4
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            }
+        },
+        {
+            "rpi.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 4000,
+                        "ccm":
+                        [
+                            2.0, -1.0, 0.0,
+                            -0.5, 2.0, -0.5,
+                            0, -1.0, 2.0
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "rpi.contrast":
+            {
+                "ce_enable": 0,
+                "gamma_curve":
+                [
+                    0, 0,
+                    1024, 5040,
+                    2048, 9338,
+                    3072, 12356,
+                    4096, 15312,
+                    5120, 18051,
+                    6144, 20790,
+                    7168, 23193,
+                    8192, 25744,
+                    9216, 27942,
+                    10240, 30035,
+                    11264, 32005,
+                    12288, 33975,
+                    13312, 35815,
+                    14336, 37600,
+                    15360, 39168,
+                    16384, 40642,
+                    18432, 43379,
+                    20480, 45749,
+                    22528, 47753,
+                    24576, 49621,
+                    26624, 51253,
+                    28672, 52698,
+                    30720, 53796,
+                    32768, 54876,
+                    36864, 57012,
+                    40960, 58656,
+                    45056, 59954,
+                    49152, 61183,
+                    53248, 62355,
+                    57344, 63419,
+                    61440, 64476,
+                    65535, 65535
+                ]
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/meson.build b/src/ipa/rpi/vc4/meson.build
new file mode 100644
index 00000000..df01c150
--- /dev/null
+++ b/src/ipa/rpi/vc4/meson.build
@@ -0,0 +1,47 @@
+# SPDX-License-Identifier: CC0-1.0
+
+ipa_name = 'ipa_rpi_vc4'
+
+vc4_ipa_deps = [
+    libcamera_private,
+    libatomic,
+]
+
+vc4_ipa_libs = [
+    rpi_ipa_cam_helper_lib,
+    rpi_ipa_controller_lib
+]
+
+vc4_ipa_includes = [
+    ipa_includes,
+    libipa_includes,
+]
+
+vc4_ipa_sources = files([
+    'raspberrypi.cpp',
+])
+
+vc4_ipa_includes += include_directories('..')
+
+mod = shared_module(ipa_name,
+                    [vc4_ipa_sources, libcamera_generated_ipa_headers],
+                    name_prefix : '',
+                    include_directories : vc4_ipa_includes,
+                    dependencies : vc4_ipa_deps,
+                    link_with : libipa,
+                    link_whole : vc4_ipa_libs,
+                    install : true,
+                    install_dir : ipa_install_dir)
+
+if ipa_sign_module
+    custom_target(ipa_name + '.so.sign',
+                  input : mod,
+                  output : ipa_name + '.so.sign',
+                  command : [ipa_sign, ipa_priv_key, '@INPUT@', '@OUTPUT@'],
+                  install : false,
+                  build_by_default : true)
+endif
+
+subdir('data')
+
+ipa_names += ipa_name
diff --git a/src/ipa/rpi/vc4/raspberrypi.cpp b/src/ipa/rpi/vc4/raspberrypi.cpp
new file mode 100644
index 00000000..5d3bf4ca
--- /dev/null
+++ b/src/ipa/rpi/vc4/raspberrypi.cpp
@@ -0,0 +1,1853 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019-2021, Raspberry Pi Ltd
+ *
+ * rpi.cpp - Raspberry Pi Image Processing Algorithms
+ */
+
+#include <algorithm>
+#include <array>
+#include <cstring>
+#include <deque>
+#include <fcntl.h>
+#include <math.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <vector>
+
+#include <linux/bcm2835-isp.h>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/shared_fd.h>
+#include <libcamera/base/span.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+#include <libcamera/framebuffer.h>
+#include <libcamera/request.h>
+
+#include <libcamera/ipa/ipa_interface.h>
+#include <libcamera/ipa/ipa_module_info.h>
+#include <libcamera/ipa/raspberrypi_ipa_interface.h>
+
+#include "libcamera/internal/mapped_framebuffer.h"
+
+#include "cam_helper/cam_helper.h"
+#include "controller/af_algorithm.h"
+#include "controller/af_status.h"
+#include "controller/agc_algorithm.h"
+#include "controller/agc_status.h"
+#include "controller/alsc_status.h"
+#include "controller/awb_algorithm.h"
+#include "controller/awb_status.h"
+#include "controller/black_level_status.h"
+#include "controller/ccm_algorithm.h"
+#include "controller/ccm_status.h"
+#include "controller/contrast_algorithm.h"
+#include "controller/contrast_status.h"
+#include "controller/controller.h"
+#include "controller/denoise_algorithm.h"
+#include "controller/denoise_status.h"
+#include "controller/dpc_status.h"
+#include "controller/geq_status.h"
+#include "controller/lux_status.h"
+#include "controller/metadata.h"
+#include "controller/sharpen_algorithm.h"
+#include "controller/sharpen_status.h"
+#include "controller/statistics.h"
+
+namespace libcamera {
+
+using namespace std::literals::chrono_literals;
+using utils::Duration;
+
+/* Number of metadata objects available in the context list. */
+constexpr unsigned int numMetadataContexts = 16;
+
+/* Number of frame length times to hold in the queue. */
+constexpr unsigned int FrameLengthsQueueSize = 10;
+
+/* Configure the sensor with these values initially. */
+constexpr double defaultAnalogueGain = 1.0;
+constexpr Duration defaultExposureTime = 20.0ms;
+constexpr Duration defaultMinFrameDuration = 1.0s / 30.0;
+constexpr Duration defaultMaxFrameDuration = 250.0s;
+
+/*
+ * Determine the minimum allowable inter-frame duration to run the controller
+ * algorithms. If the pipeline handler provider frames at a rate higher than this,
+ * we rate-limit the controller Prepare() and Process() calls to lower than or
+ * equal to this rate.
+ */
+constexpr Duration controllerMinFrameDuration = 1.0s / 30.0;
+
+/* List of controls handled by the Raspberry Pi IPA */
+static const ControlInfoMap::Map ipaControls{
+	{ &controls::AeEnable, ControlInfo(false, true) },
+	{ &controls::ExposureTime, ControlInfo(0, 66666) },
+	{ &controls::AnalogueGain, ControlInfo(1.0f, 16.0f) },
+	{ &controls::AeMeteringMode, ControlInfo(controls::AeMeteringModeValues) },
+	{ &controls::AeConstraintMode, ControlInfo(controls::AeConstraintModeValues) },
+	{ &controls::AeExposureMode, ControlInfo(controls::AeExposureModeValues) },
+	{ &controls::ExposureValue, ControlInfo(-8.0f, 8.0f, 0.0f) },
+	{ &controls::AwbEnable, ControlInfo(false, true) },
+	{ &controls::ColourGains, ControlInfo(0.0f, 32.0f) },
+	{ &controls::AwbMode, ControlInfo(controls::AwbModeValues) },
+	{ &controls::Brightness, ControlInfo(-1.0f, 1.0f, 0.0f) },
+	{ &controls::Contrast, ControlInfo(0.0f, 32.0f, 1.0f) },
+	{ &controls::Saturation, ControlInfo(0.0f, 32.0f, 1.0f) },
+	{ &controls::Sharpness, ControlInfo(0.0f, 16.0f, 1.0f) },
+	{ &controls::ColourCorrectionMatrix, ControlInfo(-16.0f, 16.0f) },
+	{ &controls::ScalerCrop, ControlInfo(Rectangle{}, Rectangle(65535, 65535, 65535, 65535), Rectangle{}) },
+	{ &controls::FrameDurationLimits, ControlInfo(INT64_C(33333), INT64_C(120000)) },
+	{ &controls::draft::NoiseReductionMode, ControlInfo(controls::draft::NoiseReductionModeValues) }
+};
+
+/* IPA controls handled conditionally, if the lens has a focus control */
+static const ControlInfoMap::Map ipaAfControls{
+	{ &controls::AfMode, ControlInfo(controls::AfModeValues) },
+	{ &controls::AfRange, ControlInfo(controls::AfRangeValues) },
+	{ &controls::AfSpeed, ControlInfo(controls::AfSpeedValues) },
+	{ &controls::AfMetering, ControlInfo(controls::AfMeteringValues) },
+	{ &controls::AfWindows, ControlInfo(Rectangle{}, Rectangle(65535, 65535, 65535, 65535), Rectangle{}) },
+	{ &controls::AfTrigger, ControlInfo(controls::AfTriggerValues) },
+	{ &controls::AfPause, ControlInfo(controls::AfPauseValues) },
+	{ &controls::LensPosition, ControlInfo(0.0f, 32.0f, 1.0f) }
+};
+
+LOG_DEFINE_CATEGORY(IPARPI)
+
+namespace ipa::RPi {
+
+class IPARPi : public IPARPiInterface
+{
+public:
+	IPARPi()
+		: controller_(), frameCount_(0), checkCount_(0), mistrustCount_(0),
+		  lastRunTimestamp_(0), lsTable_(nullptr), firstStart_(true),
+		  lastTimeout_(0s)
+	{
+	}
+
+	~IPARPi()
+	{
+		if (lsTable_)
+			munmap(lsTable_, MaxLsGridSize);
+	}
+
+	int init(const IPASettings &settings, bool lensPresent, IPAInitResult *result) override;
+	void start(const ControlList &controls, StartConfig *startConfig) override;
+	void stop() override {}
+
+	int configure(const IPACameraSensorInfo &sensorInfo, const IPAConfig &data,
+		      ControlList *controls, IPAConfigResult *result) override;
+	void mapBuffers(const std::vector<IPABuffer> &buffers) override;
+	void unmapBuffers(const std::vector<unsigned int> &ids) override;
+	void signalStatReady(const uint32_t bufferId, uint32_t ipaContext) override;
+	void signalQueueRequest(const ControlList &controls) override;
+	void signalIspPrepare(const ISPConfig &data) override;
+
+private:
+	void setMode(const IPACameraSensorInfo &sensorInfo);
+	bool validateSensorControls();
+	bool validateIspControls();
+	bool validateLensControls();
+	void queueRequest(const ControlList &controls);
+	void returnEmbeddedBuffer(unsigned int bufferId);
+	void prepareISP(const ISPConfig &data);
+	void reportMetadata(unsigned int ipaContext);
+	void fillDeviceStatus(const ControlList &sensorControls, unsigned int ipaContext);
+	RPiController::StatisticsPtr fillStatistics(bcm2835_isp_stats *stats) const;
+	void processStats(unsigned int bufferId, unsigned int ipaContext);
+	void setCameraTimeoutValue();
+	void applyFrameDurations(Duration minFrameDuration, Duration maxFrameDuration);
+	void applyAGC(const struct AgcStatus *agcStatus, ControlList &ctrls);
+	void applyAWB(const struct AwbStatus *awbStatus, ControlList &ctrls);
+	void applyDG(const struct AgcStatus *dgStatus, ControlList &ctrls);
+	void applyCCM(const struct CcmStatus *ccmStatus, ControlList &ctrls);
+	void applyBlackLevel(const struct BlackLevelStatus *blackLevelStatus, ControlList &ctrls);
+	void applyGamma(const struct ContrastStatus *contrastStatus, ControlList &ctrls);
+	void applyGEQ(const struct GeqStatus *geqStatus, ControlList &ctrls);
+	void applyDenoise(const struct DenoiseStatus *denoiseStatus, ControlList &ctrls);
+	void applySharpen(const struct SharpenStatus *sharpenStatus, ControlList &ctrls);
+	void applyDPC(const struct DpcStatus *dpcStatus, ControlList &ctrls);
+	void applyLS(const struct AlscStatus *lsStatus, ControlList &ctrls);
+	void applyAF(const struct AfStatus *afStatus, ControlList &lensCtrls);
+	void resampleTable(uint16_t dest[], const std::vector<double> &src, int destW, int destH);
+
+	std::map<unsigned int, MappedFrameBuffer> buffers_;
+
+	ControlInfoMap sensorCtrls_;
+	ControlInfoMap ispCtrls_;
+	ControlInfoMap lensCtrls_;
+	bool lensPresent_;
+	ControlList libcameraMetadata_;
+
+	/* Camera sensor params. */
+	CameraMode mode_;
+
+	/* Raspberry Pi controller specific defines. */
+	std::unique_ptr<RPiController::CamHelper> helper_;
+	RPiController::Controller controller_;
+	std::array<RPiController::Metadata, numMetadataContexts> rpiMetadata_;
+
+	/*
+	 * We count frames to decide if the frame must be hidden (e.g. from
+	 * display) or mistrusted (i.e. not given to the control algos).
+	 */
+	uint64_t frameCount_;
+
+	/* For checking the sequencing of Prepare/Process calls. */
+	uint64_t checkCount_;
+
+	/* How many frames we should avoid running control algos on. */
+	unsigned int mistrustCount_;
+
+	/* Number of frames that need to be dropped on startup. */
+	unsigned int dropFrameCount_;
+
+	/* Frame timestamp for the last run of the controller. */
+	uint64_t lastRunTimestamp_;
+
+	/* Do we run a Controller::process() for this frame? */
+	bool processPending_;
+
+	/* LS table allocation passed in from the pipeline handler. */
+	SharedFD lsTableHandle_;
+	void *lsTable_;
+
+	/* Distinguish the first camera start from others. */
+	bool firstStart_;
+
+	/* Frame duration (1/fps) limits. */
+	Duration minFrameDuration_;
+	Duration maxFrameDuration_;
+
+	/* Track the frame length times over FrameLengthsQueueSize frames. */
+	std::deque<Duration> frameLengths_;
+	Duration lastTimeout_;
+};
+
+int IPARPi::init(const IPASettings &settings, bool lensPresent, IPAInitResult *result)
+{
+	/*
+	 * Load the "helper" for this sensor. This tells us all the device specific stuff
+	 * that the kernel driver doesn't. We only do this the first time; we don't need
+	 * to re-parse the metadata after a simple mode-switch for no reason.
+	 */
+	helper_ = std::unique_ptr<RPiController::CamHelper>(RPiController::CamHelper::create(settings.sensorModel));
+	if (!helper_) {
+		LOG(IPARPI, Error) << "Could not create camera helper for "
+				   << settings.sensorModel;
+		return -EINVAL;
+	}
+
+	/*
+	 * Pass out the sensor config to the pipeline handler in order
+	 * to setup the staggered writer class.
+	 */
+	int gainDelay, exposureDelay, vblankDelay, hblankDelay, sensorMetadata;
+	helper_->getDelays(exposureDelay, gainDelay, vblankDelay, hblankDelay);
+	sensorMetadata = helper_->sensorEmbeddedDataPresent();
+
+	result->sensorConfig.gainDelay = gainDelay;
+	result->sensorConfig.exposureDelay = exposureDelay;
+	result->sensorConfig.vblankDelay = vblankDelay;
+	result->sensorConfig.hblankDelay = hblankDelay;
+	result->sensorConfig.sensorMetadata = sensorMetadata;
+
+	/* Load the tuning file for this sensor. */
+	int ret = controller_.read(settings.configurationFile.c_str());
+	if (ret) {
+		LOG(IPARPI, Error)
+			<< "Failed to load tuning data file "
+			<< settings.configurationFile;
+		return ret;
+	}
+
+	const std::string &target = controller_.getTarget();
+	if (target != "bcm2835") {
+		LOG(IPARPI, Error)
+			<< "Tuning data file target returned \"" << target << "\""
+			<< ", expected \"bcm2835\"";
+		return -EINVAL;
+	}
+
+	lensPresent_ = lensPresent;
+
+	controller_.initialise();
+
+	/* Return the controls handled by the IPA */
+	ControlInfoMap::Map ctrlMap = ipaControls;
+	if (lensPresent_)
+		ctrlMap.merge(ControlInfoMap::Map(ipaAfControls));
+	result->controlInfo = ControlInfoMap(std::move(ctrlMap), controls::controls);
+
+	return 0;
+}
+
+void IPARPi::start(const ControlList &controls, StartConfig *startConfig)
+{
+	RPiController::Metadata metadata;
+
+	ASSERT(startConfig);
+	if (!controls.empty()) {
+		/* We have been given some controls to action before start. */
+		queueRequest(controls);
+	}
+
+	controller_.switchMode(mode_, &metadata);
+
+	/* Reset the frame lengths queue state. */
+	lastTimeout_ = 0s;
+	frameLengths_.clear();
+	frameLengths_.resize(FrameLengthsQueueSize, 0s);
+
+	/* SwitchMode may supply updated exposure/gain values to use. */
+	AgcStatus agcStatus;
+	agcStatus.shutterTime = 0.0s;
+	agcStatus.analogueGain = 0.0;
+
+	metadata.get("agc.status", agcStatus);
+	if (agcStatus.shutterTime && agcStatus.analogueGain) {
+		ControlList ctrls(sensorCtrls_);
+		applyAGC(&agcStatus, ctrls);
+		startConfig->controls = std::move(ctrls);
+		setCameraTimeoutValue();
+	}
+
+	/*
+	 * Initialise frame counts, and decide how many frames must be hidden or
+	 * "mistrusted", which depends on whether this is a startup from cold,
+	 * or merely a mode switch in a running system.
+	 */
+	frameCount_ = 0;
+	checkCount_ = 0;
+	if (firstStart_) {
+		dropFrameCount_ = helper_->hideFramesStartup();
+		mistrustCount_ = helper_->mistrustFramesStartup();
+
+		/*
+		 * Query the AGC/AWB for how many frames they may take to
+		 * converge sufficiently. Where these numbers are non-zero
+		 * we must allow for the frames with bad statistics
+		 * (mistrustCount_) that they won't see. But if zero (i.e.
+		 * no convergence necessary), no frames need to be dropped.
+		 */
+		unsigned int agcConvergenceFrames = 0;
+		RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+			controller_.getAlgorithm("agc"));
+		if (agc) {
+			agcConvergenceFrames = agc->getConvergenceFrames();
+			if (agcConvergenceFrames)
+				agcConvergenceFrames += mistrustCount_;
+		}
+
+		unsigned int awbConvergenceFrames = 0;
+		RPiController::AwbAlgorithm *awb = dynamic_cast<RPiController::AwbAlgorithm *>(
+			controller_.getAlgorithm("awb"));
+		if (awb) {
+			awbConvergenceFrames = awb->getConvergenceFrames();
+			if (awbConvergenceFrames)
+				awbConvergenceFrames += mistrustCount_;
+		}
+
+		dropFrameCount_ = std::max({ dropFrameCount_, agcConvergenceFrames, awbConvergenceFrames });
+		LOG(IPARPI, Debug) << "Drop " << dropFrameCount_ << " frames on startup";
+	} else {
+		dropFrameCount_ = helper_->hideFramesModeSwitch();
+		mistrustCount_ = helper_->mistrustFramesModeSwitch();
+	}
+
+	startConfig->dropFrameCount = dropFrameCount_;
+
+	firstStart_ = false;
+	lastRunTimestamp_ = 0;
+}
+
+void IPARPi::setMode(const IPACameraSensorInfo &sensorInfo)
+{
+	mode_.bitdepth = sensorInfo.bitsPerPixel;
+	mode_.width = sensorInfo.outputSize.width;
+	mode_.height = sensorInfo.outputSize.height;
+	mode_.sensorWidth = sensorInfo.activeAreaSize.width;
+	mode_.sensorHeight = sensorInfo.activeAreaSize.height;
+	mode_.cropX = sensorInfo.analogCrop.x;
+	mode_.cropY = sensorInfo.analogCrop.y;
+	mode_.pixelRate = sensorInfo.pixelRate;
+
+	/*
+	 * Calculate scaling parameters. The scale_[xy] factors are determined
+	 * by the ratio between the crop rectangle size and the output size.
+	 */
+	mode_.scaleX = sensorInfo.analogCrop.width / sensorInfo.outputSize.width;
+	mode_.scaleY = sensorInfo.analogCrop.height / sensorInfo.outputSize.height;
+
+	/*
+	 * We're not told by the pipeline handler how scaling is split between
+	 * binning and digital scaling. For now, as a heuristic, assume that
+	 * downscaling up to 2 is achieved through binning, and that any
+	 * additional scaling is achieved through digital scaling.
+	 *
+	 * \todo Get the pipeline handle to provide the full data
+	 */
+	mode_.binX = std::min(2, static_cast<int>(mode_.scaleX));
+	mode_.binY = std::min(2, static_cast<int>(mode_.scaleY));
+
+	/* The noise factor is the square root of the total binning factor. */
+	mode_.noiseFactor = sqrt(mode_.binX * mode_.binY);
+
+	/*
+	 * Calculate the line length as the ratio between the line length in
+	 * pixels and the pixel rate.
+	 */
+	mode_.minLineLength = sensorInfo.minLineLength * (1.0s / sensorInfo.pixelRate);
+	mode_.maxLineLength = sensorInfo.maxLineLength * (1.0s / sensorInfo.pixelRate);
+
+	/*
+	 * Set the frame length limits for the mode to ensure exposure and
+	 * framerate calculations are clipped appropriately.
+	 */
+	mode_.minFrameLength = sensorInfo.minFrameLength;
+	mode_.maxFrameLength = sensorInfo.maxFrameLength;
+
+	/* Store these for convenience. */
+	mode_.minFrameDuration = mode_.minFrameLength * mode_.minLineLength;
+	mode_.maxFrameDuration = mode_.maxFrameLength * mode_.maxLineLength;
+
+	/*
+	 * Some sensors may have different sensitivities in different modes;
+	 * the CamHelper will know the correct value.
+	 */
+	mode_.sensitivity = helper_->getModeSensitivity(mode_);
+
+	const ControlInfo &gainCtrl = sensorCtrls_.at(V4L2_CID_ANALOGUE_GAIN);
+	const ControlInfo &shutterCtrl = sensorCtrls_.at(V4L2_CID_EXPOSURE);
+
+	mode_.minAnalogueGain = helper_->gain(gainCtrl.min().get<int32_t>());
+	mode_.maxAnalogueGain = helper_->gain(gainCtrl.max().get<int32_t>());
+
+	/* Shutter speed is calculated based on the limits of the frame durations. */
+	mode_.minShutter = helper_->exposure(shutterCtrl.min().get<int32_t>(), mode_.minLineLength);
+	mode_.maxShutter = Duration::max();
+	helper_->getBlanking(mode_.maxShutter,
+			     mode_.minFrameDuration, mode_.maxFrameDuration);
+}
+
+int IPARPi::configure(const IPACameraSensorInfo &sensorInfo, const IPAConfig &ipaConfig,
+		      ControlList *controls, IPAConfigResult *result)
+{
+	sensorCtrls_ = ipaConfig.sensorControls;
+	ispCtrls_ = ipaConfig.ispControls;
+
+	if (!validateSensorControls()) {
+		LOG(IPARPI, Error) << "Sensor control validation failed.";
+		return -1;
+	}
+
+	if (!validateIspControls()) {
+		LOG(IPARPI, Error) << "ISP control validation failed.";
+		return -1;
+	}
+
+	if (lensPresent_) {
+		lensCtrls_ = ipaConfig.lensControls;
+		if (!validateLensControls()) {
+			LOG(IPARPI, Warning) << "Lens validation failed, "
+					     << "no lens control will be available.";
+			lensPresent_ = false;
+		}
+	}
+
+	/* Setup a metadata ControlList to output metadata. */
+	libcameraMetadata_ = ControlList(controls::controls);
+
+	/* Re-assemble camera mode using the sensor info. */
+	setMode(sensorInfo);
+
+	mode_.transform = static_cast<libcamera::Transform>(ipaConfig.transform);
+
+	/* Store the lens shading table pointer and handle if available. */
+	if (ipaConfig.lsTableHandle.isValid()) {
+		/* Remove any previous table, if there was one. */
+		if (lsTable_) {
+			munmap(lsTable_, MaxLsGridSize);
+			lsTable_ = nullptr;
+		}
+
+		/* Map the LS table buffer into user space. */
+		lsTableHandle_ = std::move(ipaConfig.lsTableHandle);
+		if (lsTableHandle_.isValid()) {
+			lsTable_ = mmap(nullptr, MaxLsGridSize, PROT_READ | PROT_WRITE,
+					MAP_SHARED, lsTableHandle_.get(), 0);
+
+			if (lsTable_ == MAP_FAILED) {
+				LOG(IPARPI, Error) << "dmaHeap mmap failure for LS table.";
+				lsTable_ = nullptr;
+			}
+		}
+	}
+
+	/* Pass the camera mode to the CamHelper to setup algorithms. */
+	helper_->setCameraMode(mode_);
+
+	/*
+	 * Initialise this ControlList correctly, even if empty, in case the IPA is
+	 * running is isolation mode (passing the ControlList through the IPC layer).
+	 */
+	ControlList ctrls(sensorCtrls_);
+
+	/* The pipeline handler passes out the mode's sensitivity. */
+	result->modeSensitivity = mode_.sensitivity;
+
+	if (firstStart_) {
+		/* Supply initial values for frame durations. */
+		applyFrameDurations(defaultMinFrameDuration, defaultMaxFrameDuration);
+
+		/* Supply initial values for gain and exposure. */
+		AgcStatus agcStatus;
+		agcStatus.shutterTime = defaultExposureTime;
+		agcStatus.analogueGain = defaultAnalogueGain;
+		applyAGC(&agcStatus, ctrls);
+	}
+
+	ASSERT(controls);
+	*controls = std::move(ctrls);
+
+	/*
+	 * Apply the correct limits to the exposure, gain and frame duration controls
+	 * based on the current sensor mode.
+	 */
+	ControlInfoMap::Map ctrlMap = ipaControls;
+	ctrlMap[&controls::FrameDurationLimits] =
+		ControlInfo(static_cast<int64_t>(mode_.minFrameDuration.get<std::micro>()),
+			    static_cast<int64_t>(mode_.maxFrameDuration.get<std::micro>()));
+
+	ctrlMap[&controls::AnalogueGain] =
+		ControlInfo(static_cast<float>(mode_.minAnalogueGain),
+			    static_cast<float>(mode_.maxAnalogueGain));
+
+	ctrlMap[&controls::ExposureTime] =
+		ControlInfo(static_cast<int32_t>(mode_.minShutter.get<std::micro>()),
+			    static_cast<int32_t>(mode_.maxShutter.get<std::micro>()));
+
+	/* Declare Autofocus controls, only if we have a controllable lens */
+	if (lensPresent_)
+		ctrlMap.merge(ControlInfoMap::Map(ipaAfControls));
+
+	result->controlInfo = ControlInfoMap(std::move(ctrlMap), controls::controls);
+	return 0;
+}
+
+void IPARPi::mapBuffers(const std::vector<IPABuffer> &buffers)
+{
+	for (const IPABuffer &buffer : buffers) {
+		const FrameBuffer fb(buffer.planes);
+		buffers_.emplace(buffer.id,
+				 MappedFrameBuffer(&fb, MappedFrameBuffer::MapFlag::ReadWrite));
+	}
+}
+
+void IPARPi::unmapBuffers(const std::vector<unsigned int> &ids)
+{
+	for (unsigned int id : ids) {
+		auto it = buffers_.find(id);
+		if (it == buffers_.end())
+			continue;
+
+		buffers_.erase(id);
+	}
+}
+
+void IPARPi::signalStatReady(uint32_t bufferId, uint32_t ipaContext)
+{
+	unsigned int context = ipaContext % rpiMetadata_.size();
+
+	if (++checkCount_ != frameCount_) /* assert here? */
+		LOG(IPARPI, Error) << "WARNING: Prepare/Process mismatch!!!";
+	if (processPending_ && frameCount_ > mistrustCount_)
+		processStats(bufferId, context);
+
+	reportMetadata(context);
+
+	statsMetadataComplete.emit(bufferId, libcameraMetadata_);
+}
+
+void IPARPi::signalQueueRequest(const ControlList &controls)
+{
+	queueRequest(controls);
+}
+
+void IPARPi::signalIspPrepare(const ISPConfig &data)
+{
+	/*
+	 * At start-up, or after a mode-switch, we may want to
+	 * avoid running the control algos for a few frames in case
+	 * they are "unreliable".
+	 */
+	prepareISP(data);
+	frameCount_++;
+
+	/* Ready to push the input buffer into the ISP. */
+	runIsp.emit(data.bayerBufferId);
+}
+
+void IPARPi::reportMetadata(unsigned int ipaContext)
+{
+	RPiController::Metadata &rpiMetadata = rpiMetadata_[ipaContext];
+	std::unique_lock<RPiController::Metadata> lock(rpiMetadata);
+
+	/*
+	 * Certain information about the current frame and how it will be
+	 * processed can be extracted and placed into the libcamera metadata
+	 * buffer, where an application could query it.
+	 */
+	DeviceStatus *deviceStatus = rpiMetadata.getLocked<DeviceStatus>("device.status");
+	if (deviceStatus) {
+		libcameraMetadata_.set(controls::ExposureTime,
+				       deviceStatus->shutterSpeed.get<std::micro>());
+		libcameraMetadata_.set(controls::AnalogueGain, deviceStatus->analogueGain);
+		libcameraMetadata_.set(controls::FrameDuration,
+				       helper_->exposure(deviceStatus->frameLength, deviceStatus->lineLength).get<std::micro>());
+		if (deviceStatus->sensorTemperature)
+			libcameraMetadata_.set(controls::SensorTemperature, *deviceStatus->sensorTemperature);
+		if (deviceStatus->lensPosition)
+			libcameraMetadata_.set(controls::LensPosition, *deviceStatus->lensPosition);
+	}
+
+	AgcStatus *agcStatus = rpiMetadata.getLocked<AgcStatus>("agc.status");
+	if (agcStatus) {
+		libcameraMetadata_.set(controls::AeLocked, agcStatus->locked);
+		libcameraMetadata_.set(controls::DigitalGain, agcStatus->digitalGain);
+	}
+
+	LuxStatus *luxStatus = rpiMetadata.getLocked<LuxStatus>("lux.status");
+	if (luxStatus)
+		libcameraMetadata_.set(controls::Lux, luxStatus->lux);
+
+	AwbStatus *awbStatus = rpiMetadata.getLocked<AwbStatus>("awb.status");
+	if (awbStatus) {
+		libcameraMetadata_.set(controls::ColourGains, { static_cast<float>(awbStatus->gainR),
+								static_cast<float>(awbStatus->gainB) });
+		libcameraMetadata_.set(controls::ColourTemperature, awbStatus->temperatureK);
+	}
+
+	BlackLevelStatus *blackLevelStatus = rpiMetadata.getLocked<BlackLevelStatus>("black_level.status");
+	if (blackLevelStatus)
+		libcameraMetadata_.set(controls::SensorBlackLevels,
+				       { static_cast<int32_t>(blackLevelStatus->blackLevelR),
+					 static_cast<int32_t>(blackLevelStatus->blackLevelG),
+					 static_cast<int32_t>(blackLevelStatus->blackLevelG),
+					 static_cast<int32_t>(blackLevelStatus->blackLevelB) });
+
+	RPiController::FocusRegions *focusStatus =
+		rpiMetadata.getLocked<RPiController::FocusRegions>("focus.status");
+	if (focusStatus) {
+		/*
+		 * Calculate the average FoM over the central (symmetric) positions
+		 * to give an overall scene FoM. This can change later if it is
+		 * not deemed suitable.
+		 */
+		libcamera::Size size = focusStatus->size();
+		unsigned rows = size.height;
+		unsigned cols = size.width;
+
+		uint64_t sum = 0;
+		unsigned int numRegions = 0;
+		for (unsigned r = rows / 3; r < rows - rows / 3; ++r) {
+			for (unsigned c = cols / 4; c < cols - cols / 4; ++c) {
+				sum += focusStatus->get({ (int)c, (int)r }).val;
+				numRegions++;
+			}
+		}
+
+		uint32_t focusFoM = (sum / numRegions) >> 16;
+		libcameraMetadata_.set(controls::FocusFoM, focusFoM);
+	}
+
+	CcmStatus *ccmStatus = rpiMetadata.getLocked<CcmStatus>("ccm.status");
+	if (ccmStatus) {
+		float m[9];
+		for (unsigned int i = 0; i < 9; i++)
+			m[i] = ccmStatus->matrix[i];
+		libcameraMetadata_.set(controls::ColourCorrectionMatrix, m);
+	}
+
+	const AfStatus *afStatus = rpiMetadata.getLocked<AfStatus>("af.status");
+	if (afStatus) {
+		int32_t s, p;
+		switch (afStatus->state) {
+		case AfState::Scanning:
+			s = controls::AfStateScanning;
+			break;
+		case AfState::Focused:
+			s = controls::AfStateFocused;
+			break;
+		case AfState::Failed:
+			s = controls::AfStateFailed;
+			break;
+		default:
+			s = controls::AfStateIdle;
+		}
+		switch (afStatus->pauseState) {
+		case AfPauseState::Pausing:
+			p = controls::AfPauseStatePausing;
+			break;
+		case AfPauseState::Paused:
+			p = controls::AfPauseStatePaused;
+			break;
+		default:
+			p = controls::AfPauseStateRunning;
+		}
+		libcameraMetadata_.set(controls::AfState, s);
+		libcameraMetadata_.set(controls::AfPauseState, p);
+	}
+}
+
+bool IPARPi::validateSensorControls()
+{
+	static const uint32_t ctrls[] = {
+		V4L2_CID_ANALOGUE_GAIN,
+		V4L2_CID_EXPOSURE,
+		V4L2_CID_VBLANK,
+		V4L2_CID_HBLANK,
+	};
+
+	for (auto c : ctrls) {
+		if (sensorCtrls_.find(c) == sensorCtrls_.end()) {
+			LOG(IPARPI, Error) << "Unable to find sensor control "
+					   << utils::hex(c);
+			return false;
+		}
+	}
+
+	return true;
+}
+
+bool IPARPi::validateIspControls()
+{
+	static const uint32_t ctrls[] = {
+		V4L2_CID_RED_BALANCE,
+		V4L2_CID_BLUE_BALANCE,
+		V4L2_CID_DIGITAL_GAIN,
+		V4L2_CID_USER_BCM2835_ISP_CC_MATRIX,
+		V4L2_CID_USER_BCM2835_ISP_GAMMA,
+		V4L2_CID_USER_BCM2835_ISP_BLACK_LEVEL,
+		V4L2_CID_USER_BCM2835_ISP_GEQ,
+		V4L2_CID_USER_BCM2835_ISP_DENOISE,
+		V4L2_CID_USER_BCM2835_ISP_SHARPEN,
+		V4L2_CID_USER_BCM2835_ISP_DPC,
+		V4L2_CID_USER_BCM2835_ISP_LENS_SHADING,
+		V4L2_CID_USER_BCM2835_ISP_CDN,
+	};
+
+	for (auto c : ctrls) {
+		if (ispCtrls_.find(c) == ispCtrls_.end()) {
+			LOG(IPARPI, Error) << "Unable to find ISP control "
+					   << utils::hex(c);
+			return false;
+		}
+	}
+
+	return true;
+}
+
+bool IPARPi::validateLensControls()
+{
+	if (lensCtrls_.find(V4L2_CID_FOCUS_ABSOLUTE) == lensCtrls_.end()) {
+		LOG(IPARPI, Error) << "Unable to find Lens control V4L2_CID_FOCUS_ABSOLUTE";
+		return false;
+	}
+
+	return true;
+}
+
+/*
+ * Converting between enums (used in the libcamera API) and the names that
+ * we use to identify different modes. Unfortunately, the conversion tables
+ * must be kept up-to-date by hand.
+ */
+static const std::map<int32_t, std::string> MeteringModeTable = {
+	{ controls::MeteringCentreWeighted, "centre-weighted" },
+	{ controls::MeteringSpot, "spot" },
+	{ controls::MeteringMatrix, "matrix" },
+	{ controls::MeteringCustom, "custom" },
+};
+
+static const std::map<int32_t, std::string> ConstraintModeTable = {
+	{ controls::ConstraintNormal, "normal" },
+	{ controls::ConstraintHighlight, "highlight" },
+	{ controls::ConstraintShadows, "shadows" },
+	{ controls::ConstraintCustom, "custom" },
+};
+
+static const std::map<int32_t, std::string> ExposureModeTable = {
+	{ controls::ExposureNormal, "normal" },
+	{ controls::ExposureShort, "short" },
+	{ controls::ExposureLong, "long" },
+	{ controls::ExposureCustom, "custom" },
+};
+
+static const std::map<int32_t, std::string> AwbModeTable = {
+	{ controls::AwbAuto, "auto" },
+	{ controls::AwbIncandescent, "incandescent" },
+	{ controls::AwbTungsten, "tungsten" },
+	{ controls::AwbFluorescent, "fluorescent" },
+	{ controls::AwbIndoor, "indoor" },
+	{ controls::AwbDaylight, "daylight" },
+	{ controls::AwbCloudy, "cloudy" },
+	{ controls::AwbCustom, "custom" },
+};
+
+static const std::map<int32_t, RPiController::DenoiseMode> DenoiseModeTable = {
+	{ controls::draft::NoiseReductionModeOff, RPiController::DenoiseMode::Off },
+	{ controls::draft::NoiseReductionModeFast, RPiController::DenoiseMode::ColourFast },
+	{ controls::draft::NoiseReductionModeHighQuality, RPiController::DenoiseMode::ColourHighQuality },
+	{ controls::draft::NoiseReductionModeMinimal, RPiController::DenoiseMode::ColourOff },
+	{ controls::draft::NoiseReductionModeZSL, RPiController::DenoiseMode::ColourHighQuality },
+};
+
+static const std::map<int32_t, RPiController::AfAlgorithm::AfMode> AfModeTable = {
+	{ controls::AfModeManual, RPiController::AfAlgorithm::AfModeManual },
+	{ controls::AfModeAuto, RPiController::AfAlgorithm::AfModeAuto },
+	{ controls::AfModeContinuous, RPiController::AfAlgorithm::AfModeContinuous },
+};
+
+static const std::map<int32_t, RPiController::AfAlgorithm::AfRange> AfRangeTable = {
+	{ controls::AfRangeNormal, RPiController::AfAlgorithm::AfRangeNormal },
+	{ controls::AfRangeMacro, RPiController::AfAlgorithm::AfRangeMacro },
+	{ controls::AfRangeFull, RPiController::AfAlgorithm::AfRangeFull },
+};
+
+static const std::map<int32_t, RPiController::AfAlgorithm::AfPause> AfPauseTable = {
+	{ controls::AfPauseImmediate, RPiController::AfAlgorithm::AfPauseImmediate },
+	{ controls::AfPauseDeferred, RPiController::AfAlgorithm::AfPauseDeferred },
+	{ controls::AfPauseResume, RPiController::AfAlgorithm::AfPauseResume },
+};
+
+void IPARPi::queueRequest(const ControlList &controls)
+{
+	using RPiController::AfAlgorithm;
+
+	/* Clear the return metadata buffer. */
+	libcameraMetadata_.clear();
+
+	/* Because some AF controls are mode-specific, handle AF mode change first. */
+	if (controls.contains(controls::AF_MODE)) {
+		AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
+		if (!af) {
+			LOG(IPARPI, Warning)
+				<< "Could not set AF_MODE - no AF algorithm";
+		}
+
+		int32_t idx = controls.get(controls::AF_MODE).get<int32_t>();
+		auto mode = AfModeTable.find(idx);
+		if (mode == AfModeTable.end()) {
+			LOG(IPARPI, Error) << "AF mode " << idx
+					   << " not recognised";
+		} else
+			af->setMode(mode->second);
+	}
+
+	/* Iterate over controls */
+	for (auto const &ctrl : controls) {
+		LOG(IPARPI, Debug) << "Request ctrl: "
+				   << controls::controls.at(ctrl.first)->name()
+				   << " = " << ctrl.second.toString();
+
+		switch (ctrl.first) {
+		case controls::AE_ENABLE: {
+			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+				controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AE_ENABLE - no AGC algorithm";
+				break;
+			}
+
+			if (ctrl.second.get<bool>() == false)
+				agc->disableAuto();
+			else
+				agc->enableAuto();
+
+			libcameraMetadata_.set(controls::AeEnable, ctrl.second.get<bool>());
+			break;
+		}
+
+		case controls::EXPOSURE_TIME: {
+			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+				controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning)
+					<< "Could not set EXPOSURE_TIME - no AGC algorithm";
+				break;
+			}
+
+			/* The control provides units of microseconds. */
+			agc->setFixedShutter(ctrl.second.get<int32_t>() * 1.0us);
+
+			libcameraMetadata_.set(controls::ExposureTime, ctrl.second.get<int32_t>());
+			break;
+		}
+
+		case controls::ANALOGUE_GAIN: {
+			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+				controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning)
+					<< "Could not set ANALOGUE_GAIN - no AGC algorithm";
+				break;
+			}
+
+			agc->setFixedAnalogueGain(ctrl.second.get<float>());
+
+			libcameraMetadata_.set(controls::AnalogueGain,
+					       ctrl.second.get<float>());
+			break;
+		}
+
+		case controls::AE_METERING_MODE: {
+			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+				controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AE_METERING_MODE - no AGC algorithm";
+				break;
+			}
+
+			int32_t idx = ctrl.second.get<int32_t>();
+			if (MeteringModeTable.count(idx)) {
+				agc->setMeteringMode(MeteringModeTable.at(idx));
+				libcameraMetadata_.set(controls::AeMeteringMode, idx);
+			} else {
+				LOG(IPARPI, Error) << "Metering mode " << idx
+						   << " not recognised";
+			}
+			break;
+		}
+
+		case controls::AE_CONSTRAINT_MODE: {
+			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+				controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AE_CONSTRAINT_MODE - no AGC algorithm";
+				break;
+			}
+
+			int32_t idx = ctrl.second.get<int32_t>();
+			if (ConstraintModeTable.count(idx)) {
+				agc->setConstraintMode(ConstraintModeTable.at(idx));
+				libcameraMetadata_.set(controls::AeConstraintMode, idx);
+			} else {
+				LOG(IPARPI, Error) << "Constraint mode " << idx
+						   << " not recognised";
+			}
+			break;
+		}
+
+		case controls::AE_EXPOSURE_MODE: {
+			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+				controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AE_EXPOSURE_MODE - no AGC algorithm";
+				break;
+			}
+
+			int32_t idx = ctrl.second.get<int32_t>();
+			if (ExposureModeTable.count(idx)) {
+				agc->setExposureMode(ExposureModeTable.at(idx));
+				libcameraMetadata_.set(controls::AeExposureMode, idx);
+			} else {
+				LOG(IPARPI, Error) << "Exposure mode " << idx
+						   << " not recognised";
+			}
+			break;
+		}
+
+		case controls::EXPOSURE_VALUE: {
+			RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+				controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning)
+					<< "Could not set EXPOSURE_VALUE - no AGC algorithm";
+				break;
+			}
+
+			/*
+			 * The SetEv() function takes in a direct exposure multiplier.
+			 * So convert to 2^EV
+			 */
+			double ev = pow(2.0, ctrl.second.get<float>());
+			agc->setEv(ev);
+			libcameraMetadata_.set(controls::ExposureValue,
+					       ctrl.second.get<float>());
+			break;
+		}
+
+		case controls::AWB_ENABLE: {
+			RPiController::AwbAlgorithm *awb = dynamic_cast<RPiController::AwbAlgorithm *>(
+				controller_.getAlgorithm("awb"));
+			if (!awb) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AWB_ENABLE - no AWB algorithm";
+				break;
+			}
+
+			if (ctrl.second.get<bool>() == false)
+				awb->disableAuto();
+			else
+				awb->enableAuto();
+
+			libcameraMetadata_.set(controls::AwbEnable,
+					       ctrl.second.get<bool>());
+			break;
+		}
+
+		case controls::AWB_MODE: {
+			RPiController::AwbAlgorithm *awb = dynamic_cast<RPiController::AwbAlgorithm *>(
+				controller_.getAlgorithm("awb"));
+			if (!awb) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AWB_MODE - no AWB algorithm";
+				break;
+			}
+
+			int32_t idx = ctrl.second.get<int32_t>();
+			if (AwbModeTable.count(idx)) {
+				awb->setMode(AwbModeTable.at(idx));
+				libcameraMetadata_.set(controls::AwbMode, idx);
+			} else {
+				LOG(IPARPI, Error) << "AWB mode " << idx
+						   << " not recognised";
+			}
+			break;
+		}
+
+		case controls::COLOUR_GAINS: {
+			auto gains = ctrl.second.get<Span<const float>>();
+			RPiController::AwbAlgorithm *awb = dynamic_cast<RPiController::AwbAlgorithm *>(
+				controller_.getAlgorithm("awb"));
+			if (!awb) {
+				LOG(IPARPI, Warning)
+					<< "Could not set COLOUR_GAINS - no AWB algorithm";
+				break;
+			}
+
+			awb->setManualGains(gains[0], gains[1]);
+			if (gains[0] != 0.0f && gains[1] != 0.0f)
+				/* A gain of 0.0f will switch back to auto mode. */
+				libcameraMetadata_.set(controls::ColourGains,
+						       { gains[0], gains[1] });
+			break;
+		}
+
+		case controls::BRIGHTNESS: {
+			RPiController::ContrastAlgorithm *contrast = dynamic_cast<RPiController::ContrastAlgorithm *>(
+				controller_.getAlgorithm("contrast"));
+			if (!contrast) {
+				LOG(IPARPI, Warning)
+					<< "Could not set BRIGHTNESS - no contrast algorithm";
+				break;
+			}
+
+			contrast->setBrightness(ctrl.second.get<float>() * 65536);
+			libcameraMetadata_.set(controls::Brightness,
+					       ctrl.second.get<float>());
+			break;
+		}
+
+		case controls::CONTRAST: {
+			RPiController::ContrastAlgorithm *contrast = dynamic_cast<RPiController::ContrastAlgorithm *>(
+				controller_.getAlgorithm("contrast"));
+			if (!contrast) {
+				LOG(IPARPI, Warning)
+					<< "Could not set CONTRAST - no contrast algorithm";
+				break;
+			}
+
+			contrast->setContrast(ctrl.second.get<float>());
+			libcameraMetadata_.set(controls::Contrast,
+					       ctrl.second.get<float>());
+			break;
+		}
+
+		case controls::SATURATION: {
+			RPiController::CcmAlgorithm *ccm = dynamic_cast<RPiController::CcmAlgorithm *>(
+				controller_.getAlgorithm("ccm"));
+			if (!ccm) {
+				LOG(IPARPI, Warning)
+					<< "Could not set SATURATION - no ccm algorithm";
+				break;
+			}
+
+			ccm->setSaturation(ctrl.second.get<float>());
+			libcameraMetadata_.set(controls::Saturation,
+					       ctrl.second.get<float>());
+			break;
+		}
+
+		case controls::SHARPNESS: {
+			RPiController::SharpenAlgorithm *sharpen = dynamic_cast<RPiController::SharpenAlgorithm *>(
+				controller_.getAlgorithm("sharpen"));
+			if (!sharpen) {
+				LOG(IPARPI, Warning)
+					<< "Could not set SHARPNESS - no sharpen algorithm";
+				break;
+			}
+
+			sharpen->setStrength(ctrl.second.get<float>());
+			libcameraMetadata_.set(controls::Sharpness,
+					       ctrl.second.get<float>());
+			break;
+		}
+
+		case controls::SCALER_CROP: {
+			/* We do nothing with this, but should avoid the warning below. */
+			break;
+		}
+
+		case controls::FRAME_DURATION_LIMITS: {
+			auto frameDurations = ctrl.second.get<Span<const int64_t>>();
+			applyFrameDurations(frameDurations[0] * 1.0us, frameDurations[1] * 1.0us);
+			break;
+		}
+
+		case controls::NOISE_REDUCTION_MODE: {
+			RPiController::DenoiseAlgorithm *sdn = dynamic_cast<RPiController::DenoiseAlgorithm *>(
+				controller_.getAlgorithm("SDN"));
+			if (!sdn) {
+				LOG(IPARPI, Warning)
+					<< "Could not set NOISE_REDUCTION_MODE - no SDN algorithm";
+				break;
+			}
+
+			int32_t idx = ctrl.second.get<int32_t>();
+			auto mode = DenoiseModeTable.find(idx);
+			if (mode != DenoiseModeTable.end()) {
+				sdn->setMode(mode->second);
+
+				/*
+				 * \todo If the colour denoise is not going to run due to an
+				 * analysis image resolution or format mismatch, we should
+				 * report the status correctly in the metadata.
+				 */
+				libcameraMetadata_.set(controls::draft::NoiseReductionMode, idx);
+			} else {
+				LOG(IPARPI, Error) << "Noise reduction mode " << idx
+						   << " not recognised";
+			}
+			break;
+		}
+
+		case controls::AF_MODE:
+			break; /* We already handled this one above */
+
+		case controls::AF_RANGE: {
+			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
+			if (!af) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AF_RANGE - no focus algorithm";
+				break;
+			}
+
+			auto range = AfRangeTable.find(ctrl.second.get<int32_t>());
+			if (range == AfRangeTable.end()) {
+				LOG(IPARPI, Error) << "AF range " << ctrl.second.get<int32_t>()
+						   << " not recognised";
+				break;
+			}
+			af->setRange(range->second);
+			break;
+		}
+
+		case controls::AF_SPEED: {
+			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
+			if (!af) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AF_SPEED - no focus algorithm";
+				break;
+			}
+
+			AfAlgorithm::AfSpeed speed = ctrl.second.get<int32_t>() == controls::AfSpeedFast ?
+						      AfAlgorithm::AfSpeedFast : AfAlgorithm::AfSpeedNormal;
+			af->setSpeed(speed);
+			break;
+		}
+
+		case controls::AF_METERING: {
+			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
+			if (!af) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AF_METERING - no AF algorithm";
+				break;
+			}
+			af->setMetering(ctrl.second.get<int32_t>() == controls::AfMeteringWindows);
+			break;
+		}
+
+		case controls::AF_WINDOWS: {
+			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
+			if (!af) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AF_WINDOWS - no AF algorithm";
+				break;
+			}
+			af->setWindows(ctrl.second.get<Span<const Rectangle>>());
+			break;
+		}
+
+		case controls::AF_PAUSE: {
+			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
+			if (!af || af->getMode() != AfAlgorithm::AfModeContinuous) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AF_PAUSE - no AF algorithm or not Continuous";
+				break;
+			}
+			auto pause = AfPauseTable.find(ctrl.second.get<int32_t>());
+			if (pause == AfPauseTable.end()) {
+				LOG(IPARPI, Error) << "AF pause " << ctrl.second.get<int32_t>()
+						   << " not recognised";
+				break;
+			}
+			af->pause(pause->second);
+			break;
+		}
+
+		case controls::AF_TRIGGER: {
+			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
+			if (!af || af->getMode() != AfAlgorithm::AfModeAuto) {
+				LOG(IPARPI, Warning)
+					<< "Could not set AF_TRIGGER - no AF algorithm or not Auto";
+				break;
+			} else {
+				if (ctrl.second.get<int32_t>() == controls::AfTriggerStart)
+					af->triggerScan();
+				else
+					af->cancelScan();
+			}
+			break;
+		}
+
+		case controls::LENS_POSITION: {
+			AfAlgorithm *af = dynamic_cast<AfAlgorithm *>(controller_.getAlgorithm("af"));
+			if (af) {
+				int32_t hwpos;
+				if (af->setLensPosition(ctrl.second.get<float>(), &hwpos)) {
+					ControlList lensCtrls(lensCtrls_);
+					lensCtrls.set(V4L2_CID_FOCUS_ABSOLUTE, hwpos);
+					setLensControls.emit(lensCtrls);
+				}
+			} else {
+				LOG(IPARPI, Warning)
+					<< "Could not set LENS_POSITION - no AF algorithm";
+			}
+			break;
+		}
+
+		default:
+			LOG(IPARPI, Warning)
+				<< "Ctrl " << controls::controls.at(ctrl.first)->name()
+				<< " is not handled.";
+			break;
+		}
+	}
+}
+
+void IPARPi::returnEmbeddedBuffer(unsigned int bufferId)
+{
+	embeddedComplete.emit(bufferId);
+}
+
+void IPARPi::prepareISP(const ISPConfig &data)
+{
+	int64_t frameTimestamp = data.controls.get(controls::SensorTimestamp).value_or(0);
+	unsigned int ipaContext = data.ipaContext % rpiMetadata_.size();
+	RPiController::Metadata &rpiMetadata = rpiMetadata_[ipaContext];
+	Span<uint8_t> embeddedBuffer;
+
+	rpiMetadata.clear();
+	fillDeviceStatus(data.controls, ipaContext);
+
+	if (data.embeddedBufferPresent) {
+		/*
+		 * Pipeline handler has supplied us with an embedded data buffer,
+		 * we must pass it to the CamHelper for parsing.
+		 */
+		auto it = buffers_.find(data.embeddedBufferId);
+		ASSERT(it != buffers_.end());
+		embeddedBuffer = it->second.planes()[0];
+	}
+
+	/*
+	 * AGC wants to know the algorithm status from the time it actioned the
+	 * sensor exposure/gain changes. So fetch it from the metadata list
+	 * indexed by the IPA cookie returned, and put it in the current frame
+	 * metadata.
+	 */
+	AgcStatus agcStatus;
+	RPiController::Metadata &delayedMetadata = rpiMetadata_[data.delayContext];
+	if (!delayedMetadata.get<AgcStatus>("agc.status", agcStatus))
+		rpiMetadata.set("agc.delayed_status", agcStatus);
+
+	/*
+	 * This may overwrite the DeviceStatus using values from the sensor
+	 * metadata, and may also do additional custom processing.
+	 */
+	helper_->prepare(embeddedBuffer, rpiMetadata);
+
+	/* Done with embedded data now, return to pipeline handler asap. */
+	if (data.embeddedBufferPresent)
+		returnEmbeddedBuffer(data.embeddedBufferId);
+
+	/* Allow a 10% margin on the comparison below. */
+	Duration delta = (frameTimestamp - lastRunTimestamp_) * 1.0ns;
+	if (lastRunTimestamp_ && frameCount_ > dropFrameCount_ &&
+	    delta < controllerMinFrameDuration * 0.9) {
+		/*
+		 * Ensure we merge the previous frame's metadata with the current
+		 * frame. This will not overwrite exposure/gain values for the
+		 * current frame, or any other bits of metadata that were added
+		 * in helper_->Prepare().
+		 */
+		RPiController::Metadata &lastMetadata =
+			rpiMetadata_[(ipaContext ? ipaContext : rpiMetadata_.size()) - 1];
+		rpiMetadata.mergeCopy(lastMetadata);
+		processPending_ = false;
+		return;
+	}
+
+	lastRunTimestamp_ = frameTimestamp;
+	processPending_ = true;
+
+	ControlList ctrls(ispCtrls_);
+
+	controller_.prepare(&rpiMetadata);
+
+	/* Lock the metadata buffer to avoid constant locks/unlocks. */
+	std::unique_lock<RPiController::Metadata> lock(rpiMetadata);
+
+	AwbStatus *awbStatus = rpiMetadata.getLocked<AwbStatus>("awb.status");
+	if (awbStatus)
+		applyAWB(awbStatus, ctrls);
+
+	CcmStatus *ccmStatus = rpiMetadata.getLocked<CcmStatus>("ccm.status");
+	if (ccmStatus)
+		applyCCM(ccmStatus, ctrls);
+
+	AgcStatus *dgStatus = rpiMetadata.getLocked<AgcStatus>("agc.status");
+	if (dgStatus)
+		applyDG(dgStatus, ctrls);
+
+	AlscStatus *lsStatus = rpiMetadata.getLocked<AlscStatus>("alsc.status");
+	if (lsStatus)
+		applyLS(lsStatus, ctrls);
+
+	ContrastStatus *contrastStatus = rpiMetadata.getLocked<ContrastStatus>("contrast.status");
+	if (contrastStatus)
+		applyGamma(contrastStatus, ctrls);
+
+	BlackLevelStatus *blackLevelStatus = rpiMetadata.getLocked<BlackLevelStatus>("black_level.status");
+	if (blackLevelStatus)
+		applyBlackLevel(blackLevelStatus, ctrls);
+
+	GeqStatus *geqStatus = rpiMetadata.getLocked<GeqStatus>("geq.status");
+	if (geqStatus)
+		applyGEQ(geqStatus, ctrls);
+
+	DenoiseStatus *denoiseStatus = rpiMetadata.getLocked<DenoiseStatus>("denoise.status");
+	if (denoiseStatus)
+		applyDenoise(denoiseStatus, ctrls);
+
+	SharpenStatus *sharpenStatus = rpiMetadata.getLocked<SharpenStatus>("sharpen.status");
+	if (sharpenStatus)
+		applySharpen(sharpenStatus, ctrls);
+
+	DpcStatus *dpcStatus = rpiMetadata.getLocked<DpcStatus>("dpc.status");
+	if (dpcStatus)
+		applyDPC(dpcStatus, ctrls);
+
+	const AfStatus *afStatus = rpiMetadata.getLocked<AfStatus>("af.status");
+	if (afStatus) {
+		ControlList lensctrls(lensCtrls_);
+		applyAF(afStatus, lensctrls);
+		if (!lensctrls.empty())
+			setLensControls.emit(lensctrls);
+	}
+
+	if (!ctrls.empty())
+		setIspControls.emit(ctrls);
+}
+
+void IPARPi::fillDeviceStatus(const ControlList &sensorControls, unsigned int ipaContext)
+{
+	DeviceStatus deviceStatus = {};
+
+	int32_t exposureLines = sensorControls.get(V4L2_CID_EXPOSURE).get<int32_t>();
+	int32_t gainCode = sensorControls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>();
+	int32_t vblank = sensorControls.get(V4L2_CID_VBLANK).get<int32_t>();
+	int32_t hblank = sensorControls.get(V4L2_CID_HBLANK).get<int32_t>();
+
+	deviceStatus.lineLength = helper_->hblankToLineLength(hblank);
+	deviceStatus.shutterSpeed = helper_->exposure(exposureLines, deviceStatus.lineLength);
+	deviceStatus.analogueGain = helper_->gain(gainCode);
+	deviceStatus.frameLength = mode_.height + vblank;
+
+	RPiController::AfAlgorithm *af = dynamic_cast<RPiController::AfAlgorithm *>(
+			controller_.getAlgorithm("af"));
+	if (af)
+		deviceStatus.lensPosition = af->getLensPosition();
+
+	LOG(IPARPI, Debug) << "Metadata - " << deviceStatus;
+
+	rpiMetadata_[ipaContext].set("device.status", deviceStatus);
+}
+
+RPiController::StatisticsPtr IPARPi::fillStatistics(bcm2835_isp_stats *stats) const
+{
+	using namespace RPiController;
+
+	const Controller::HardwareConfig &hw = controller_.getHardwareConfig();
+	unsigned int i;
+	StatisticsPtr statistics =
+		std::make_unique<Statistics>(Statistics::AgcStatsPos::PreWb, Statistics::ColourStatsPos::PostLsc);
+
+	/* RGB histograms are not used, so do not populate them. */
+	statistics->yHist = RPiController::Histogram(stats->hist[0].g_hist,
+						     hw.numHistogramBins);
+
+	/* All region sums are based on a 16-bit normalised pipeline bit-depth. */
+	unsigned int scale = Statistics::NormalisationFactorPow2 - hw.pipelineWidth;
+
+	statistics->awbRegions.init(hw.awbRegions);
+	for (i = 0; i < statistics->awbRegions.numRegions(); i++)
+		statistics->awbRegions.set(i, { { stats->awb_stats[i].r_sum << scale,
+						  stats->awb_stats[i].g_sum << scale,
+						  stats->awb_stats[i].b_sum << scale },
+						stats->awb_stats[i].counted,
+						stats->awb_stats[i].notcounted });
+
+	statistics->agcRegions.init(hw.agcRegions);
+	for (i = 0; i < statistics->agcRegions.numRegions(); i++)
+		statistics->agcRegions.set(i, { { stats->agc_stats[i].r_sum << scale,
+						  stats->agc_stats[i].g_sum << scale,
+						  stats->agc_stats[i].b_sum << scale },
+						stats->agc_stats[i].counted,
+						stats->awb_stats[i].notcounted });
+
+	statistics->focusRegions.init(hw.focusRegions);
+	for (i = 0; i < statistics->focusRegions.numRegions(); i++)
+		statistics->focusRegions.set(i, { stats->focus_stats[i].contrast_val[1][1] / 1000,
+						  stats->focus_stats[i].contrast_val_num[1][1],
+						  stats->focus_stats[i].contrast_val_num[1][0] });
+	return statistics;
+}
+
+void IPARPi::processStats(unsigned int bufferId, unsigned int ipaContext)
+{
+	RPiController::Metadata &rpiMetadata = rpiMetadata_[ipaContext];
+
+	auto it = buffers_.find(bufferId);
+	if (it == buffers_.end()) {
+		LOG(IPARPI, Error) << "Could not find stats buffer!";
+		return;
+	}
+
+	Span<uint8_t> mem = it->second.planes()[0];
+	bcm2835_isp_stats *stats = reinterpret_cast<bcm2835_isp_stats *>(mem.data());
+	RPiController::StatisticsPtr statistics = fillStatistics(stats);
+
+	/* Save the focus stats in the metadata structure to report out later. */
+	rpiMetadata_[ipaContext].set("focus.status", statistics->focusRegions);
+
+	helper_->process(statistics, rpiMetadata);
+	controller_.process(statistics, &rpiMetadata);
+
+	struct AgcStatus agcStatus;
+	if (rpiMetadata.get("agc.status", agcStatus) == 0) {
+		ControlList ctrls(sensorCtrls_);
+		applyAGC(&agcStatus, ctrls);
+
+		setDelayedControls.emit(ctrls, ipaContext);
+		setCameraTimeoutValue();
+	}
+}
+
+void IPARPi::setCameraTimeoutValue()
+{
+	/*
+	 * Take the maximum value of the exposure queue as the camera timeout
+	 * value to pass back to the pipeline handler. Only signal if it has changed
+	 * from the last set value.
+	 */
+	auto max = std::max_element(frameLengths_.begin(), frameLengths_.end());
+
+	if (*max != lastTimeout_) {
+		setCameraTimeout.emit(max->get<std::milli>());
+		lastTimeout_ = *max;
+	}
+}
+
+void IPARPi::applyAWB(const struct AwbStatus *awbStatus, ControlList &ctrls)
+{
+	LOG(IPARPI, Debug) << "Applying WB R: " << awbStatus->gainR << " B: "
+			   << awbStatus->gainB;
+
+	ctrls.set(V4L2_CID_RED_BALANCE,
+		  static_cast<int32_t>(awbStatus->gainR * 1000));
+	ctrls.set(V4L2_CID_BLUE_BALANCE,
+		  static_cast<int32_t>(awbStatus->gainB * 1000));
+}
+
+void IPARPi::applyFrameDurations(Duration minFrameDuration, Duration maxFrameDuration)
+{
+	/*
+	 * This will only be applied once AGC recalculations occur.
+	 * The values may be clamped based on the sensor mode capabilities as well.
+	 */
+	minFrameDuration_ = minFrameDuration ? minFrameDuration : defaultMinFrameDuration;
+	maxFrameDuration_ = maxFrameDuration ? maxFrameDuration : defaultMaxFrameDuration;
+	minFrameDuration_ = std::clamp(minFrameDuration_,
+				       mode_.minFrameDuration, mode_.maxFrameDuration);
+	maxFrameDuration_ = std::clamp(maxFrameDuration_,
+				       mode_.minFrameDuration, mode_.maxFrameDuration);
+	maxFrameDuration_ = std::max(maxFrameDuration_, minFrameDuration_);
+
+	/* Return the validated limits via metadata. */
+	libcameraMetadata_.set(controls::FrameDurationLimits,
+			       { static_cast<int64_t>(minFrameDuration_.get<std::micro>()),
+				 static_cast<int64_t>(maxFrameDuration_.get<std::micro>()) });
+
+	/*
+	 * Calculate the maximum exposure time possible for the AGC to use.
+	 * getBlanking() will update maxShutter with the largest exposure
+	 * value possible.
+	 */
+	Duration maxShutter = Duration::max();
+	helper_->getBlanking(maxShutter, minFrameDuration_, maxFrameDuration_);
+
+	RPiController::AgcAlgorithm *agc = dynamic_cast<RPiController::AgcAlgorithm *>(
+		controller_.getAlgorithm("agc"));
+	agc->setMaxShutter(maxShutter);
+}
+
+void IPARPi::applyAGC(const struct AgcStatus *agcStatus, ControlList &ctrls)
+{
+	const int32_t minGainCode = helper_->gainCode(mode_.minAnalogueGain);
+	const int32_t maxGainCode = helper_->gainCode(mode_.maxAnalogueGain);
+	int32_t gainCode = helper_->gainCode(agcStatus->analogueGain);
+
+	/*
+	 * Ensure anything larger than the max gain code will not be passed to
+	 * DelayedControls. The AGC will correctly handle a lower gain returned
+	 * by the sensor, provided it knows the actual gain used.
+	 */
+	gainCode = std::clamp<int32_t>(gainCode, minGainCode, maxGainCode);
+
+	/* getBlanking might clip exposure time to the fps limits. */
+	Duration exposure = agcStatus->shutterTime;
+	auto [vblank, hblank] = helper_->getBlanking(exposure, minFrameDuration_, maxFrameDuration_);
+	int32_t exposureLines = helper_->exposureLines(exposure,
+						       helper_->hblankToLineLength(hblank));
+
+	LOG(IPARPI, Debug) << "Applying AGC Exposure: " << exposure
+			   << " (Shutter lines: " << exposureLines << ", AGC requested "
+			   << agcStatus->shutterTime << ") Gain: "
+			   << agcStatus->analogueGain << " (Gain Code: "
+			   << gainCode << ")";
+
+	ctrls.set(V4L2_CID_VBLANK, static_cast<int32_t>(vblank));
+	ctrls.set(V4L2_CID_EXPOSURE, exposureLines);
+	ctrls.set(V4L2_CID_ANALOGUE_GAIN, gainCode);
+
+	/*
+	 * At present, there is no way of knowing if a control is read-only.
+	 * As a workaround, assume that if the minimum and maximum values of
+	 * the V4L2_CID_HBLANK control are the same, it implies the control
+	 * is read-only. This seems to be the case for all the cameras our IPA
+	 * works with.
+	 *
+	 * \todo The control API ought to have a flag to specify if a control
+	 * is read-only which could be used below.
+	 */
+	if (mode_.minLineLength != mode_.maxLineLength)
+		ctrls.set(V4L2_CID_HBLANK, static_cast<int32_t>(hblank));
+
+	/*
+	 * Store the frame length times in a circular queue, up-to FrameLengthsQueueSize
+	 * elements. This will be used to advertise a camera timeout value to the
+	 * pipeline handler.
+	 */
+	frameLengths_.pop_front();
+	frameLengths_.push_back(helper_->exposure(vblank + mode_.height,
+						  helper_->hblankToLineLength(hblank)));
+}
+
+void IPARPi::applyDG(const struct AgcStatus *dgStatus, ControlList &ctrls)
+{
+	ctrls.set(V4L2_CID_DIGITAL_GAIN,
+		  static_cast<int32_t>(dgStatus->digitalGain * 1000));
+}
+
+void IPARPi::applyCCM(const struct CcmStatus *ccmStatus, ControlList &ctrls)
+{
+	bcm2835_isp_custom_ccm ccm;
+
+	for (int i = 0; i < 9; i++) {
+		ccm.ccm.ccm[i / 3][i % 3].den = 1000;
+		ccm.ccm.ccm[i / 3][i % 3].num = 1000 * ccmStatus->matrix[i];
+	}
+
+	ccm.enabled = 1;
+	ccm.ccm.offsets[0] = ccm.ccm.offsets[1] = ccm.ccm.offsets[2] = 0;
+
+	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&ccm),
+					    sizeof(ccm) });
+	ctrls.set(V4L2_CID_USER_BCM2835_ISP_CC_MATRIX, c);
+}
+
+void IPARPi::applyGamma(const struct ContrastStatus *contrastStatus, ControlList &ctrls)
+{
+	const unsigned int numGammaPoints = controller_.getHardwareConfig().numGammaPoints;
+	struct bcm2835_isp_gamma gamma;
+
+	for (unsigned int i = 0; i < numGammaPoints - 1; i++) {
+		int x = i < 16 ? i * 1024
+			       : (i < 24 ? (i - 16) * 2048 + 16384
+					 : (i - 24) * 4096 + 32768);
+		gamma.x[i] = x;
+		gamma.y[i] = std::min<uint16_t>(65535, contrastStatus->gammaCurve.eval(x));
+	}
+
+	gamma.x[numGammaPoints - 1] = 65535;
+	gamma.y[numGammaPoints - 1] = 65535;
+	gamma.enabled = 1;
+
+	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&gamma),
+					    sizeof(gamma) });
+	ctrls.set(V4L2_CID_USER_BCM2835_ISP_GAMMA, c);
+}
+
+void IPARPi::applyBlackLevel(const struct BlackLevelStatus *blackLevelStatus, ControlList &ctrls)
+{
+	bcm2835_isp_black_level blackLevel;
+
+	blackLevel.enabled = 1;
+	blackLevel.black_level_r = blackLevelStatus->blackLevelR;
+	blackLevel.black_level_g = blackLevelStatus->blackLevelG;
+	blackLevel.black_level_b = blackLevelStatus->blackLevelB;
+
+	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&blackLevel),
+					    sizeof(blackLevel) });
+	ctrls.set(V4L2_CID_USER_BCM2835_ISP_BLACK_LEVEL, c);
+}
+
+void IPARPi::applyGEQ(const struct GeqStatus *geqStatus, ControlList &ctrls)
+{
+	bcm2835_isp_geq geq;
+
+	geq.enabled = 1;
+	geq.offset = geqStatus->offset;
+	geq.slope.den = 1000;
+	geq.slope.num = 1000 * geqStatus->slope;
+
+	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&geq),
+					    sizeof(geq) });
+	ctrls.set(V4L2_CID_USER_BCM2835_ISP_GEQ, c);
+}
+
+void IPARPi::applyDenoise(const struct DenoiseStatus *denoiseStatus, ControlList &ctrls)
+{
+	using RPiController::DenoiseMode;
+
+	bcm2835_isp_denoise denoise;
+	DenoiseMode mode = static_cast<DenoiseMode>(denoiseStatus->mode);
+
+	denoise.enabled = mode != DenoiseMode::Off;
+	denoise.constant = denoiseStatus->noiseConstant;
+	denoise.slope.num = 1000 * denoiseStatus->noiseSlope;
+	denoise.slope.den = 1000;
+	denoise.strength.num = 1000 * denoiseStatus->strength;
+	denoise.strength.den = 1000;
+
+	/* Set the CDN mode to match the SDN operating mode. */
+	bcm2835_isp_cdn cdn;
+	switch (mode) {
+	case DenoiseMode::ColourFast:
+		cdn.enabled = 1;
+		cdn.mode = CDN_MODE_FAST;
+		break;
+	case DenoiseMode::ColourHighQuality:
+		cdn.enabled = 1;
+		cdn.mode = CDN_MODE_HIGH_QUALITY;
+		break;
+	default:
+		cdn.enabled = 0;
+	}
+
+	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&denoise),
+					    sizeof(denoise) });
+	ctrls.set(V4L2_CID_USER_BCM2835_ISP_DENOISE, c);
+
+	c = ControlValue(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&cdn),
+					      sizeof(cdn) });
+	ctrls.set(V4L2_CID_USER_BCM2835_ISP_CDN, c);
+}
+
+void IPARPi::applySharpen(const struct SharpenStatus *sharpenStatus, ControlList &ctrls)
+{
+	bcm2835_isp_sharpen sharpen;
+
+	sharpen.enabled = 1;
+	sharpen.threshold.num = 1000 * sharpenStatus->threshold;
+	sharpen.threshold.den = 1000;
+	sharpen.strength.num = 1000 * sharpenStatus->strength;
+	sharpen.strength.den = 1000;
+	sharpen.limit.num = 1000 * sharpenStatus->limit;
+	sharpen.limit.den = 1000;
+
+	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&sharpen),
+					    sizeof(sharpen) });
+	ctrls.set(V4L2_CID_USER_BCM2835_ISP_SHARPEN, c);
+}
+
+void IPARPi::applyDPC(const struct DpcStatus *dpcStatus, ControlList &ctrls)
+{
+	bcm2835_isp_dpc dpc;
+
+	dpc.enabled = 1;
+	dpc.strength = dpcStatus->strength;
+
+	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&dpc),
+					    sizeof(dpc) });
+	ctrls.set(V4L2_CID_USER_BCM2835_ISP_DPC, c);
+}
+
+void IPARPi::applyLS(const struct AlscStatus *lsStatus, ControlList &ctrls)
+{
+	/*
+	 * Program lens shading tables into pipeline.
+	 * Choose smallest cell size that won't exceed 63x48 cells.
+	 */
+	const int cellSizes[] = { 16, 32, 64, 128, 256 };
+	unsigned int numCells = std::size(cellSizes);
+	unsigned int i, w, h, cellSize;
+	for (i = 0; i < numCells; i++) {
+		cellSize = cellSizes[i];
+		w = (mode_.width + cellSize - 1) / cellSize;
+		h = (mode_.height + cellSize - 1) / cellSize;
+		if (w < 64 && h <= 48)
+			break;
+	}
+
+	if (i == numCells) {
+		LOG(IPARPI, Error) << "Cannot find cell size";
+		return;
+	}
+
+	/* We're going to supply corner sampled tables, 16 bit samples. */
+	w++, h++;
+	bcm2835_isp_lens_shading ls = {
+		.enabled = 1,
+		.grid_cell_size = cellSize,
+		.grid_width = w,
+		.grid_stride = w,
+		.grid_height = h,
+		/* .dmabuf will be filled in by pipeline handler. */
+		.dmabuf = 0,
+		.ref_transform = 0,
+		.corner_sampled = 1,
+		.gain_format = GAIN_FORMAT_U4P10
+	};
+
+	if (!lsTable_ || w * h * 4 * sizeof(uint16_t) > MaxLsGridSize) {
+		LOG(IPARPI, Error) << "Do not have a correctly allocate lens shading table!";
+		return;
+	}
+
+	if (lsStatus) {
+		/* Format will be u4.10 */
+		uint16_t *grid = static_cast<uint16_t *>(lsTable_);
+
+		resampleTable(grid, lsStatus->r, w, h);
+		resampleTable(grid + w * h, lsStatus->g, w, h);
+		std::memcpy(grid + 2 * w * h, grid + w * h, w * h * sizeof(uint16_t));
+		resampleTable(grid + 3 * w * h, lsStatus->b, w, h);
+	}
+
+	ControlValue c(Span<const uint8_t>{ reinterpret_cast<uint8_t *>(&ls),
+					    sizeof(ls) });
+	ctrls.set(V4L2_CID_USER_BCM2835_ISP_LENS_SHADING, c);
+}
+
+void IPARPi::applyAF(const struct AfStatus *afStatus, ControlList &lensCtrls)
+{
+	if (afStatus->lensSetting) {
+		ControlValue v(afStatus->lensSetting.value());
+		lensCtrls.set(V4L2_CID_FOCUS_ABSOLUTE, v);
+	}
+}
+
+/*
+ * Resamples a 16x12 table with central sampling to destW x destH with corner
+ * sampling.
+ */
+void IPARPi::resampleTable(uint16_t dest[], const std::vector<double> &src,
+			   int destW, int destH)
+{
+	/*
+	 * Precalculate and cache the x sampling locations and phases to
+	 * save recomputing them on every row.
+	 */
+	assert(destW > 1 && destH > 1 && destW <= 64);
+	int xLo[64], xHi[64];
+	double xf[64];
+	double x = -0.5, xInc = 16.0 / (destW - 1);
+	for (int i = 0; i < destW; i++, x += xInc) {
+		xLo[i] = floor(x);
+		xf[i] = x - xLo[i];
+		xHi[i] = xLo[i] < 15 ? xLo[i] + 1 : 15;
+		xLo[i] = xLo[i] > 0 ? xLo[i] : 0;
+	}
+
+	/* Now march over the output table generating the new values. */
+	double y = -0.5, yInc = 12.0 / (destH - 1);
+	for (int j = 0; j < destH; j++, y += yInc) {
+		int yLo = floor(y);
+		double yf = y - yLo;
+		int yHi = yLo < 11 ? yLo + 1 : 11;
+		yLo = yLo > 0 ? yLo : 0;
+		double const *rowAbove = src.data() + yLo * 16;
+		double const *rowBelow = src.data() + yHi * 16;
+		for (int i = 0; i < destW; i++) {
+			double above = rowAbove[xLo[i]] * (1 - xf[i]) + rowAbove[xHi[i]] * xf[i];
+			double below = rowBelow[xLo[i]] * (1 - xf[i]) + rowBelow[xHi[i]] * xf[i];
+			int result = floor(1024 * (above * (1 - yf) + below * yf) + .5);
+			*(dest++) = result > 16383 ? 16383 : result; /* want u4.10 */
+		}
+	}
+}
+
+} /* namespace ipa::RPi */
+
+/*
+ * External IPA module interface
+ */
+extern "C" {
+const struct IPAModuleInfo ipaModuleInfo = {
+	IPA_MODULE_API_VERSION,
+	1,
+	"PipelineHandlerRPi",
+	"rpi/vc4",
+};
+
+IPAInterface *ipaCreate()
+{
+	return new ipa::RPi::IPARPi();
+}
+
+} /* extern "C" */
+
+} /* namespace libcamera */
-- 
cgit v1.2.1