Merge remote-tracking branch 'upstream/master'

* upstream/master:
  utils: libtuning: Correct GBRG Image parsing
  apps: common: dng_writer: Support RAW10 and RAW12 format
  apps: common: dng_writer: Add thumbnail scanline function for Raw
  apps: common: dng_writer: Rename packing functions
  apps: common: dng_writer: Support RAW16 formats
  libcamera: Fix maybe-uninitialized error
  apps: common: dng_writer: Workaround for "Unknown tag 33421" error
  meson: Enable warnings to flag missing declarations
  test: gstreamer: Include missing sanitizer/asan_interface.h header
  test: ipc: unixsocket: Define local function in anonymous namespace
  apps: Define local functions in anonymous namespace
  v4l2: v4l2_compat: Selectively disable -Wmissing-declarations
  v4l2: v4l2_compat: Include missing headers
  py: libcamera: Move function declarations to common header
  ipa: rpi: Define local functions in anonymous namespace
  ipa: ipa_interface: Declare ipaCreate() in libcamera namespace
  libcamera: base: log: Declare log categories when defining them
  v4l2: v4l2_compat: Move `open*()` flag check into function
  v4l2: v4l2_compat: Fix redirect from `__open(at)64_2()`
  ipa: libipa: matrix: Fix incorrect symbol namespace
  libcamera: yaml_parser: Add support for float types
  ipa: rkisp1: agc: Rename frame context update variable to updateMetering
  libcamera: debayer_cpu: Add 32bits/aligned output formats
  ipa: libipa: pwl: Drop readYaml() function
  ipa: rpi: controller: Replace Pwl::readYaml() with YamlObject::get()
  ipa: rkisp1: agc: Move AeEnable control to the AGC algorithm
  ipa: rkisp1: agc: Correctly clamp maximum shutter speed
  ipa: rkisp1: agc: Rename maxShutterSpeed to maxFrameDuration
  ipa: rkisp1: agc: Use mode from frame context to calculate new EV
  ipa: rkisp1: agc: Simplify predivider calculation
  ipa: rkisp1: agc: Don't update histogram parameters unnecessarily
  ipa: rkisp1: agc: Make size argument to computeHistogramPredivider const
  ipa: rkisp1: Document all AGC parameters in IPAFrameContext
  ipa: rkisp1: Document all AGC parameters in IPAActiveState
  ipa: libipa: agc_mean_luminance: Include missing header
  ipa: libipa: agc_mean_luminance: Fix enumerator names
  ipa: rkisp1: agc: Fix initialization without metering modes
  ipa: libipa: vector: Add matrix-vector multiplication
  ipa: rkisp1: algorithms: Add crosstalk algorithm
  ipa: libipa: Add MatrixInterpolator class
  ipa: libipa: Add Matrix class
  ipa: libipa: pwl: Specialize YamlObject getter
  ipa: libipa: pwl: Add a constructor that moves a Point vector
  ipa: libipa: pwl: Add a size() function
  ipa: libipa: pwl: Make the empty() function inline
  ipa: libipa: pwl: Suffix \param with direction
  ipa: libipa: vector: Drop readYaml() function
  ipa: libipa: vector: Specialize YamlObject getter
  libcamera: yaml_parser: Delegate YamlObject::get() to helper structure
  libcamera: yaml_parser: Drop std::enable_if_t guards for get() function
  utils: tuning: rkisp1: Add skeletal AGC to the rkisp1 tuning script
  utils: libtuning: modules: Add skeletal AGC module
  ipa: rkisp1: agc: Plumb mode-selection and frame duration controls
  ipa: rkisp1: agc: Read histogram weights from tuning file
  ipa: rkisp1: cproc: Drop incorrect [[maybe_unused]]
  ipa: rkisp1: cproc: Merge identical functions
  ipa: rkisp1: cproc: Use anonymous namespace to limit symbol visibility
  utils: raspberrypi: ctt: Add a maximum gain parameter for LSC
  utils: raspberrypi: ctt: Add option to convert between vc4/pisp targets
  utils: raspberrypi: ctt: Update tuning tool for HDR
  utils: raspberrypi: ctt: Changed CTT handling of VC4 and PiSP
  utils: raspberrypi: ctt: Added CAC support to the CTT
  utils: raspberrypi: ctt: Adapt tuning tool for both VC4 and PiSP
  pipeline: rpi: Don't validate configuration in generateConfiguration()
  libcamera: yaml_parser: Make default value templated in `get()`
  libcamera: libipa: camera_sensor: Define AR0521 helper functions inline
  libcamera: controls: Add missing period at end of Gamma description
  ipa: rkisp1: goc: Avoid use of auto for short type
  ipa: rkisp1: goc: Mark default gamma table as static constexpr
  ipa: rkisp1: goc: Use copydoc
  ipa: rkisp1: goc: Drop incorrect [[maybe_unused]]
  ipa: rkisp1: goc: Fix typographical issues in documentation.
  ipa: rpi: controller: Use libipa's Pwl class
  ipa: libipa: Copy pwl from rpi, and clean it up to match libcamera
  ipa: libipa: Add Vector class
  pipeline: rkisp1: cproc: Fix default value handling
  meson: Group libipa and libipa_includes in a dependency object
  libcamera: software_isp: Fix includes ordering in simple.cpp
  clang-format: Make Qt includes matching case sensitive
  ipa: rkisp1: Add GammaOutCorrection algorithm
  ipa: rkisp1: Fix algorithm controls vanish after configure
  libcamera: Add gamma control id
  test: ipa: rkisp1: utils: Fix floating and fixed point conversion test
  libcamera: dma_buf_allocator: Work around lack of file seals in uClibc
  libcamera: dma_buf_allocator: Work around lack of memfd_create() in uClibc
  libcamera: dma_buf_allocator: Create memfd with CLOEXEC
  ipa: rpi: Disable StatsOutputEnable control by default
  libcamera: software_isp: Allow using dma-buffers from /dev/udmabuf
  libcamera: DmaBufAllocator: Support allocating from /dev/udmabuf
  libcamera: Rename DmaHeap class to DmaBufAllocator
  libcamera: v4l2_subdevice: Update to the new kernel routing API
  include: linux: Update kernel headers to version v6.10-rc1
  update-kernel-headers: Add linux/udmabuf.h to headers to sync
  test: ipa: rkisp1-utils: Fix coding style for template arguments
  pipeline: vimc: Don't hardcode scaling factor with recent kernels
  test: v4l2_videodevice: Increase timeout for vimc capture tests
  test: fence: Increase timeout for fence test
  test: fence: Fix race condition
  test: fence: Turn class member variable into local variable
  test: camera: Increase timeout for vimc capture tests
  libcamera: software_isp: Remove TODO about internal representation
  libcamera: software_isp: Remove DebayerParams::kGain10
  libcamera: software_isp: Move color mappings out of debayering
  libcamera: software_isp: Honor black level in AWB
  libcamera: software_isp: Use a specific integer type for black level
  utils: checkstyle.py: Show location of coding style issue within line
  utils: checkstyle.py: Add a check for hex values
  utils: checkstyle.py: Extend IncludeChecker to cover math.h
  utils: checkstyle.py: Refactor IncludeChecker
  test: ipa: rkisp1-utils: Fix capitalization of hex numbers
  ipa: rkisp1: Add a helper to convert floating-point to fixed-point
  android: camera_capabilities: Fix GCC 14 warning
  ipa: rpi: Make monoSensor() accessor const
  libcamera: yaml_parser: Avoid double lookup in `operator[]`
  treewide: Query list of cameras just once
  libcamera v0.3.0
  ipa: rpi: Add accessor function for monoSensor_
  ipa: rpi: Add HDR support
  libcamera: v4l2_subdevice: Add missing MEDIA_BUS_FMT_xxxx16_1X16 info
  libcamera: formats: Add PiSP specific image and config buffer formats
  include: linux: Add PiSP specific image and config buffer formats
  libcamera: formats: Add RGB48 formats
  include: linux: Add RGB48 formats
  libcamera: camera_manager: Add environment variable to order pipelines match
  libcamera: pipeline: Add a get factory by name helper
  libcamera: pipeline: Rename pipelines to a shorter name
  gstreamer: Use copied camera name
  gstreamer: Fix string memory leak
  Documentation: Add option to treat Doxygen warnings as errors
  test: gstreamer: Simplify single stream test
  ipa: libipa: histogram: Fix documentation of constructor
  libcamera: Drop remaining file name from header comment blocks
  libcamera: Drop file name from header comment blocks in templates
  ipa: rkisp1: agc: Fix histogram construction
  ipa: rpi: Fix for incorrectly reported max shutter speed
  pipeline: rpi: Avoid duplicating size range for the same pixel format
  ipa: libipa: histogram: Add transform parameter to constructor
  libcamera: Drop file name from header comment blocks
  ipa: rkisp1: Remove bespoke Agc functions
  ipa: rkisp1: Derive rkisp1::algorithms::Agc from AgcMeanLuminance
  ipa: ipu3: Remove bespoke AGC functions from IPU3
  ipa: ipu3: Derive ipu3::algorithms::Agc from AgcMeanLuminance
  ipa: libipa: Add AgcMeanLuminance base class
  ipa: libipa: Add ExposureModeHelper
  libcamera: controls: Generate enum value-name maps
  ipa: libipa: Allow creation of empty Histogram
  Revert "meson: Deprecate bitwise operations between different enumeration type"
  test: Don't add current build directory to include path
  meson: Deprecate bitwise operations between different enumeration type
  apps: qcam: Use standard key sequence for quit action
  libcamera: shared_mem_object: Fix compilation with uClibc
  libcamera: utils: Avoid infinite recursion with strtod()
  libcamera: libipa: camera_sensor: Add Sony IMX415 sensor properties
  apps: cam: Fix C++20 deprecation warning
  libcamera: libipa: camera_sensor: Add Sony IMX335 sensor properties
  libcamera: libipa: camera_sensor: Add Sony IMX283 sensor properties
  android: camera_device: Always clear descriptors_ in stop()
  libcamera: Fix output spelling error
  libcamera: device_enumerator: Fix spell error
  utils: checkstyle.py: Drop period at end of Doxygen one liners
  utils: checkstyle.py: Use r'' strings for regular expressions
  Documentation: coding-style: Clean up header guard style
  ipu3: Use posix basename
  libcamera: software_isp: Apply black level compensation
  libcamera: Add "Software ISP benchmarking" documentation
  libcamera: debayer_cpu: Add BGR888 output support
  libcamera: debayer_cpu: Add support for 8, 10 and 12 bpp unpacked bayer input
  libcamera: swstats_cpu: Add support for 8, 10 and 12 bpp unpacked bayer input
  libcamera: pipeline: simple: Enable use of Soft ISP and Soft IPA
  libcamera: pipeline: simple: Rename converterBuffers_ and related vars
  libcamera: Introduce SoftwareIsp
  libcamera: ipa: Add Soft IPA
  libcamera: software_isp: Add DebayerCpu class
  libcamera: software_isp: Add Debayer base class
  libcamera: software_isp: Add SwStatsCpu class
  libcamera: shared_mem_object: Reorganize the code and document the SharedMemObject class
  libcamera: shared_mem_object: Rename SIZE constant to `size'
  libcamera: internal: Move SharedMemObject class to a common directory
  libcamera: dma_heaps: extend DmaHeap class to support system heap
  libcamera: internal: Move dma_heaps.[h, cpp] to common directories
  libcamera: pipeline: simple: fix size adjustment in validate()
  utils: ipc: Fix async main interface functions with no parameters
  utils: ipc: Fix event functions with no parameters
  apps: cam: Add support for PPM output format
  v4l2: meson: Keep defines sorted alphabetically
  v4l2: Unset _TIME_BITS in addition to _FILE_OFFSET_BITS
  libcamera: pipeline: Add Mali-C55 ISP pipeline
  android: camera_device: Save capture settings unconditionally
  apps: lc-compliance: Re-organize source directory
  apps: lc-compliance: Fix typo in comment
  apps: lc-compliance: Check that requests complete successfully
  utils: tuning: readme: Improve names of dependency packages
  checkstyle: Work around bug in difflib
  libcamera: camera_sensor: Expose the Bayer order
  libcamera: camera_sensor: Test for read-only HBLANK with READ_ONLY flag
  libcamera: camera_sensor: Reorder functions
  libcamera: camera_sensor: Drop updateControlInfo() function
  libcamera: camera_sensor: Move related classes to subdirectory
  libcamera: v4l2_subdevice: Add V4L2Subdevice::Route structure
  libcamera: v4l2_subdevice: Replace Routing::toString() with operator<<()
  libcamera: v4l2_subdevice: Add stream support to get/set functions
  libcamera: v4l2_subdevice: Rename V4L2SubdeviceFormat::mbus_code to code
  libcamera: v4l2_subdevice: Drop V4L2SubdeviceFormat::bitsPerPixel()
  libcamera: v4l2_subdevice: Extend MediaBusFormatInfo with metadata formats
  libcamera: v4l2_subdevice: Expose media bus format info as internal API
  libcamera: v4l2_subdevice: Add code member to MediaBusFormatInfo
  libcamera: v4l2_subdevice: Rename V4L2SubdeviceFormatInfo
  libcamera: controls: Add policy parameter to ControlList::merge()
  libcamera: framebuffer_allocator: Remove entry if allocation fails
  libcamera: framebuffer_allocator: Use default destructor
  libcamera: framebuffer_allocator: Move from argument in constructor
  cam: capture_script: Make parseRectangles work for non-array
  utils: checkstyle.py: Fix known_trailers sort order
  utils: checkstyle.py: Add Co-developed-by to known_trailers
  ipa: rpi: cam_helper: Extend embedded data parsing to RAW14 modes
  libcamera: v4l2_subdevice: Add 14-bit Bayer formats
  guides: pipeline-handler: Fix controlInfo_ initialization
  libcamera: controls: Fix typo on thermal
  pipeline: simple: Add support for MediaTek MT8365 platforms
  meson_options: Autodetect pycamera support
  README.rst: Report py dependencies
  utils: ipu3: Fix return value check on file output
  libcamera: formats: Fix planes bounds check
  utils: rkisp1: gen-csc-table.py: Don't presume python3 location
  utils: checkstyle.py: Update LogCategoryChecker regexp
  utils: checkstyle.py: Don't presume python3 location
  libcamera: formats: Fix typos in PixelFormatInfo documentation
  libcamera: Use utils::to_underlying()
  libcamera: utils: Add to_underlying() helper function
  ipa: rkisp1: Support the i.MX8MP ISP version
  include: linux: Add RKISP1_V_IMX8MP version
  ipa: rkisp1: Fix histogram weights configuration
  ipa: rkisp1: Store hardware parameters in IPA context
  ipa: rkisp1: agc: Wrap variable length C arrays in spans
  pipeline: rpi: vc4: Use an unpacked format if no packed one is available
  utils: mojom: Fix build error caused by the mojom tool update
  gstreamer: Map 10/12/14/16 bayer formats supports
  gstreamer: Map R8/R16 pixel format
  doc: gstreamer: Add missing queues
  libcamera: object: Add and use thread-bound assertion
  libcamera: object: Document and ensure Object deletion constraints
  test: timer-thread: Destroy Object from correct thread context
  test: timer-thread: Move timer start from wrong thread to separate test
  test: signal-threads: Destroy Object from correct thread context
  test: message: Destroy Object from correct thread context
  test: message: Remove incorrect slow receiver test
  test: event-thread: Destroy Object from correct thread context
  libcamera: thread: Ensure deferred deletion of all objects before stopping
  test: object-delete: Test deferred delete just before thread stops
  libcamera: signal: Replace object.h inclusion with forward declatation
  libcamera: object: Fix thread-bound reference in documentation
  apps: common: dng_writer: Add a default case for switch-case on a modulo
  libcamera: v4l2_subdevice: Explain sort order of formatInfoMap
  libcamera: bayer_format: Add Y12 to bayerToFormat
  libcamera: formats: Fix sort ordering of R10_CSI2P
  libcamera: formats: Add 16-bit mono format
  libcamera: Remove empty formats.c
  pipeline: rpi: Always initialize the embedded buffer in tryRunPipeline
  test: gstreamer: Use env instead of registry edit
  gstreamer: Add meson devenv support
  utils: ipc: extract-docs: Fix escape characters in regex
  Documentation: Update Qt documentation links to Qt 6
  libcamera: base: signal: Update Qt documentation link to Qt 6
  libcamera: v4l2_subdevice: Enable streams API when supported
  include: linux: Update kernel headers to version v6.7
  libipa: camera_sensor_helper: Mark createInstance() with override
  libcamera v0.2.0
  utils: ipc: Update mojo
  utils: ipc: generate.py: Disable attributes checker
  utils: ipc: generate.py: Add bindings directory to Python path
  utils: update-mojo.sh: Commit the mojo update
  utils: update-mojo.sh: Reject a dirty libcamera tree
  utils: update-mojo.sh: Change to the libcamera source directory
  utils: update-mojo.sh: Exit immediately on error
  utils: update-mojo.sh: Properly remove old sources
  libcamera: geometry: Correct doxygen reference to classes
  hooks: pre-push: Disable interpretation of escape sequences
  ipa: rpi: vc4: data: Update tuning files for HDR
  ipa: rpi: Implement HDR control
  libcamera: framebuffer_allocator: Remove unnecessary `clear()`
  utils: ipc: mojom_libcamera_generator.py: Fix Python warning
  libcamera: device_enumerator: ensure deviceNode is not empty
  utils: ipc: Fix deserialization of multiple fd parameters
  apps: lc-compliance: Fix source file ordering in meson.build
  ipa: rpi: vc4: Add OV64A40 tuning files
  libipa: camera_sensor_helper: Add OV64A40 helper
  ipa: rpi: Provide a Camera Helper for the OV64A40
  libcamera: camera_sensor: Add OV64A40 sensor properties
  ipa: vc4: Implement the StatsOutputEnable vendor control
  build: controls: Add Raspberry Pi vendor specific controls
  utils: raspberrypi: ctt: Improve the Macbeth Chart search reliability
  ipa: rpi: awb: Add an initialValues method
  ipa: rpi: black_level: Add an initialValues method
  ipa: rpi: Allow the mean of an empty histogram interval
  Documentation: camera-sensor-model: Support Sphinx < 2.0
  gstreamer: Support for pre-1.16 GstEvent
  lc-compliance: Set minimum version for gtest dependency
  ipa: rpi: Add hardware line rate constraints
  pipeline: rpi: Respect provided stride
  libcamera: camera: Fix unused variable compiler warning
  meson: Replace hack with usage of '@BASENAME@'
  README.rst: Add missing libgtest-dev dependency for lc-compliance
  test: gstreamer: Remove videoconvert element from pipeline
  test: ipc: unixsocket: Increase process exit timeout
  test: log: log_process: Increase process exit timeout
  test: log: log_process: Improve debugging on process exit failures
  test: log: log_process: Fix uninitialized variable on process exit failure
  test: log: log_process: Log an error when failing due to incorrect message
  meson: Tag all installed files
  gstreamer: Implement renegotiation
  gstreamer: Add GstLibcameraSrcState::clearRequests method
  gstreamer: Move negotiation logic to separate function
  test: gstreamer: Fix indentation in comments
  meson_options: Sort options alphabetically
  README.rst: Document dependencies for the Python bindings
  README.rst: Drop indirect dependencies on Qt5 packages
  ipa: rpi: cac: Minor code improvements and tidying
  ipa: rpi: agc: Fix bug where AeLocked was never getting set
  documentation: Document vendor specific controls and properties handling
  libcamera: controls: Use vendor tags for draft controls and properties
  libcamera: control: Add vendor control id range reservation
  build: controls: Rework how controls and properties are generated
  controls: Update argument handling for controls generation scripts
  controls: Add vendor control/property support to generation scripts
  libcamera: pipeline: Fix c++20 compile warning
  Documentation: Fix list indentation
  README.rst: Use code directive for all code examples
  Documentation: contributing: Integrate the code of conduct
  Documentation: code-of-conduct: Add contact information
  Documentation: code-of-conduct: Add license information
  Documentation: code-of-conduct: Convert to reStructuredText
  Documentation: Add code of conduct
  gstreamer: Fix unused variable error
  ipa: rpi: alsc: Do not allow zero colour ratio statistics
  gstreamer: Implement element EOS handling
  pipeline: rpi: Add some useful logging messages
  libcamera: camera_sensor: Only access V4L_CID_HBLANK if existing
  pipeline: rpi: vc4: Fix drop frame bug when no ISP streams are configured
  pipeline: rpi: Remove unused variable
  libcamera: controls: Add controls for HDR
  ipa: rpi: agc: Make AGC controls affect all channels
  ipa: rpi: agc: Fetch AWB status in the prepare method
  ipa: rpi: vc4: Move denoise control handling into the VC4 derived IPA
  ipa: rpi: agc: Allow AGC channels to avoid using "fast desaturation"
  ipa: rpi: alsc: Do not re-read the alsc.status metadata
  ipa: rpi: hdr: Add the ability to alter the LSC table
  libcamera: transform: Make the transformFromOrientation() function static
  libcamera: transform: Fold transformToOrientation() in its only caller
  libcamera: camera_sensor: Cache mounting orientation instead of transform
  py: cam: Add option to set stream orientation
  apps: cam: Add option to set stream orientation
  libcamera: Use CameraConfiguration::orientation
  py: libcamera: Define and use Orientation
  test: Add unit test for Transform and Orientation
  libcamera: transform: Add operations with Orientation
  libcamera: transform: Invert operator*() operands
  libcamera: transform: Add functions to convert Orientation
  libcamera: properties: Make 'Rotation' the mounting rotation
  Documentation: Add figures to document Orientation
  libcamera: camera: Introduce Orientation
  libcamera: camera_sensor: Cache rotationTransform_
  apps: qcam: Add support for RGB565
  pipeline: rpi: vc4: Allocate more embedded data buffers
  utils: update-kernel-headers: Support git worktrees
  ipa: rpi: agc: When AGC channels are changed, start with the 1st channel
  ipa: rpi: agc: Avoid overwriting caller's statistics pointer
  ipa: rpi: denoise: Support different denoise configurations
  ipa: rpi: contrast: Allow adaptive contrast enhancement to be disabled
  ipa: rpi: agc: Add an AGC stable region
  pipeline: rpi: Make color space members in RPiCameraConfiguration public
  pipeline: rpi: Move flip handling validation code
  build: ipa: Fix bug in building multiple IPA interfaces with the same mojom file
  pipeline: rpi: Add SW downscale status to RPi::Stream
  ipa: rpi: Add new algorithms for PiSP
  ipa: rpi: Prepare AWB for PiSP support
  ipa: rpi: Prepare ALSC for PiSP support
  ipa: rpi: Add PiSP definitions to the Controller hardware description
  ipa: rpi: Add statsInline to the Controller hardware description
  ipa: rpi: Add try_lock() to RPiController::Metadata
  mojom: pipeline: ipa: rpi: Add fields for PiSP objects
  ipa: rpi: Add IpaBase::platformStart() member function
  pipeline: rpi: Add SharedMemObject class
  pipeline: rpi: Add Recurrent and Needs32bitConv flags to RPi::Stream
  pipeline: rpi: Add RequiresMmap flag to RPi::Stream
  py: gen-py-controls: Remove SceneFlicker workaround
  py: Add the SensorConfiguration class
  libcamera: converter: drop unnecessary #include directive
  libcamera: converter: A few fixes to ConverterFactoryBase documentation
  ipa: rpi: Avoid skipping IPAs on the first frame after the drop frames
  libcamera: rpi: Simplify validate() and configure() for YUV/RGB streams
  libcamera: rpi: Change default stream formats
  libcamera: rpi: Simplify validate() and configure() for RAW streams
  libcamera: rpi: Add some helpers to PipelineHandlerBase
  libcamera: rpi: Cache rawStreams and outStreams
  libcamera: rpi: Make isRaw/isYuv/isRgb static functions
  libcamera: rpi: Fix wrong comment indentation
  libcamera: rpi: Allow platformValidate() to adjust format strides
  libcamera: rpi: Handle SensorConfiguration
  libcamera: camera_sensor: Support SensorConfiguration
  libcamera: camera: Introduce SensorConfiguration
  Documentation: Introduce Camera Sensor Model
  pipeline: rpi: Reset the frame lengths queue during configure
  ipa: rpi: Fix frame count logic when running algorithms
  libcamera: formats: Fix typo in YV444 V4L2PixelFormat listing

Change-Id: I882a312735a555622cd31d076efca85a4ffd3964
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/third_party/libcamera/+/5669744
Commit-Queue: Hanlin Chen <hanlinchen@google.com>
Tested-by: Harvey Yang <chenghaoyang@chromium.org>
Auto-Submit: Harvey Yang <chenghaoyang@chromium.org>
Reviewed-by: Hanlin Chen <hanlinchen@google.com>
diff --git a/.clang-format b/.clang-format
index cac7029..f2b44e8 100644
--- a/.clang-format
+++ b/.clang-format
@@ -1,6 +1,6 @@
 # SPDX-License-Identifier: GPL-2.0-only
 #
-# clang-format configuration file. Intended for clang-format >= 7.
+# clang-format configuration file. Intended for clang-format >= 12.
 #
 # For more information, see:
 #
@@ -75,6 +75,7 @@
     Priority:        9
   # Qt includes (match before C++ standard library)
   - Regex:           '<Q([A-Za-z0-9\-_])+>'
+    CaseSensitive:   true
     Priority:        9
   # Headers in <> with an extension. (+system libraries)
   - Regex:           '<([A-Za-z0-9\-_])+\.h>'
diff --git a/.reuse/dep5 b/.reuse/dep5
index d283a0c..c5ef5e0 100644
--- a/.reuse/dep5
+++ b/.reuse/dep5
@@ -3,6 +3,12 @@
 Upstream-Contact: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 Source: https://git.libcamera.org/libcamera/libcamera.git/
 
+Files: Documentation/binning.svg
+       Documentation/camera-sensor-model.rst
+       Documentation/sensor_model.svg
+Copyright: Copyright 2023 Ideas On Board Oy
+License: CC-BY-SA-4.0
+
 Files: Documentation/theme/static/search.png
 Copyright: 2022 Fonticons, Inc.
 License: CC-BY-4.0
diff --git a/Documentation/Doxyfile.in b/Documentation/Doxyfile.in
index 697a14d..abafcf6 100644
--- a/Documentation/Doxyfile.in
+++ b/Documentation/Doxyfile.in
@@ -20,6 +20,7 @@
 CASE_SENSE_NAMES       = YES
 
 QUIET                  = YES
+WARN_AS_ERROR          = @WARN_AS_ERROR@
 
 INPUT                  = "@TOP_SRCDIR@/include/libcamera" \
                          "@TOP_SRCDIR@/src/ipa/ipu3" \
@@ -44,6 +45,7 @@
                          @TOP_SRCDIR@/src/libcamera/pipeline/ \
                          @TOP_SRCDIR@/src/libcamera/tracepoints.cpp \
                          @TOP_BUILDDIR@/include/libcamera/internal/tracepoints.h \
+                         @TOP_BUILDDIR@/include/libcamera/ipa/soft_ipa_interface.h \
                          @TOP_BUILDDIR@/src/libcamera/proxy/
 
 EXCLUDE_PATTERNS       = @TOP_BUILDDIR@/include/libcamera/ipa/*_serializer.h \
@@ -78,6 +80,8 @@
 INCLUDE_PATH           = "@TOP_SRCDIR@/include/libcamera"
 INCLUDE_FILE_PATTERNS  = *.h
 
+IMAGE_PATH             = "@TOP_SRCDIR@/Documentation/images"
+
 PREDEFINED             = __DOXYGEN__ \
                          __cplusplus \
                          __attribute__(x)= \
diff --git a/Documentation/binning.svg b/Documentation/binning.svg
new file mode 100644
index 0000000..c6a3b63
--- /dev/null
+++ b/Documentation/binning.svg
@@ -0,0 +1,5053 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="binning.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     inkscape:zoom="0.96951281"
+     inkscape:cx="513.66005"
+     inkscape:cy="278.49039"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458333"
+       spacingy="0.26458334"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1">
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-87"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-7"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-91" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-74"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-1" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-12" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-1" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-41"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-58"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-1"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-60" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-13" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-60" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-64"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-1"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-82"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-38" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-50"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-92"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-2"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-7"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-87" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-62" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-37" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-2"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-35" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-87-1"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-0-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-5-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-2-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-7-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-91-1" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-9-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-6-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-3-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-6-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-8-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-4-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-6-7"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-6-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-74-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-1-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-0-1"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-0-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-6-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-4-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-4-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-3-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-4-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-3-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-3-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-6-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-9-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-0-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-4-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-8-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-5-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-12-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-9-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-7-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-3-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-8-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-9-2"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-2-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-3-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-1-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-8-2"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-7-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-41-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-6-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-3-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-5-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-58-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-7-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-1-7"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-7-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-0-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-60-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-4-2"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-13-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-0-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-2-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-9-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-60-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-3-7"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-5-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-4-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-3-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-64-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-3-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-1-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-9-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-74" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-7"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-1" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-57"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-88" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-90"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-78" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-73" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-2"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-15"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-33" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-87-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-0-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-5-85"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-2-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-7-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-91-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-9-69"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-6-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-3-37"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-6-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-8-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-4-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-6-1"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-6-39" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-74-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-1-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-0-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-0-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-6-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-4-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-4-7"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-3-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-4-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-3-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-3-91"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-6-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-9-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-0-1" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-4-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-8-1" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-5-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-12-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-9-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-7-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-3-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-8-5" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-9-1"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-2-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-3-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-1-1" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-8-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-7-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-41-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-6-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-3-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-5-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-58-2"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-7-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-1-4"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-7-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-0-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-60-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-4-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-13-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-0-7"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-2-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-60-0-5-2-8-6-9-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-48-9-9-4-1-8-60-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-3-0"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-5-64" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-7-4-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-4-6-3-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-8-64-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-8-3-8" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-1-1"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path2-7-9-0" />
+    </marker>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3"
+       width="13.873985"
+       height="13.536115"
+       x="65.388786"
+       y="21.368647" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6"
+       width="13.873985"
+       height="13.536115"
+       x="79.4552"
+       y="21.265594" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3"
+       width="13.873985"
+       height="13.536115"
+       x="65.400574"
+       y="35.581291" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3"
+       width="13.873985"
+       height="13.536115"
+       x="79.487213"
+       y="35.445648" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3"
+       width="13.873985"
+       height="13.536115"
+       x="36.816032"
+       y="21.611094" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9"
+       width="13.873985"
+       height="13.536115"
+       x="50.88245"
+       y="21.508043" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7"
+       width="13.873985"
+       height="13.536115"
+       x="36.827824"
+       y="35.823738" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2"
+       width="13.873985"
+       height="13.536115"
+       x="50.914459"
+       y="35.688095" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3"
+       width="13.873985"
+       height="13.536115"
+       x="65.473557"
+       y="49.829689" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5"
+       width="13.873985"
+       height="13.536115"
+       x="79.53997"
+       y="49.726639" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0"
+       width="13.873985"
+       height="13.536115"
+       x="65.485344"
+       y="64.042336" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8"
+       width="13.873985"
+       height="13.536115"
+       x="79.571983"
+       y="63.906689" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4"
+       width="13.873985"
+       height="13.536115"
+       x="36.900803"
+       y="50.072136" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1"
+       width="13.873985"
+       height="13.536115"
+       x="50.96722"
+       y="49.969086" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1"
+       width="13.873985"
+       height="13.536115"
+       x="36.912594"
+       y="64.284782" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3"
+       width="13.873985"
+       height="13.536115"
+       x="50.999229"
+       y="64.149139" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded)"
+       d="M 42.081702,56.601729 C 40.286141,52.204844 39.84416,48.248056 39.775934,43.793323 39.705994,39.227507 40.058028,34.155406 42.189147,28.949241"
+       id="path1"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60)"
+       d="M 55.099315,56.107712 C 46.749885,47.204301 45.5057,37.755811 55.174295,27.470139"
+       id="path1-8"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0)"
+       d="M 57.045954,72.804438 C 51.147833,63.901027 50.090593,54.779467 56.920539,44.493796"
+       id="path1-8-5"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5)"
+       d="M 73.072075,56.721348 C 57.027587,57.689095 47.292535,48.710474 44.701645,29.866502"
+       id="path1-8-5-6"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2)"
+       d="M 72.90507,70.575484 C 57.457506,78.709997 45.319286,62.459593 45.244064,43.230157"
+       id="path1-8-5-6-7"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8)"
+       d="M 86.158751,59.09319 C 69.785862,58.470577 60.1065,48.008767 56.857885,29.968538"
+       id="path1-8-5-6-7-8"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6)"
+       d="M 86.457671,71.13699 C 71.010108,79.271503 59.102258,64.163122 59.027036,44.933685"
+       id="path1-8-5-6-7-8-2"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded)"
+       d="m 44.687693,41.420418 c 6.448084,-5.278368 15.466061,-6.30545 28.431123,-0.80303"
+       id="path1-7"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6)"
+       d="M 57.078428,27.878124 C 67.151136,21.834217 72.464093,21.119079 86.668639,27.745841"
+       id="path1-7-3"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7)"
+       d="m 43.794332,28.085665 c 3.63222,-4.278367 8.633495,-5.657536 14.125001,-5.510111 4.339623,0.11649 8.774913,0.820414 14.546216,4.344855"
+       id="path1-7-3-3"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8)"
+       d="m 58.86173,43.233874 c 6.448089,-7.475501 14.979372,-8.678728 27.944435,-0.885912"
+       id="path1-7-1"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1)"
+       d="M 43.150248,70.196421 C 39.946602,64.842954 38.991849,59.890688 39.59491,54.079585 c 0.399898,-3.853407 1.143216,-8.419661 3.603258,-12.520737"
+       id="path1-5"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-4"
+       width="13.873985"
+       height="13.536115"
+       x="122.23671"
+       y="20.698904" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-7"
+       width="13.873985"
+       height="13.536115"
+       x="136.30313"
+       y="20.595854" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-2"
+       width="13.873985"
+       height="13.536115"
+       x="122.2485"
+       y="34.911549" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-1"
+       width="13.873985"
+       height="13.536115"
+       x="136.33514"
+       y="34.775906" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-9"
+       width="13.873985"
+       height="13.536115"
+       x="93.663963"
+       y="20.941351" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-5"
+       width="13.873985"
+       height="13.536115"
+       x="107.73038"
+       y="20.838301" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-6"
+       width="13.873985"
+       height="13.536115"
+       x="93.675751"
+       y="35.153996" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-8"
+       width="13.873985"
+       height="13.536115"
+       x="107.76238"
+       y="35.018353" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-5"
+       width="13.873985"
+       height="13.536115"
+       x="122.32149"
+       y="49.159946" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-5"
+       width="13.873985"
+       height="13.536115"
+       x="136.38791"
+       y="49.056896" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-2"
+       width="13.873985"
+       height="13.536115"
+       x="122.33327"
+       y="63.372593" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-1"
+       width="13.873985"
+       height="13.536115"
+       x="136.41991"
+       y="63.23695" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-9"
+       width="13.873985"
+       height="13.536115"
+       x="93.748734"
+       y="49.402393" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-9"
+       width="13.873985"
+       height="13.536115"
+       x="107.81515"
+       y="49.299343" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-1"
+       width="13.873985"
+       height="13.536115"
+       x="93.760521"
+       y="63.61504" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-6"
+       width="13.873985"
+       height="13.536115"
+       x="107.84715"
+       y="63.479397" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-87)"
+       d="m 98.929634,55.931997 c -1.795575,-4.396885 -2.23755,-8.353673 -2.30576,-12.808407 -0.06986,-4.565816 0.282091,-9.637916 2.413212,-14.844081"
+       id="path3"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-5)"
+       d="m 111.94724,55.43798 c -8.34942,-8.903412 -9.59363,-18.351902 0.075,-28.637573"
+       id="path1-8-2"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-7)"
+       d="m 113.89388,72.134706 c -5.89813,-8.903412 -6.95537,-18.024971 -0.12542,-28.310642"
+       id="path1-8-5-1"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-9)"
+       d="M 129.92,56.051616 C 113.87551,57.019363 104.14045,48.040742 101.54958,29.196769"
+       id="path1-8-5-6-0"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-3)"
+       d="M 129.75299,69.905752 C 114.30543,78.040265 102.16722,61.789861 102.092,42.560424"
+       id="path1-8-5-6-7-5"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-8)"
+       d="M 143.00667,58.423457 C 126.63378,57.800845 116.95442,47.339035 113.70581,29.298805"
+       id="path1-8-5-6-7-8-3"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-6)"
+       d="M 143.30559,70.467258 C 127.85803,78.601771 115.95018,63.49339 115.87496,44.263953"
+       id="path1-8-5-6-7-8-2-2"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-87)"
+       d="m 101.53561,40.750686 c 6.44809,-5.278368 15.46606,-6.305451 28.43113,-0.803031"
+       id="path1-7-19"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-74)"
+       d="m 113.92635,27.208392 c 10.07271,-6.043908 15.38566,-6.759046 29.59021,-0.132283"
+       id="path1-7-3-7"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-0)"
+       d="m 100.64224,27.415933 c 3.63223,-4.278368 8.63351,-5.657536 14.12501,-5.510111 4.33963,0.11649 8.77492,0.820414 14.54622,4.344855"
+       id="path1-7-3-3-6"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-6)"
+       d="m 115.70965,42.564142 c 6.44809,-7.475501 14.97937,-8.678728 27.94444,-0.885913"
+       id="path1-7-1-8"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-4)"
+       d="M 99.998175,69.526688 C 96.79452,64.173222 95.83977,59.220956 96.442844,53.409853 c 0.3999,-3.853408 1.143197,-8.419661 3.603256,-12.520737"
+       id="path1-5-2"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-5"
+       width="13.873985"
+       height="13.536115"
+       x="179.51106"
+       y="20.926241" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-5"
+       width="13.873985"
+       height="13.536115"
+       x="193.57747"
+       y="20.823191" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-0"
+       width="13.873985"
+       height="13.536115"
+       x="179.52284"
+       y="35.138885" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-12"
+       width="13.873985"
+       height="13.536115"
+       x="193.60948"
+       y="35.003242" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-4"
+       width="13.873985"
+       height="13.536115"
+       x="150.93829"
+       y="21.16869" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-8"
+       width="13.873985"
+       height="13.536115"
+       x="165.00471"
+       y="21.065638" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-5"
+       width="13.873985"
+       height="13.536115"
+       x="150.95009"
+       y="35.381336" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-6"
+       width="13.873985"
+       height="13.536115"
+       x="165.03673"
+       y="35.245689" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-52"
+       width="13.873985"
+       height="13.536115"
+       x="179.59583"
+       y="49.387283" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-6"
+       width="13.873985"
+       height="13.536115"
+       x="193.66225"
+       y="49.284233" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-9"
+       width="13.873985"
+       height="13.536115"
+       x="179.60762"
+       y="63.59993" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-13"
+       width="13.873985"
+       height="13.536115"
+       x="193.69426"
+       y="63.464287" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-1"
+       width="13.873985"
+       height="13.536115"
+       x="151.02307"
+       y="49.629734" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-5"
+       width="13.873985"
+       height="13.536115"
+       x="165.08948"
+       y="49.526684" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-2"
+       width="13.873985"
+       height="13.536115"
+       x="151.03485"
+       y="63.842381" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-69"
+       width="13.873985"
+       height="13.536115"
+       x="165.12149"
+       y="63.706734" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-4)"
+       d="m 156.20396,56.159335 c -1.79555,-4.396885 -2.23753,-8.353673 -2.30576,-12.808407 -0.0699,-4.565816 0.28209,-9.637916 2.41321,-14.844082"
+       id="path3-4"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-3)"
+       d="m 169.22158,55.665317 c -8.34944,-8.903411 -9.59362,-18.351901 0.075,-28.637572"
+       id="path1-8-4"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-9)"
+       d="M 171.16822,72.362043 C 165.2701,63.458632 164.21286,54.337072 171.04281,44.051401"
+       id="path1-8-5-60"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-4)"
+       d="M 187.19434,56.278954 C 171.14986,57.246701 161.4148,48.26808 158.8239,29.424107"
+       id="path1-8-5-6-74"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-5)"
+       d="m 187.02734,70.133089 c -15.44756,8.134513 -27.5858,-8.11589 -27.66102,-27.345327"
+       id="path1-8-5-6-7-3"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-9)"
+       d="M 200.28102,58.650795 C 183.90813,58.028182 174.22877,47.566373 170.98015,29.526143"
+       id="path1-8-5-6-7-8-7"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-3)"
+       d="M 200.57994,70.694595 C 185.13238,78.829108 173.22453,63.720728 173.14931,44.491291"
+       id="path1-8-5-6-7-8-2-9"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-4)"
+       d="m 158.80996,40.978023 c 6.44809,-5.278367 15.46606,-6.30545 28.43113,-0.80303"
+       id="path1-7-0"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-9)"
+       d="m 171.2007,27.43573 c 10.07271,-6.043908 15.38566,-6.759046 29.59021,-0.132283"
+       id="path1-7-3-0"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-3)"
+       d="m 157.91659,27.643271 c 3.63223,-4.278368 8.63351,-5.657536 14.12501,-5.510111 4.33963,0.11649 8.77492,0.820413 14.54622,4.344855"
+       id="path1-7-3-3-2"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-8)"
+       d="m 172.984,42.791479 c 6.44809,-7.4755 14.97937,-8.678727 27.94443,-0.885912"
+       id="path1-7-1-2"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-41)"
+       d="m 157.27252,69.754026 c -3.20365,-5.353467 -4.1584,-10.305732 -3.55535,-16.116836 0.3999,-3.853407 1.14322,-8.41966 3.60325,-12.520737"
+       id="path1-5-6"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-9"
+       width="13.873985"
+       height="13.536115"
+       x="236.369"
+       y="20.627245" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-4"
+       width="13.873985"
+       height="13.536115"
+       x="250.43542"
+       y="20.524195" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-5"
+       width="13.873985"
+       height="13.536115"
+       x="236.3808"
+       y="34.83989" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-6"
+       width="13.873985"
+       height="13.536115"
+       x="250.46744"
+       y="34.704247" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-5"
+       width="13.873985"
+       height="13.536115"
+       x="207.79625"
+       y="20.869694" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-9"
+       width="13.873985"
+       height="13.536115"
+       x="221.86267"
+       y="20.766644" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-1"
+       width="13.873985"
+       height="13.536115"
+       x="207.80804"
+       y="35.082336" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-7"
+       width="13.873985"
+       height="13.536115"
+       x="221.89468"
+       y="34.946693" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-4"
+       width="13.873985"
+       height="13.536115"
+       x="236.45378"
+       y="49.088287" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-1"
+       width="13.873985"
+       height="13.536115"
+       x="250.5202"
+       y="48.985237" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-1"
+       width="13.873985"
+       height="13.536115"
+       x="236.46558"
+       y="63.300934" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-9"
+       width="13.873985"
+       height="13.536115"
+       x="250.5522"
+       y="63.165291" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-3"
+       width="13.873985"
+       height="13.536115"
+       x="207.88103"
+       y="49.330734" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-8"
+       width="13.873985"
+       height="13.536115"
+       x="221.94745"
+       y="49.227684" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-7"
+       width="13.873985"
+       height="13.536115"
+       x="207.89282"
+       y="63.543381" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-4"
+       width="13.873985"
+       height="13.536115"
+       x="221.97945"
+       y="63.407738" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-3)"
+       d="m 213.06192,55.860339 c -1.79558,-4.396885 -2.23755,-8.353673 -2.30579,-12.808407 -0.0699,-4.565816 0.28212,-9.637916 2.41321,-14.844081"
+       id="path3-9"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-58)"
+       d="M 226.07952,55.366322 C 217.7301,46.46291 216.4859,37.01442 226.15452,26.728749"
+       id="path1-8-3"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-1)"
+       d="M 228.02616,72.063048 C 222.12804,63.159636 221.0708,54.038077 227.90074,43.752406"
+       id="path1-8-5-13"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-0)"
+       d="M 244.05228,55.979958 C 228.00779,56.947705 218.27273,47.969084 215.68186,29.125111"
+       id="path1-8-5-6-8"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-4)"
+       d="M 243.88527,69.834094 C 228.43771,77.968606 216.2995,61.718203 216.22428,42.488766"
+       id="path1-8-5-6-7-7"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-0)"
+       d="M 257.13895,58.351799 C 240.76607,57.729187 231.0867,47.267377 227.83809,29.227147"
+       id="path1-8-5-6-7-8-9"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-9)"
+       d="M 257.43787,70.3956 C 241.99031,78.530112 230.08246,63.421732 230.00724,44.192295"
+       id="path1-8-5-6-7-8-2-3"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-3)"
+       d="m 215.66789,40.679027 c 6.44809,-5.278367 15.46607,-6.30545 28.43113,-0.80303"
+       id="path1-7-8"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-3)"
+       d="m 228.05863,27.136734 c 10.07271,-6.043908 15.38567,-6.759046 29.59021,-0.132283"
+       id="path1-7-3-8"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-4)"
+       d="m 214.77453,27.344275 c 3.63222,-4.278368 8.6335,-5.657536 14.12501,-5.510111 4.33962,0.11649 8.77491,0.820413 14.54621,4.344855"
+       id="path1-7-3-3-4"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-64)"
+       d="m 229.84193,42.492484 c 6.44809,-7.475501 14.97938,-8.678728 27.94444,-0.885913"
+       id="path1-7-1-0"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-1)"
+       d="m 214.13046,49.215575 c -3.20366,-5.353466 -4.15841,-10.305732 -3.55533,-16.116835 0.39987,-3.853408 1.14319,-8.419661 3.60325,-12.520737"
+       id="path1-5-0"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-46"
+       width="13.873985"
+       height="13.536115"
+       x="65.132538"
+       y="77.161232" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-8"
+       width="13.873985"
+       height="13.536115"
+       x="79.198952"
+       y="77.058182" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-3"
+       width="13.873985"
+       height="13.536115"
+       x="65.144325"
+       y="91.373878" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-2"
+       width="13.873985"
+       height="13.536115"
+       x="79.230965"
+       y="91.238235" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-6"
+       width="13.873985"
+       height="13.536115"
+       x="36.559784"
+       y="77.403679" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-2"
+       width="13.873985"
+       height="13.536115"
+       x="50.626202"
+       y="77.300629" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-2"
+       width="13.873985"
+       height="13.536115"
+       x="36.571575"
+       y="91.616325" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-4"
+       width="13.873985"
+       height="13.536115"
+       x="50.658211"
+       y="91.480682" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-49"
+       width="13.873985"
+       height="13.536115"
+       x="65.217308"
+       y="105.62228" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-65"
+       width="13.873985"
+       height="13.536115"
+       x="79.283722"
+       y="105.51923" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-4"
+       width="13.873985"
+       height="13.536115"
+       x="65.229095"
+       y="119.83492" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-6"
+       width="13.873985"
+       height="13.536115"
+       x="79.315735"
+       y="119.69928" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-2"
+       width="13.873985"
+       height="13.536115"
+       x="36.644554"
+       y="105.86472" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-51"
+       width="13.873985"
+       height="13.536115"
+       x="50.710972"
+       y="105.76167" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-6"
+       width="13.873985"
+       height="13.536115"
+       x="36.656345"
+       y="120.07737" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-1"
+       width="13.873985"
+       height="13.536115"
+       x="50.742981"
+       y="119.94173" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-82)"
+       d="m 41.825452,112.39432 c -1.795561,-4.39688 -2.237542,-8.35367 -2.305768,-12.808406 -0.06994,-4.565816 0.282094,-9.637917 2.413213,-14.844082"
+       id="path6"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-50)"
+       d="m 54.843065,111.9003 c -8.34943,-8.90341 -9.593615,-18.351898 0.07498,-28.63757"
+       id="path1-8-37"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-92)"
+       d="m 56.789704,128.59703 c -5.898121,-8.90341 -6.955361,-18.02497 -0.125415,-28.31064"
+       id="path1-8-5-7"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-8)"
+       d="m 72.815825,112.51394 c -16.044488,0.96775 -25.77954,-8.01087 -28.37043,-26.854847"
+       id="path1-8-5-6-1"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-2)"
+       d="M 72.64882,126.36808 C 57.201256,134.50259 45.063036,118.25218 44.987814,99.022748"
+       id="path1-8-5-6-7-2"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-3)"
+       d="M 85.902501,114.88578 C 69.529612,114.26317 59.85025,103.80136 56.601635,85.761129"
+       id="path1-8-5-6-7-8-1"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-7)"
+       d="m 86.201421,126.92958 c -15.447563,8.13451 -27.355413,-6.97387 -27.430635,-26.2033"
+       id="path1-8-5-6-7-8-2-0"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-82)"
+       d="m 44.431443,97.213009 c 6.448084,-5.278368 15.466061,-6.30545 28.431123,-0.80303"
+       id="path1-7-89"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-6)"
+       d="M 56.822178,83.670715 C 66.894886,77.626808 72.207843,76.91167 86.412389,83.538432"
+       id="path1-7-3-01"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-9)"
+       d="m 43.538082,83.878256 c 3.63222,-4.278367 8.633495,-5.657536 14.125001,-5.510111 4.339623,0.11649 8.774913,0.820414 14.546216,4.344855"
+       id="path1-7-3-3-3"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-0)"
+       d="m 58.60548,99.026465 c 6.448089,-7.475501 14.979372,-8.678728 27.944435,-0.885912"
+       id="path1-7-1-4"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-2)"
+       d="m 42.893998,125.98901 c -3.203646,-5.35346 -4.158399,-10.30573 -3.555338,-16.11683 0.399898,-3.85341 1.143216,-8.41966 3.603258,-12.520741"
+       id="path1-5-8"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-4-8"
+       width="13.873985"
+       height="13.536115"
+       x="121.98046"
+       y="76.491493" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-7-2"
+       width="13.873985"
+       height="13.536115"
+       x="136.04688"
+       y="76.388443" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-2-2"
+       width="13.873985"
+       height="13.536115"
+       x="121.99226"
+       y="90.70414" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-1-0"
+       width="13.873985"
+       height="13.536115"
+       x="136.07889"
+       y="90.568497" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-9-3"
+       width="13.873985"
+       height="13.536115"
+       x="93.407707"
+       y="76.73394" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-5-6"
+       width="13.873985"
+       height="13.536115"
+       x="107.47413"
+       y="76.63089" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-6-1"
+       width="13.873985"
+       height="13.536115"
+       x="93.419502"
+       y="90.946587" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-8-0"
+       width="13.873985"
+       height="13.536115"
+       x="107.50613"
+       y="90.810944" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-5-7"
+       width="13.873985"
+       height="13.536115"
+       x="122.06523"
+       y="104.95254" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-5-4"
+       width="13.873985"
+       height="13.536115"
+       x="136.13165"
+       y="104.84949" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-2-2"
+       width="13.873985"
+       height="13.536115"
+       x="122.07703"
+       y="119.16518" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-1-0"
+       width="13.873985"
+       height="13.536115"
+       x="136.16367"
+       y="119.02954" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-9-3"
+       width="13.873985"
+       height="13.536115"
+       x="93.492477"
+       y="105.19498" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-9-3"
+       width="13.873985"
+       height="13.536115"
+       x="107.5589"
+       y="105.09193" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-1-9"
+       width="13.873985"
+       height="13.536115"
+       x="93.504272"
+       y="119.40763" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-6-0"
+       width="13.873985"
+       height="13.536115"
+       x="107.5909"
+       y="119.27199" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-87-1)"
+       d="m 98.673384,111.72459 c -1.795575,-4.39689 -2.23755,-8.35368 -2.30576,-12.808409 -0.06986,-4.565816 0.282091,-9.637916 2.413212,-14.844081"
+       id="path7"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-5-8)"
+       d="m 111.69099,111.23057 c -8.34942,-8.90341 -9.59363,-18.351901 0.075,-28.637572"
+       id="path1-8-2-4"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-7-4)"
+       d="m 113.63763,127.9273 c -5.89813,-8.90341 -6.95537,-18.02497 -0.12542,-28.310645"
+       id="path1-8-5-1-3"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-9-6)"
+       d="m 129.66375,111.84421 c -16.04449,0.96774 -25.77955,-8.01088 -28.37042,-26.85485"
+       id="path1-8-5-6-0-3"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-3-3)"
+       d="m 129.49674,125.69834 c -15.44756,8.13452 -27.58577,-8.11589 -27.66099,-27.345325"
+       id="path1-8-5-6-7-5-4"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-8-4)"
+       d="M 142.75042,114.21605 C 126.37753,113.59344 116.69817,103.13163 113.44956,85.091396"
+       id="path1-8-5-6-7-8-3-2"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-6-7)"
+       d="m 143.04934,126.25985 c -15.44756,8.13451 -27.35541,-6.97387 -27.43063,-26.20331"
+       id="path1-8-5-6-7-8-2-2-4"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-87-1)"
+       d="m 101.27936,96.543277 c 6.44809,-5.278368 15.46606,-6.305451 28.43113,-0.803031"
+       id="path1-7-19-5"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-74-5)"
+       d="m 113.6701,83.000983 c 10.07271,-6.043908 15.38566,-6.759046 29.59021,-0.132283"
+       id="path1-7-3-7-3"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-0-1)"
+       d="m 100.38599,83.208524 c 3.63223,-4.278368 8.63351,-5.657536 14.12501,-5.510111 4.33963,0.11649 8.77492,0.820414 14.54622,4.344855"
+       id="path1-7-3-3-6-8"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-6-5)"
+       d="m 115.4534,98.356733 c 6.44809,-7.475501 14.97937,-8.678728 27.94444,-0.885913"
+       id="path1-7-1-8-1"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-4-0)"
+       d="m 99.741925,125.31928 c -3.203655,-5.35347 -4.158405,-10.30573 -3.555331,-16.11684 0.3999,-3.8534 1.143197,-8.41966 3.603258,-12.520733"
+       id="path1-5-2-1"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-5-1"
+       width="13.873985"
+       height="13.536115"
+       x="179.25481"
+       y="76.718834" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-5-5"
+       width="13.873985"
+       height="13.536115"
+       x="193.32123"
+       y="76.615784" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-0-8"
+       width="13.873985"
+       height="13.536115"
+       x="179.2666"
+       y="90.93148" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-12-1"
+       width="13.873985"
+       height="13.536115"
+       x="193.35323"
+       y="90.79583" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-4-3"
+       width="13.873985"
+       height="13.536115"
+       x="150.68204"
+       y="76.961281" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-8-9"
+       width="13.873985"
+       height="13.536115"
+       x="164.74846"
+       y="76.858231" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-5-7"
+       width="13.873985"
+       height="13.536115"
+       x="150.69383"
+       y="91.173927" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-6-5"
+       width="13.873985"
+       height="13.536115"
+       x="164.78047"
+       y="91.038284" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-52-4"
+       width="13.873985"
+       height="13.536115"
+       x="179.33958"
+       y="105.17988" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-6-9"
+       width="13.873985"
+       height="13.536115"
+       x="193.40599"
+       y="105.07683" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-9-6"
+       width="13.873985"
+       height="13.536115"
+       x="179.35136"
+       y="119.39252" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-13-7"
+       width="13.873985"
+       height="13.536115"
+       x="193.438"
+       y="119.25687" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-1-1"
+       width="13.873985"
+       height="13.536115"
+       x="150.76682"
+       y="105.42233" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-5-0"
+       width="13.873985"
+       height="13.536115"
+       x="164.83324"
+       y="105.31927" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-2-1"
+       width="13.873985"
+       height="13.536115"
+       x="150.77861"
+       y="119.63497" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-69-3"
+       width="13.873985"
+       height="13.536115"
+       x="164.86523"
+       y="119.49933" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-4-8)"
+       d="m 155.94771,111.95193 c -1.79555,-4.39689 -2.23753,-8.35368 -2.30576,-12.808411 -0.0699,-4.565816 0.28209,-9.637916 2.41321,-14.844082"
+       id="path3-4-7"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-3-9)"
+       d="m 168.96533,111.45791 c -8.34944,-8.90341 -9.59362,-18.351903 0.075,-28.637574"
+       id="path1-8-4-7"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-9-9)"
+       d="m 170.91197,128.15463 c -5.89812,-8.90341 -6.95536,-18.02497 -0.12541,-28.310638"
+       id="path1-8-5-60-6"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-4-6)"
+       d="m 186.93809,112.07154 c -16.04448,0.96775 -25.77954,-8.01087 -28.37044,-26.854842"
+       id="path1-8-5-6-74-3"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-5-5)"
+       d="m 186.77109,125.92568 c -15.44756,8.13451 -27.5858,-8.11589 -27.66102,-27.345327"
+       id="path1-8-5-6-7-3-9"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-9-5)"
+       d="M 200.02477,114.44339 C 183.65188,113.82077 173.97252,103.35896 170.7239,85.318734"
+       id="path1-8-5-6-7-8-7-1"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-3-0)"
+       d="m 200.32369,126.48719 c -15.44756,8.13451 -27.35541,-6.97387 -27.43063,-26.20331"
+       id="path1-8-5-6-7-8-2-9-0"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-4-8)"
+       d="m 158.55371,96.770614 c 6.44809,-5.278367 15.46606,-6.30545 28.43113,-0.80303"
+       id="path1-7-0-4"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-9-2)"
+       d="m 170.94445,83.228321 c 10.07271,-6.043908 15.38566,-6.759046 29.59021,-0.132283"
+       id="path1-7-3-0-1"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-3-9)"
+       d="m 157.66034,83.435862 c 3.63223,-4.278368 8.63351,-5.657536 14.12501,-5.510111 4.33963,0.11649 8.77492,0.820413 14.54622,4.344855"
+       id="path1-7-3-3-2-1"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-8-2)"
+       d="m 172.72775,98.58407 c 6.44809,-7.4755 14.97937,-8.678727 27.94443,-0.885912"
+       id="path1-7-1-2-8"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-41-6)"
+       d="m 157.01627,125.54662 c -3.20365,-5.35347 -4.1584,-10.30574 -3.55535,-16.11684 0.3999,-3.85341 1.14322,-8.41966 3.60325,-12.520736"
+       id="path1-5-6-6"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-9-3"
+       width="13.873985"
+       height="13.536115"
+       x="236.11276"
+       y="76.419838" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-4-4"
+       width="13.873985"
+       height="13.536115"
+       x="250.17918"
+       y="76.316788" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-5-7"
+       width="13.873985"
+       height="13.536115"
+       x="236.12454"
+       y="90.632484" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-6-7"
+       width="13.873985"
+       height="13.536115"
+       x="250.21118"
+       y="90.496841" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-5-7"
+       width="13.873985"
+       height="13.536115"
+       x="207.54001"
+       y="76.662285" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-9-4"
+       width="13.873985"
+       height="13.536115"
+       x="221.60643"
+       y="76.559235" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-1-5"
+       width="13.873985"
+       height="13.536115"
+       x="207.55179"
+       y="90.874931" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-7-3"
+       width="13.873985"
+       height="13.536115"
+       x="221.63843"
+       y="90.739288" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-4-7"
+       width="13.873985"
+       height="13.536115"
+       x="236.19753"
+       y="104.88088" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-1-2"
+       width="13.873985"
+       height="13.536115"
+       x="250.26395"
+       y="104.77783" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-1-9"
+       width="13.873985"
+       height="13.536115"
+       x="236.20932"
+       y="119.09353" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-9-9"
+       width="13.873985"
+       height="13.536115"
+       x="250.29596"
+       y="118.95789" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-3-0"
+       width="13.873985"
+       height="13.536115"
+       x="207.62477"
+       y="105.12333" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-8-2"
+       width="13.873985"
+       height="13.536115"
+       x="221.69119"
+       y="105.02028" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-7-7"
+       width="13.873985"
+       height="13.536115"
+       x="207.63657"
+       y="119.33598" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-4-9"
+       width="13.873985"
+       height="13.536115"
+       x="221.72321"
+       y="119.20033" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-3-5)"
+       d="m 212.80567,111.65293 c -1.79558,-4.39689 -2.23755,-8.35368 -2.30579,-12.808407 -0.0699,-4.565816 0.28212,-9.637916 2.41321,-14.844081"
+       id="path3-9-9"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-58-8)"
+       d="m 225.82327,111.15891 c -8.34942,-8.90341 -9.59362,-18.351899 0.075,-28.63757"
+       id="path1-8-3-5"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-1-7)"
+       d="m 227.76991,127.85564 c -5.89812,-8.90342 -6.95536,-18.02498 -0.12542,-28.310643"
+       id="path1-8-5-13-2"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-0-6)"
+       d="m 243.79603,111.77255 c -16.04449,0.96774 -25.77955,-8.01088 -28.37042,-26.854848"
+       id="path1-8-5-6-8-0"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-4-2)"
+       d="m 243.62902,125.62668 c -15.44756,8.13451 -27.58577,-8.11589 -27.66099,-27.345323"
+       id="path1-8-5-6-7-7-8"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-0-9)"
+       d="M 256.8827,114.14439 C 240.50982,113.52177 230.83045,103.05996 227.58184,85.019738"
+       id="path1-8-5-6-7-8-9-3"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-9-0)"
+       d="m 257.18162,126.18819 c -15.44756,8.13451 -27.35541,-6.97387 -27.43063,-26.203304"
+       id="path1-8-5-6-7-8-2-3-5"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-3-5)"
+       d="m 215.41164,96.471618 c 6.44809,-5.278367 15.46607,-6.30545 28.43113,-0.80303"
+       id="path1-7-8-9"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-3-7)"
+       d="m 227.80238,82.929325 c 10.07271,-6.043908 15.38567,-6.759046 29.59021,-0.132283"
+       id="path1-7-3-8-6"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-4-5)"
+       d="m 214.51828,83.136866 c 3.63222,-4.278368 8.6335,-5.657536 14.12501,-5.510111 4.33962,0.11649 8.77491,0.820413 14.54621,4.344855"
+       id="path1-7-3-3-4-5"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-64-4)"
+       d="m 229.58568,98.285075 c 6.44809,-7.475501 14.97938,-8.678728 27.94444,-0.885913"
+       id="path1-7-1-0-6"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-1-0)"
+       d="m 213.87421,105.00816 c -3.20366,-5.35346 -4.15841,-10.305726 -3.55533,-16.116829 0.39987,-3.853408 1.14319,-8.419661 3.60325,-12.520737"
+       id="path1-5-0-3"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-6"
+       width="13.873985"
+       height="13.536115"
+       x="65.269989"
+       y="133.67944" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-43"
+       width="13.873985"
+       height="13.536115"
+       x="79.336411"
+       y="133.57639" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-8"
+       width="13.873985"
+       height="13.536115"
+       x="65.281784"
+       y="147.89209" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-67"
+       width="13.873985"
+       height="13.536115"
+       x="79.368416"
+       y="147.75644" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-8"
+       width="13.873985"
+       height="13.536115"
+       x="36.697243"
+       y="133.92189" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-1"
+       width="13.873985"
+       height="13.536115"
+       x="50.76366"
+       y="133.81883" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-9"
+       width="13.873985"
+       height="13.536115"
+       x="36.709034"
+       y="148.13454" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-81"
+       width="13.873985"
+       height="13.536115"
+       x="50.79567"
+       y="147.99889" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-48"
+       width="13.873985"
+       height="13.536115"
+       x="65.354759"
+       y="162.14049" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-50"
+       width="13.873985"
+       height="13.536115"
+       x="79.421181"
+       y="162.03743" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-15"
+       width="13.873985"
+       height="13.536115"
+       x="65.366554"
+       y="176.35313" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-97"
+       width="13.873985"
+       height="13.536115"
+       x="79.453186"
+       y="176.21748" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-6"
+       width="13.873985"
+       height="13.536115"
+       x="36.782013"
+       y="162.38293" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-2"
+       width="13.873985"
+       height="13.536115"
+       x="50.848431"
+       y="162.27988" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-8"
+       width="13.873985"
+       height="13.536115"
+       x="36.793804"
+       y="176.59558" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-9"
+       width="13.873985"
+       height="13.536115"
+       x="50.88044"
+       y="176.45993" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-0)"
+       d="m 41.962911,168.91253 c -1.795561,-4.39689 -2.237542,-8.35368 -2.305768,-12.80841 -0.06994,-4.56582 0.282094,-9.63792 2.413213,-14.84408"
+       id="path6-5"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-7)"
+       d="m 54.980524,168.41851 c -8.34943,-8.90341 -9.593615,-18.3519 0.07498,-28.63757"
+       id="path1-8-0"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-0)"
+       d="m 56.927163,185.11524 c -5.898121,-8.90342 -6.955361,-18.02498 -0.125415,-28.31065"
+       id="path1-8-5-2"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-6)"
+       d="m 72.953284,169.03215 c -16.044488,0.96774 -25.77954,-8.01088 -28.37043,-26.85485"
+       id="path1-8-5-6-72"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-57)"
+       d="M 72.786279,182.88628 C 57.338715,191.02079 45.200495,174.77039 45.125273,155.54095"
+       id="path1-8-5-6-7-81"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-90)"
+       d="M 86.03996,171.40399 C 69.667071,170.78137 59.987709,160.31956 56.739094,142.27933"
+       id="path1-8-5-6-7-8-77"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-0)"
+       d="M 86.33888,183.44779 C 70.891317,191.5823 58.983467,176.47392 58.908245,157.24448"
+       id="path1-8-5-6-7-8-2-97"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-0)"
+       d="m 44.568902,153.73121 c 6.448084,-5.27836 15.466061,-6.30545 28.431123,-0.80303"
+       id="path1-7-9"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-0)"
+       d="m 56.959637,140.18892 c 10.072708,-6.04391 15.385665,-6.75904 29.590211,-0.13228"
+       id="path1-7-3-88"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-6)"
+       d="m 43.675541,140.39646 c 3.63222,-4.27837 8.633495,-5.65753 14.125001,-5.51011 4.339623,0.11649 8.774913,0.82041 14.546216,4.34486"
+       id="path1-7-3-3-9"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-2)"
+       d="m 58.742939,155.54467 c 6.448089,-7.4755 14.979372,-8.67873 27.944435,-0.88591"
+       id="path1-7-1-80"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-15)"
+       d="m 43.031457,182.50722 c -3.203646,-5.35347 -4.158399,-10.30573 -3.555338,-16.11684 0.399898,-3.85341 1.143216,-8.41966 3.603258,-12.52074"
+       id="path1-5-5"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-4-0"
+       width="13.873985"
+       height="13.536115"
+       x="122.11791"
+       y="133.0097" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-7-24"
+       width="13.873985"
+       height="13.536115"
+       x="136.18434"
+       y="132.90665" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-2-3"
+       width="13.873985"
+       height="13.536115"
+       x="122.12971"
+       y="147.22234" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-1-7"
+       width="13.873985"
+       height="13.536115"
+       x="136.21634"
+       y="147.0867" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-9-0"
+       width="13.873985"
+       height="13.536115"
+       x="93.545166"
+       y="133.25215" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-5-0"
+       width="13.873985"
+       height="13.536115"
+       x="107.61157"
+       y="133.14909" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-6-15"
+       width="13.873985"
+       height="13.536115"
+       x="93.556953"
+       y="147.4648" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-8-8"
+       width="13.873985"
+       height="13.536115"
+       x="107.64359"
+       y="147.32915" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-5-0"
+       width="13.873985"
+       height="13.536115"
+       x="122.20268"
+       y="161.47075" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-5-0"
+       width="13.873985"
+       height="13.536115"
+       x="136.2691"
+       y="161.36769" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-2-4"
+       width="13.873985"
+       height="13.536115"
+       x="122.21448"
+       y="175.68338" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-1-7"
+       width="13.873985"
+       height="13.536115"
+       x="136.30112"
+       y="175.54774" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-9-31"
+       width="13.873985"
+       height="13.536115"
+       x="93.629936"
+       y="161.7132" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-9-4"
+       width="13.873985"
+       height="13.536115"
+       x="107.69634"
+       y="161.61014" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-1-2"
+       width="13.873985"
+       height="13.536115"
+       x="93.641724"
+       y="175.92583" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-6-01"
+       width="13.873985"
+       height="13.536115"
+       x="107.72836"
+       y="175.79019" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-87-8)"
+       d="m 98.810843,168.24279 c -1.795575,-4.39688 -2.23755,-8.35367 -2.30576,-12.8084 -0.06986,-4.56582 0.282091,-9.63792 2.413212,-14.84408"
+       id="path7-3"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-5-85)"
+       d="m 111.82845,167.74878 c -8.34942,-8.90341 -9.59363,-18.3519 0.075,-28.63758"
+       id="path1-8-2-0"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-7-0)"
+       d="m 113.77509,184.4455 c -5.89813,-8.90341 -6.95537,-18.02497 -0.12542,-28.31064"
+       id="path1-8-5-1-0"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-9-69)"
+       d="m 129.80121,168.36241 c -16.04449,0.96775 -25.77955,-8.01087 -28.37042,-26.85484"
+       id="path1-8-5-6-0-4"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-3-37)"
+       d="m 129.6342,182.21655 c -15.44756,8.13451 -27.58577,-8.11589 -27.66099,-27.34533"
+       id="path1-8-5-6-7-5-8"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-8-3)"
+       d="M 142.88788,170.73425 C 126.51499,170.11164 116.83563,159.64983 113.58702,141.6096"
+       id="path1-8-5-6-7-8-3-22"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-6-1)"
+       d="m 143.1868,182.77805 c -15.44756,8.13452 -27.35541,-6.97386 -27.43063,-26.2033"
+       id="path1-8-5-6-7-8-2-2-0"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-87-8)"
+       d="m 101.41682,153.06148 c 6.44809,-5.27836 15.46606,-6.30545 28.43113,-0.80303"
+       id="path1-7-19-2"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-74-4)"
+       d="m 113.80756,139.51919 c 10.07271,-6.04391 15.38566,-6.75905 29.59021,-0.13228"
+       id="path1-7-3-7-4"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-0-0)"
+       d="m 100.52345,139.72673 c 3.63223,-4.27837 8.63351,-5.65754 14.12501,-5.51011 4.33963,0.11649 8.77492,0.82041 14.54622,4.34485"
+       id="path1-7-3-3-6-5"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-6-9)"
+       d="m 115.59086,154.87494 c 6.44809,-7.4755 14.97937,-8.67873 27.94444,-0.88591"
+       id="path1-7-1-8-8"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-4-7)"
+       d="m 99.879384,181.83749 c -3.203655,-5.35347 -4.158405,-10.30574 -3.555331,-16.11684 0.3999,-3.85341 1.143197,-8.41966 3.603256,-12.52074"
+       id="path1-5-2-4"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-5-8"
+       width="13.873985"
+       height="13.536115"
+       x="179.39226"
+       y="133.23703" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-5-4"
+       width="13.873985"
+       height="13.536115"
+       x="193.45868"
+       y="133.13399" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-0-2"
+       width="13.873985"
+       height="13.536115"
+       x="179.40405"
+       y="147.44968" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-12-5"
+       width="13.873985"
+       height="13.536115"
+       x="193.49069"
+       y="147.31404" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-4-2"
+       width="13.873985"
+       height="13.536115"
+       x="150.8195"
+       y="133.47948" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-8-7"
+       width="13.873985"
+       height="13.536115"
+       x="164.88593"
+       y="133.37643" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-5-5"
+       width="13.873985"
+       height="13.536115"
+       x="150.83128"
+       y="147.69212" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-6-7"
+       width="13.873985"
+       height="13.536115"
+       x="164.91792"
+       y="147.55649" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-52-2"
+       width="13.873985"
+       height="13.536115"
+       x="179.47704"
+       y="161.69807" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-6-2"
+       width="13.873985"
+       height="13.536115"
+       x="193.54346"
+       y="161.59503" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-9-8"
+       width="13.873985"
+       height="13.536115"
+       x="179.48883"
+       y="175.91072" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-13-6"
+       width="13.873985"
+       height="13.536115"
+       x="193.57545"
+       y="175.77509" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-1-0"
+       width="13.873985"
+       height="13.536115"
+       x="150.90427"
+       y="161.94052" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-5-09"
+       width="13.873985"
+       height="13.536115"
+       x="164.97069"
+       y="161.83748" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-2-12"
+       width="13.873985"
+       height="13.536115"
+       x="150.91606"
+       y="176.15317" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-69-6"
+       width="13.873985"
+       height="13.536115"
+       x="165.0027"
+       y="176.01753" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-4-6)"
+       d="m 156.08517,168.47013 c -1.79555,-4.39688 -2.23753,-8.35367 -2.30576,-12.8084 -0.0699,-4.56582 0.28209,-9.63792 2.41321,-14.84409"
+       id="path3-4-9"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-3-91)"
+       d="m 169.10279,167.97611 c -8.34944,-8.90341 -9.59362,-18.3519 0.075,-28.63757"
+       id="path1-8-4-4"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-9-3)"
+       d="m 171.04943,184.67284 c -5.89812,-8.90341 -6.95536,-18.02497 -0.12541,-28.31064"
+       id="path1-8-5-60-8"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-4-8)"
+       d="m 187.07555,168.58975 c -16.04448,0.96775 -25.77954,-8.01087 -28.37044,-26.85485"
+       id="path1-8-5-6-74-2"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-5-3)"
+       d="m 186.90855,182.44389 c -15.44756,8.13451 -27.5858,-8.11589 -27.66102,-27.34533"
+       id="path1-8-5-6-7-3-8"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-9-9)"
+       d="m 200.16223,170.96159 c -16.37289,-0.62261 -26.05225,-11.08442 -29.30087,-29.12465"
+       id="path1-8-5-6-7-8-7-2"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-3-4)"
+       d="m 200.46115,183.00539 c -15.44756,8.13452 -27.35541,-6.97387 -27.43063,-26.2033"
+       id="path1-8-5-6-7-8-2-9-7"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-4-6)"
+       d="m 158.69117,153.28882 c 6.44809,-5.27837 15.46606,-6.30545 28.43113,-0.80303"
+       id="path1-7-0-8"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-9-1)"
+       d="m 171.08191,139.74653 c 10.07271,-6.04391 15.38566,-6.75905 29.59021,-0.13229"
+       id="path1-7-3-0-6"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-3-0)"
+       d="m 157.7978,139.95407 c 3.63223,-4.27837 8.63351,-5.65754 14.12501,-5.51011 4.33963,0.11649 8.77492,0.82041 14.54622,4.34485"
+       id="path1-7-3-3-2-7"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-8-3)"
+       d="m 172.86521,155.10228 c 6.44809,-7.4755 14.97937,-8.67873 27.94443,-0.88592"
+       id="path1-7-1-2-7"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-41-0)"
+       d="m 157.15373,182.06482 c -3.20365,-5.35346 -4.1584,-10.30573 -3.55535,-16.11683 0.3999,-3.85341 1.14322,-8.41966 3.60325,-12.52074"
+       id="path1-5-6-0"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-937-0-3-9-4"
+       width="13.873985"
+       height="13.536115"
+       x="236.25021"
+       y="132.93803" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-4-7-6-4-5"
+       width="13.873985"
+       height="13.536115"
+       x="250.31664"
+       y="132.83498" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-5-5-3-5-4"
+       width="13.873985"
+       height="13.536115"
+       x="236.26201"
+       y="147.15068" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-25-8-3-6-2"
+       width="13.873985"
+       height="13.536115"
+       x="250.34863"
+       y="147.01503" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-7-0-3-5-3"
+       width="13.873985"
+       height="13.536115"
+       x="207.67746"
+       y="133.18048" />
+    <rect
+       style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-4-4-9-9-9"
+       width="13.873985"
+       height="13.536115"
+       x="221.74388"
+       y="133.07742" />
+    <rect
+       style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-4-8-7-1-6"
+       width="13.873985"
+       height="13.536115"
+       x="207.68925"
+       y="147.39313" />
+    <rect
+       style="fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-3-0-2-7-5"
+       width="13.873985"
+       height="13.536115"
+       x="221.77588"
+       y="147.25748" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-6-4-3-4-4"
+       width="13.873985"
+       height="13.536115"
+       x="236.33499"
+       y="161.39908" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-10-0-5-1-3"
+       width="13.873985"
+       height="13.536115"
+       x="250.4014"
+       y="161.29602" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-6-9-0-1-94"
+       width="13.873985"
+       height="13.536115"
+       x="236.34677"
+       y="175.61172" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-1-1-8-9-4"
+       width="13.873985"
+       height="13.536115"
+       x="250.43341"
+       y="175.47607" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-9-9-6-4-3-6"
+       width="13.873985"
+       height="13.536115"
+       x="207.76224"
+       y="161.64153" />
+    <rect
+       style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-5-1-49-2-1-8-0"
+       width="13.873985"
+       height="13.536115"
+       x="221.82864"
+       y="161.53847" />
+    <rect
+       style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-3-2-0-5-1-7-2"
+       width="13.873985"
+       height="13.536115"
+       x="207.77402"
+       y="175.85417" />
+    <rect
+       style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370418;paint-order:markers stroke fill"
+       id="rect288-56-7-9-4-3-4-2"
+       width="13.873985"
+       height="13.536115"
+       x="221.86066"
+       y="175.71852" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-3-0)"
+       d="m 212.94313,168.17113 c -1.79558,-4.39688 -2.23755,-8.35367 -2.30579,-12.8084 -0.0699,-4.56582 0.28212,-9.63792 2.41321,-14.84409"
+       id="path3-9-8"
+       inkscape:transform-center-x="-0.76484503"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-58-2)"
+       d="m 225.96073,167.67712 c -8.34942,-8.90342 -9.59362,-18.35191 0.075,-28.63758"
+       id="path1-8-3-4"
+       inkscape:transform-center-x="-1.756365"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-1-4)"
+       d="m 227.90737,184.37384 c -5.89812,-8.90341 -6.95536,-18.02497 -0.12542,-28.31064"
+       id="path1-8-5-13-3"
+       inkscape:transform-center-x="-1.2407313"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-0-9)"
+       d="M 243.93349,168.29075 C 227.889,169.2585 218.15394,160.27988 215.56307,141.43591"
+       id="path1-8-5-6-8-1"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-4-5)"
+       d="M 243.76648,182.14489 C 228.31892,190.2794 216.18071,174.029 216.10549,154.79956"
+       id="path1-8-5-6-7-7-1"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-0-7)"
+       d="M 257.02016,170.66259 C 240.64728,170.03998 230.96791,159.57817 227.7193,141.53794"
+       id="path1-8-5-6-7-8-9-1"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-60-0-5-2-8-6-9-6)"
+       d="m 257.31908,182.70639 c -15.44756,8.13452 -27.35541,-6.97386 -27.43063,-26.2033"
+       id="path1-8-5-6-7-8-2-3-9"
+       inkscape:transform-center-x="-4.374983"
+       inkscape:transform-center-y="-2.6647327"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-3-0)"
+       d="m 215.5491,152.98982 c 6.44809,-5.27837 15.46607,-6.30545 28.43113,-0.80303"
+       id="path1-7-8-0"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.1779744"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-3-0)"
+       d="m 227.93984,139.44753 c 10.07271,-6.04391 15.38567,-6.75905 29.59021,-0.13229"
+       id="path1-7-3-8-0"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.5378977"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-6-7-4-3)"
+       d="m 214.65574,139.65507 c 3.63222,-4.27837 8.6335,-5.65754 14.12501,-5.51011 4.33962,0.11649 8.77491,0.82041 14.54621,4.34485"
+       id="path1-7-3-3-4-0"
+       inkscape:transform-center-x="1.0156762"
+       inkscape:transform-center-y="1.709327"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-start:url(#ArrowWideRounded-8-64-5)"
+       d="m 229.72314,154.80328 c 6.44809,-7.4755 14.97938,-8.67873 27.94444,-0.88591"
+       id="path1-7-1-0-2"
+       inkscape:transform-center-x="1.007168"
+       inkscape:transform-center-y="1.6683096"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.25;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-1-1)"
+       d="m 214.01167,161.52637 c -3.20366,-5.35347 -4.15841,-10.30573 -3.55533,-16.11684 0.39987,-3.8534 1.14319,-8.41966 3.60325,-12.52073"
+       id="path1-5-0-5"
+       inkscape:transform-center-x="-1.1208011"
+       inkscape:transform-center-y="1.8780721"
+       sodipodi:nodetypes="csc" />
+  </g>
+</svg>
diff --git a/Documentation/camera-sensor-model.rst b/Documentation/camera-sensor-model.rst
new file mode 100644
index 0000000..b66c880
--- /dev/null
+++ b/Documentation/camera-sensor-model.rst
@@ -0,0 +1,173 @@
+.. SPDX-License-Identifier: CC-BY-SA-4.0
+
+.. _camera-sensor-model:
+
+.. todo: Move to Doxygen-generated documentation
+
+The libcamera camera sensor model
+=================================
+
+libcamera defines an abstract camera sensor model in order to provide
+a description of each of the processing steps that result in image data being
+sent on the media bus and that form the image stream delivered to applications.
+
+Applications should use the abstract camera sensor model defined here to
+precisely control the operations of the camera sensor.
+
+The libcamera camera sensor model targets image sensors producing frames in
+RAW format, delivered through a MIPI CSI-2 compliant bus implementation.
+
+The abstract sensor model maps libcamera components to the characteristics and
+operations of an image sensor, and serves as a reference to model the libcamera
+CameraSensor class and SensorConfiguration classes and operations.
+
+In order to control the configuration of the camera sensor through the
+SensorConfiguration class, applications should understand this model and map it
+to the combination of image sensor and kernel driver in use.
+
+The camera sensor model defined here is based on the *MIPI CCS specification*,
+particularly on *Section 8.2 - Image readout* of *Chapter 8 - Video Timings*.
+
+
+Glossary
+--------
+
+.. glossary::
+
+   Pixel array
+      The full grid of pixels, active and inactive ones
+
+   Pixel array active area
+      The portion(s) of the pixel array that contains valid and readable pixels;
+      corresponds to the libcamera properties::PixelArrayActiveAreas
+
+   Analog crop rectangle
+      The portion of the *pixel array active area* which is read out and passed
+      to further processing stages
+
+   Subsampling
+      Pixel processing techniques that reduce the image size by binning or by
+      skipping adjacent pixels
+
+   Digital crop
+      Crop of the sub-sampled image data before scaling
+
+   Frame output
+      The frame (image) as output on the media bus by the camera sensor
+
+Camera sensor model
+-------------------
+
+The abstract sensor model is described in the following diagram.
+
+.. figure:: sensor_model.svg
+
+
+1. The sensor reads pixels from the *pixel array*. The pixels being read out are
+   selected by the *analog crop rectangle*.
+
+2. The pixels can be subsampled to reduce the image size without affecting the
+   field of view. Two subsampling techniques can be used:
+
+   - Binning: combines adjacent pixels of the same colour by averaging or
+     summing their values, in the analog domain and/or the digital domain.
+
+      .. figure:: binning.svg
+
+
+   - Skipping: skips the read out of a number of adjacent pixels.
+
+      .. figure:: skipping.svg
+
+
+3. The output of the optional sub-sampling stage is then cropped after the
+   conversion of the analogue pixel values in the digital domain.
+
+4. The resulting output frame is sent on the media bus by the sensor.
+
+Camera Sensor configuration parameters
+--------------------------------------
+
+The libcamera camera sensor model defines parameters that allow users to
+control:
+
+1. The image format bit depth
+
+2. The size and position of the  *Analog crop rectangle*
+
+3. The subsampling factors used to downscale the pixel array readout data to a
+   smaller frame size without reducing the image *field of view*. Two
+   configuration parameters are made available to control the downscaling
+   factor:
+
+   - binning
+      A vertical and horizontal binning factor can be specified, the image
+      will be downscaled in its vertical and horizontal sizes by the specified
+      factor.
+
+      .. code-block:: c
+         :caption: Definition: The horizontal and vertical binning factors
+
+         horizontal_binning = xBin;
+         vertical_binning = yBin;
+
+   - skipping
+      Skipping reduces the image resolution by skipping the read-out of a number
+      of adjacent pixels. The skipping factor is specified by the 'increment'
+      number (number of pixels to 'skip') in the vertical and horizontal
+      directions and for even and odd rows and columns.
+
+      .. code-block:: c
+         :caption: Definition: The horizontal and vertical skipping factors
+
+         horizontal_skipping = (xOddInc + xEvenInc) / 2;
+         vertical_skipping = (yOddInc + yEvenInc) / 2;
+
+   Different sensors perform the binning and skipping stages in different
+   orders. For the sake of computing the final output image size the order of
+   execution is not relevant. The overall down-scaling factor is obtained by
+   combining the binning and skipping factors.
+
+   .. code-block:: c
+      :caption: Definition: The total scaling factor (binning + sub-sampling)
+
+      total_horizontal_downscale = horizontal_binning + horizontal_skipping;
+      total_vertical_downscale = vertical_binning + vertical_skipping;
+
+
+4. The output size is used to specify any additional cropping on the sub-sampled
+   frame.
+
+5. The total line length and frame height (*visibile* pixels + *blankings*) as
+   sent on the MIPI CSI-2 bus.
+
+6. The pixel transmission rate on the MIPI CSI-2 bus.
+
+The above parameters are combined to obtain the following high-level
+configurations:
+
+- **frame output size**
+
+   Obtained by applying a crop to the physical pixel array size in the analog
+   domain, followed by optional binning and sub-sampling (in any order),
+   followed by an optional crop step in the output digital domain.
+
+- **frame rate**
+
+   The combination of the *total frame size*, the image format *bit depth* and
+   the *pixel rate* of the data sent on the MIPI CSI-2 bus allows to compute the
+   image stream frame rate. The equation is the well known:
+
+   .. code-block:: c
+
+      frame_duration = total_frame_size / pixel_rate;
+      frame_rate = 1 / frame_duration;
+
+
+   where the *pixel_rate* parameter is the result of the sensor's configuration
+   of the MIPI CSI-2 bus *(the following formula applies to MIPI CSI-2 when
+   used on MIPI D-PHY physical protocol layer only)*
+
+   .. code-block:: c
+
+      pixel_rate = csi_2_link_freq * 2 * nr_of_lanes / bits_per_sample;
diff --git a/Documentation/code-of-conduct.rst b/Documentation/code-of-conduct.rst
new file mode 100644
index 0000000..38b7d7a
--- /dev/null
+++ b/Documentation/code-of-conduct.rst
@@ -0,0 +1,94 @@
+.. SPDX-License-Identifier: CC-BY-4.0
+
+.. _code-of-conduct:
+
+Contributor Covenant Code of Conduct
+====================================
+
+Our Pledge
+----------
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to make participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, sex characteristics, gender identity and expression,
+level of experience, education, socio-economic status, nationality, personal
+appearance, race, religion, or sexual identity and orientation.
+
+Our Standards
+-------------
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+  advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+  address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+  professional setting
+
+Our Responsibilities
+--------------------
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+Scope
+-----
+
+This Code of Conduct applies within all project spaces, and it also applies when
+an individual is representing the project or its community in public spaces.
+Examples of representing a project or community include using an official
+project e-mail address, posting via an official social media account, or acting
+as an appointed representative at an online or offline event. Representation of
+a project may be further defined and clarified by project maintainers.
+
+Enforcement
+-----------
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at conduct@libcamera.org, or directly to
+any member of the code of conduct team:
+
+* Kieran Bingham <kieran.bingham@ideasonboard.com>
+* Laurent Pinchart <laurent.pinchart@ideasonboard.com>
+
+All complaints will be reviewed and investigated and will result in a response
+that is deemed necessary and appropriate to the circumstances. The project team
+is obligated to maintain confidentiality with regard to the reporter of an
+incident. Further details of specific enforcement policies may be posted
+separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+Attribution
+-----------
+
+This Code of Conduct is adapted from the `Contributor Covenant`_, version 1.4,
+available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
+
+.. _Contributor Covenant: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see
+https://www.contributor-covenant.org/faq
+
diff --git a/Documentation/coding-style.rst b/Documentation/coding-style.rst
index 053fdd9..72cb28d 100644
--- a/Documentation/coding-style.rst
+++ b/Documentation/coding-style.rst
@@ -59,7 +59,7 @@
   underscores in between
 * All formatting rules specified in the selected sections of the Linux kernel
   Code Style for indentation, braces, spacing, etc
-* Header guards are formatted as '__LIBCAMERA_FILE_NAME_H__'
+* Headers are guarded by the use of '#pragma once'
 
 Order of Includes
 ~~~~~~~~~~~~~~~~~
diff --git a/Documentation/contributing.rst b/Documentation/contributing.rst
index 2f0b492..18b1914 100644
--- a/Documentation/contributing.rst
+++ b/Documentation/contributing.rst
@@ -8,6 +8,10 @@
 new features, or just discussing the project with the community, you can join
 our official public communication channels, or simply check out the code.
 
+The project adheres to a :ref:`code of conduct <code-of-conduct>` that
+maintainers, contributors and community members are expected to follow in all
+online and offline communication.
+
 Mailing List
 ------------
 
@@ -134,4 +138,5 @@
 .. toctree::
    :hidden:
 
+   Code of Conduct <code-of-conduct>
    Coding Style <coding-style>
diff --git a/Documentation/environment_variables.rst b/Documentation/environment_variables.rst
index a9b230b..4e9fbb2 100644
--- a/Documentation/environment_variables.rst
+++ b/Documentation/environment_variables.rst
@@ -37,6 +37,14 @@
 
    Example value: ``${HOME}/.libcamera/lib:/opt/libcamera/vendor/lib``
 
+LIBCAMERA_PIPELINES_MATCH_LIST
+   Define an ordered list of pipeline names to be used to match the media
+   devices in the system. The pipeline handler names used to populate the
+   variable are the ones passed to the REGISTER_PIPELINE_HANDLER() macro in the
+   source code.
+
+   Example value: ``rkisp1,simple``
+
 LIBCAMERA_RPI_CONFIG_FILE
    Define a custom configuration file to use in the Raspberry Pi pipeline handler.
 
diff --git a/Documentation/guides/application-developer.rst b/Documentation/guides/application-developer.rst
index c46d336..92e2a37 100644
--- a/Documentation/guides/application-developer.rst
+++ b/Documentation/guides/application-developer.rst
@@ -116,19 +116,21 @@
 
 .. code:: cpp
 
-   if (cm->cameras().empty()) {
+   auto cameras = cm->cameras();
+   if (cameras.empty()) {
        std::cout << "No cameras were identified on the system."
                  << std::endl;
        cm->stop();
        return EXIT_FAILURE;
    }
 
-   std::string cameraId = cm->cameras()[0]->id();
-   camera = cm->get(cameraId);
+   std::string cameraId = cameras[0]->id();
 
+   auto camera = cm->get(cameraId);
    /*
-    * Note that is equivalent to:
-    * camera = cm->cameras()[0];
+    * Note that `camera` may not compare equal to `cameras[0]`.
+    * In fact, it might simply be a `nullptr`, as the particular
+    * device might have disappeared (and reappeared) in the meantime.
     */
 
 Once a camera has been selected an application needs to acquire an exclusive
@@ -348,7 +350,7 @@
 Signals and Slots`_) to connect events with callbacks to handle them.
 
 .. _signals and slots: https://libcamera.org/api-html/classlibcamera_1_1Signal.html#details
-.. _Qt Signals and Slots: https://doc.qt.io/qt-5/signalsandslots.html
+.. _Qt Signals and Slots: https://doc.qt.io/qt-6/signalsandslots.html
 
 The ``Camera`` device emits two signals that applications can connect to in
 order to execute callbacks on frame completion events.
diff --git a/Documentation/guides/pipeline-handler.rst b/Documentation/guides/pipeline-handler.rst
index 10b9c75..7e45cdb 100644
--- a/Documentation/guides/pipeline-handler.rst
+++ b/Documentation/guides/pipeline-handler.rst
@@ -258,7 +258,7 @@
           return false;
    }
 
-   REGISTER_PIPELINE_HANDLER(PipelineHandlerVivid)
+   REGISTER_PIPELINE_HANDLER(PipelineHandlerVivid, "vivid")
 
    } /* namespace libcamera */
 
@@ -266,6 +266,8 @@
 handler factory using the `REGISTER_PIPELINE_HANDLER`_ macro which
 registers it and creates a global symbol to reference the class and make it
 available to try and match devices.
+String "vivid" is the name assigned to the pipeline, matching the pipeline
+subdirectory name in the source tree.
 
 .. _REGISTER_PIPELINE_HANDLER: https://libcamera.org/api-html/pipeline__handler_8h.html
 
@@ -587,12 +589,12 @@
 
 The libcamera controls and properties are defined in YAML form which is
 processed to automatically generate documentation and interfaces. Controls are
-defined by the src/libcamera/`control_ids.yaml`_ file and camera properties
-are defined by src/libcamera/`properties_ids.yaml`_.
+defined by the src/libcamera/`control_ids_core.yaml`_ file and camera properties
+are defined by src/libcamera/`properties_ids_core.yaml`_.
 
 .. _controls framework: https://libcamera.org/api-html/controls_8h.html
-.. _control_ids.yaml: https://libcamera.org/api-html/control__ids_8h.html
-.. _properties_ids.yaml: https://libcamera.org/api-html/property__ids_8h.html
+.. _control_ids_core.yaml: https://libcamera.org/api-html/control__ids_8h.html
+.. _properties_ids_core.yaml: https://libcamera.org/api-html/property__ids_8h.html
 
 Pipeline handlers can optionally register the list of controls an application
 can set as well as a list of immutable camera properties. Being both
@@ -651,7 +653,7 @@
            ctrls.emplace(id, info);
    }
 
-   controlInfo_ = std::move(ctrls);
+   controlInfo_ = ControlInfoMap(std::move(ctrls), controls::controls);
 
 The ``properties_`` field is  a list of ``ControlId`` instances
 associated with immutable values, which represent static characteristics that can
@@ -672,6 +674,58 @@
    #include <libcamera/controls.h>
    #include <libcamera/control_ids.h>
 
+Vendor-specific controls and properties
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Vendor-specific controls and properties must be defined in a separate YAML file
+and included in the build by defining the pipeline handler to file mapping in
+include/libcamera/meson.build. These YAML files live in the src/libcamera
+directory.
+
+For example, adding a Raspberry Pi vendor control file for the PiSP pipeline
+handler is done with the following mapping:
+
+.. code-block:: meson
+
+   controls_map = {
+      'controls': {
+         'draft': 'control_ids_draft.yaml',
+         'libcamera': 'control_ids_core.yaml',
+         'rpi/pisp': 'control_ids_rpi.yaml',
+      },
+
+      'properties': {
+         'draft': 'property_ids_draft.yaml',
+         'libcamera': 'property_ids_core.yaml',
+      }
+   }
+
+The pipeline handler named above must match the pipeline handler option string
+specified in the meson build configuration.
+
+Vendor-specific controls and properties must contain a `vendor: <vendor_string>`
+tag in the YAML file. Every unique vendor tag must define a unique and
+non-overlapping range of reserved control IDs in src/libcamera/control_ranges.yaml.
+
+For example, the following block defines a vendor-specific control with the
+`rpi` vendor tag:
+
+.. code-block:: yaml
+
+    vendor: rpi
+    controls:
+      - PispConfigDumpFile:
+          type: string
+          description: |
+            Triggers the Raspberry Pi PiSP pipeline handler to generate a JSON
+            formatted dump of the Backend configuration to the filename given by the
+            value of the control.
+
+The controls will be generated in the vendor-specific namespace
+`libcamera::controls::rpi`. Additionally a `#define
+LIBCAMERA_HAS_RPI_VENDOR_CONTROLS` will be available to allow applications to
+test for the availability of these controls.
+
 Generating a default configuration
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1370,7 +1424,7 @@
 of the libcamera implementation is available in the `libcamera Signal and Slot`_
 classes documentation.
 
-.. _Qt Signals and Slots: https://doc.qt.io/qt-5/signalsandslots.html
+.. _Qt Signals and Slots: https://doc.qt.io/qt-6/signalsandslots.html
 .. _libcamera Signal and Slot: https://libcamera.org/api-html/classlibcamera_1_1Signal.html#details
 
 In order to notify applications about the availability of new frames and data,
diff --git a/Documentation/images/rotation/rotate0.svg b/Documentation/images/rotation/rotate0.svg
new file mode 100644
index 0000000..13cde16
--- /dev/null
+++ b/Documentation/images/rotation/rotate0.svg
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="rotate0.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     showguides="false"
+     inkscape:zoom="1.4854147"
+     inkscape:cx="666.48052"
+     inkscape:cy="448.35962"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458334"
+       spacingy="0.26458333"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;paint-order:markers stroke fill;stroke-dasharray:none"
+       id="rect1"
+       width="152.88184"
+       height="119.41136"
+       x="77.237244"
+       y="81.982094" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;paint-order:markers stroke fill;stroke-dasharray:none"
+       id="rect2"
+       width="49.755535"
+       height="36.468258"
+       x="92.612343"
+       y="98.912964" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2-5"
+       width="49.755535"
+       height="36.468258"
+       x="167.25099"
+       y="98.912964" />
+    <g
+       id="g4"
+       transform="translate(-0.98582077)"
+       style="stroke-width:1.5875;stroke-dasharray:none">
+      <rect
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="rect3"
+         width="40.994682"
+         height="43.605846"
+         x="134.16664"
+         y="157.24184" />
+      <ellipse
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="path3"
+         cx="140.15703"
+         cy="176.44627"
+         rx="1.889045"
+         ry="1.925626" />
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="M 244.95942,81.765726 62.444825,81.97209 154.25639,28.65633 Z"
+       id="path4"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
+       d="m 199.76751,33.368887 0.0285,21.581353"
+       id="path5" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
+       d="m 215.59016,33.189206 0.0959,31.330304"
+       id="path6" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 194.42835,33.189356 25.2821,-0.220612"
+       id="path7" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;stroke-dasharray:none"
+       d="m 195.19248,33.096339 -0.0701,-5.375793 23.77787,-0.05613 0.0553,5.315811"
+       id="path8" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 194.20874,25.616264 25.25485,-0.02536"
+       id="path7-5"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 195.03436,26.298566 -0.0455,-5.426692 23.77787,-0.05613 0.0553,5.315811"
+       id="path8-9"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/Documentation/images/rotation/rotate0Mirror.svg b/Documentation/images/rotation/rotate0Mirror.svg
new file mode 100644
index 0000000..a7edda8
--- /dev/null
+++ b/Documentation/images/rotation/rotate0Mirror.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="rotate0Mirror.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     showguides="false"
+     inkscape:zoom="0.82900578"
+     inkscape:cx="599.51331"
+     inkscape:cy="579.00682"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458334"
+       spacingy="0.26458333"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect1"
+       width="152.88184"
+       height="119.41136"
+       x="-230.13463"
+       y="81.982094"
+       transform="scale(-1,1)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2"
+       width="49.755535"
+       height="36.468258"
+       x="-214.75954"
+       y="98.912964"
+       transform="scale(-1,1)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2-5"
+       width="49.755535"
+       height="36.468258"
+       x="-140.12088"
+       y="98.912964"
+       transform="scale(-1,1)" />
+    <g
+       id="g4"
+       transform="matrix(-1,0,0,1,308.35769,0)"
+       style="stroke-width:1.5875;stroke-dasharray:none">
+      <rect
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="rect3"
+         width="40.994682"
+         height="43.605846"
+         x="134.16664"
+         y="157.24184" />
+      <ellipse
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="path3"
+         cx="140.15703"
+         cy="176.44627"
+         rx="1.889045"
+         ry="1.925626" />
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="M 62.412454,81.765726 244.92705,81.97209 153.11548,28.65633 Z"
+       id="path4"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 107.60436,33.368887 -0.0285,21.581353"
+       id="path5" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 91.781714,33.189206 -0.0959,31.330304"
+       id="path6" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="M 112.94352,33.189356 87.661424,32.968744"
+       id="path7" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 112.17939,33.096339 0.0701,-5.375793 -23.777866,-0.05613 -0.0553,5.315811"
+       id="path8" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="M 113.16313,25.616264 87.908284,25.590904"
+       id="path7-5"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 112.33751,26.298566 0.0455,-5.426692 -23.777866,-0.05613 -0.0553,5.315811"
+       id="path8-9"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/Documentation/images/rotation/rotate180.svg b/Documentation/images/rotation/rotate180.svg
new file mode 100644
index 0000000..d092a53
--- /dev/null
+++ b/Documentation/images/rotation/rotate180.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="rotate180.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     showguides="false"
+     inkscape:zoom="0.94272086"
+     inkscape:cx="467.79489"
+     inkscape:cy="423.24299"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458334"
+       spacingy="0.26458333"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect1"
+       width="152.88184"
+       height="119.41136"
+       x="-230.13461"
+       y="-140.22527"
+       transform="scale(-1)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2"
+       width="49.755535"
+       height="36.468258"
+       x="-214.75951"
+       y="-123.2944"
+       transform="scale(-1)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2-5"
+       width="49.755535"
+       height="36.468258"
+       x="-140.12086"
+       y="-123.2944"
+       transform="scale(-1)" />
+    <g
+       id="g4"
+       transform="rotate(180,154.17884,111.10368)"
+       style="stroke-width:1.5875;stroke-dasharray:none">
+      <rect
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="rect3"
+         width="40.994682"
+         height="43.605846"
+         x="134.16664"
+         y="157.24184" />
+      <ellipse
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="path3"
+         cx="140.15703"
+         cy="176.44627"
+         rx="1.889045"
+         ry="1.925626" />
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 62.412437,140.44163 182.514593,-0.20636 -91.81156,53.31576 z"
+       id="path4"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 107.60435,188.83847 -0.0285,-21.58135"
+       id="path5" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 91.781697,189.01815 -0.0959,-31.3303"
+       id="path6" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 112.94351,189.018 -25.282103,0.22061"
+       id="path7" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 112.17938,189.11102 0.0701,5.37579 -23.777873,0.0561 -0.0553,-5.31581"
+       id="path8" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 113.16312,196.59109 -25.254853,0.0254"
+       id="path7-5"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 112.3375,195.90879 0.0455,5.42669 -23.777873,0.0561 -0.0553,-5.31581"
+       id="path8-9"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/Documentation/images/rotation/rotate180Mirror.svg b/Documentation/images/rotation/rotate180Mirror.svg
new file mode 100644
index 0000000..d4a77d5
--- /dev/null
+++ b/Documentation/images/rotation/rotate180Mirror.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="rotate180Mirror.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     showguides="false"
+     inkscape:zoom="0.94272086"
+     inkscape:cx="467.79489"
+     inkscape:cy="423.24299"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458334"
+       spacingy="0.26458333"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect1"
+       width="152.88184"
+       height="119.41136"
+       x="77.237228"
+       y="-140.22527"
+       transform="scale(1,-1)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2"
+       width="49.755535"
+       height="36.468258"
+       x="92.612335"
+       y="-123.2944"
+       transform="scale(1,-1)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2-5"
+       width="49.755535"
+       height="36.468258"
+       x="167.25098"
+       y="-123.2944"
+       transform="scale(1,-1)" />
+    <g
+       id="g4"
+       transform="matrix(1,0,0,-1,-0.98584226,222.20736)"
+       style="stroke-width:1.5875;stroke-dasharray:none">
+      <rect
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="rect3"
+         width="40.994682"
+         height="43.605846"
+         x="134.16664"
+         y="157.24184" />
+      <ellipse
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="path3"
+         cx="140.15703"
+         cy="176.44627"
+         rx="1.889045"
+         ry="1.925626" />
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="M 244.9594,140.44163 62.444808,140.23527 154.25637,193.55103 Z"
+       id="path4"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 199.76749,188.83847 0.0285,-21.58135"
+       id="path5" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 215.59014,189.01815 0.0959,-31.3303"
+       id="path6" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 194.42833,189.018 25.2821,0.22061"
+       id="path7" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 195.19246,189.11102 -0.0701,5.37579 23.77787,0.0561 0.0553,-5.31581"
+       id="path8" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 194.20872,196.59109 25.25485,0.0254"
+       id="path7-5"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 195.03434,195.90879 -0.0455,5.42669 23.77787,0.0561 0.0553,-5.31581"
+       id="path8-9"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/Documentation/images/rotation/rotate270.svg b/Documentation/images/rotation/rotate270.svg
new file mode 100644
index 0000000..13ea1e5
--- /dev/null
+++ b/Documentation/images/rotation/rotate270.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="rotate270.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     showguides="false"
+     inkscape:zoom="0.94272086"
+     inkscape:cx="467.26451"
+     inkscape:cy="423.24299"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458334"
+       spacingy="0.26458333"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect1"
+       width="152.88184"
+       height="119.41136"
+       x="-187.55237"
+       y="124.56432"
+       transform="rotate(-90)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2"
+       width="49.755535"
+       height="36.468258"
+       x="-172.17726"
+       y="141.49518"
+       transform="rotate(-90)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2-5"
+       width="49.755535"
+       height="36.468258"
+       x="-97.538612"
+       y="141.49518"
+       transform="rotate(-90)" />
+    <g
+       id="g4"
+       transform="rotate(-90,154.17883,111.5966)"
+       style="stroke-width:1.5875;stroke-dasharray:none">
+      <rect
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="rect3"
+         width="40.994682"
+         height="43.605846"
+         x="134.16664"
+         y="157.24184" />
+      <ellipse
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="path3"
+         cx="140.15703"
+         cy="176.44627"
+         rx="1.889045"
+         ry="1.925626" />
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="M 124.34796,19.830188 124.55432,202.34478 71.238559,110.53322 Z"
+       id="path4"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 75.951119,65.022101 21.58135,-0.0285"
+       id="path5" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 75.771439,49.199448 31.330301,-0.0959"
+       id="path6" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="M 75.771589,70.361261 75.550979,45.079158"
+       id="path7" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 75.678569,69.597131 -5.37579,0.0701 -0.0561,-23.777873 5.31581,-0.0553"
+       id="path8" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 68.198499,70.580871 -0.0254,-25.254853"
+       id="path7-5"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 68.880799,69.755251 -5.42669,0.0455 -0.0561,-23.777873 5.31581,-0.0553"
+       id="path8-9"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/Documentation/images/rotation/rotate270Mirror.svg b/Documentation/images/rotation/rotate270Mirror.svg
new file mode 100644
index 0000000..6116f50
--- /dev/null
+++ b/Documentation/images/rotation/rotate270Mirror.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="rotate270Mirror.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     showguides="false"
+     inkscape:zoom="0.94272086"
+     inkscape:cx="467.79489"
+     inkscape:cy="423.24299"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458334"
+       spacingy="0.26458333"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect1"
+       width="152.88184"
+       height="119.41136"
+       x="-187.55237"
+       y="-182.80751"
+       transform="matrix(0,-1,-1,0,0,0)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2"
+       width="49.755535"
+       height="36.468258"
+       x="-172.17726"
+       y="-165.87666"
+       transform="matrix(0,-1,-1,0,0,0)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2-5"
+       width="49.755535"
+       height="36.468258"
+       x="-97.538612"
+       y="-165.87666"
+       transform="matrix(0,-1,-1,0,0,0)" />
+    <g
+       id="g4"
+       transform="matrix(0,-1,-1,0,264.78961,265.77543)"
+       style="stroke-width:1.5875;stroke-dasharray:none">
+      <rect
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="rect3"
+         width="40.994682"
+         height="43.605846"
+         x="134.16664"
+         y="157.24184" />
+      <ellipse
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="path3"
+         cx="140.15703"
+         cy="176.44627"
+         rx="1.889045"
+         ry="1.925626" />
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 183.02388,19.830188 -0.20636,182.514592 53.31576,-91.81156 z"
+       id="path4"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 231.42072,65.022101 -21.58135,-0.0285"
+       id="path5" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 231.6004,49.199448 -31.3303,-0.0959"
+       id="path6" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 231.60025,70.361261 0.22061,-25.282103"
+       id="path7" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 231.69327,69.597131 5.37579,0.0701 0.0561,-23.777873 -5.31581,-0.0553"
+       id="path8" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 239.17334,70.580871 0.0254,-25.254853"
+       id="path7-5"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 238.49104,69.755251 5.42669,0.0455 0.0561,-23.777873 -5.31581,-0.0553"
+       id="path8-9"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/Documentation/images/rotation/rotate90.svg b/Documentation/images/rotation/rotate90.svg
new file mode 100644
index 0000000..af62763
--- /dev/null
+++ b/Documentation/images/rotation/rotate90.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="rotate90.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     showguides="false"
+     inkscape:zoom="0.94272086"
+     inkscape:cx="467.26451"
+     inkscape:cy="423.24299"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458334"
+       spacingy="0.26458333"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect1"
+       width="152.88184"
+       height="119.41136"
+       x="34.65498"
+       y="-182.80751"
+       transform="rotate(90)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2"
+       width="49.755535"
+       height="36.468258"
+       x="50.030079"
+       y="-165.87665"
+       transform="rotate(90)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2-5"
+       width="49.755535"
+       height="36.468258"
+       x="124.66872"
+       y="-165.87665"
+       transform="rotate(90)" />
+    <g
+       id="g4"
+       transform="rotate(90,154.17885,110.61076)"
+       style="stroke-width:1.5875;stroke-dasharray:none">
+      <rect
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="rect3"
+         width="40.994682"
+         height="43.605846"
+         x="134.16664"
+         y="157.24184" />
+      <ellipse
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="path3"
+         cx="140.15703"
+         cy="176.44627"
+         rx="1.889045"
+         ry="1.925626" />
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 183.02388,202.37715 -0.20636,-182.51459 53.31576,91.81156 z"
+       id="path4"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 231.42072,157.18524 -21.58135,0.0285"
+       id="path5" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 231.6004,173.00789 -31.3303,0.0959"
+       id="path6" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 231.60025,151.84608 0.22061,25.2821"
+       id="path7" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 231.69327,152.61021 5.37579,-0.0701 0.0561,23.77787 -5.31581,0.0553"
+       id="path8" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 239.17334,151.62647 0.0254,25.25485"
+       id="path7-5"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 238.49104,152.45209 5.42669,-0.0455 0.0561,23.77787 -5.31581,0.0553"
+       id="path8-9"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/Documentation/images/rotation/rotate90Mirror.svg b/Documentation/images/rotation/rotate90Mirror.svg
new file mode 100644
index 0000000..1760c46
--- /dev/null
+++ b/Documentation/images/rotation/rotate90Mirror.svg
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="rotate90Mirror.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     showguides="false"
+     inkscape:zoom="0.94272086"
+     inkscape:cx="467.79489"
+     inkscape:cy="423.24299"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458334"
+       spacingy="0.26458333"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1" />
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect1"
+       width="152.88184"
+       height="119.41136"
+       x="34.65498"
+       y="124.56432"
+       transform="matrix(0,1,1,0,0,0)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2"
+       width="49.755535"
+       height="36.468258"
+       x="50.030079"
+       y="141.49519"
+       transform="matrix(0,1,1,0,0,0)" />
+    <rect
+       style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="rect2-5"
+       width="49.755535"
+       height="36.468258"
+       x="124.66872"
+       y="141.49519"
+       transform="matrix(0,1,1,0,0,0)" />
+    <g
+       id="g4"
+       transform="matrix(0,1,1,0,42.582224,-43.56809)"
+       style="stroke-width:1.5875;stroke-dasharray:none">
+      <rect
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="rect3"
+         width="40.994682"
+         height="43.605846"
+         x="134.16664"
+         y="157.24184" />
+      <ellipse
+         style="fill:none;stroke:#000000;stroke-width:1.5875;stroke-dasharray:none;paint-order:markers stroke fill"
+         id="path3"
+         cx="140.15703"
+         cy="176.44627"
+         rx="1.889045"
+         ry="1.925626" />
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="M 124.34795,202.37715 124.55431,19.86256 71.238554,111.67412 Z"
+       id="path4"
+       sodipodi:nodetypes="cccc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 75.951114,157.18524 21.58135,0.0285"
+       id="path5" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 75.771434,173.00789 31.330296,0.0959"
+       id="path6" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 75.771584,151.84608 -0.22061,25.2821"
+       id="path7" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 75.678564,152.61021 -5.37579,-0.0701 -0.0561,23.77787 5.31581,0.0553"
+       id="path8" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 68.198494,151.62647 -0.0254,25.25485"
+       id="path7-5"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5875;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
+       d="m 68.880794,152.45209 -5.42669,-0.0455 -0.0561,23.77787 5.31581,0.0553"
+       id="path8-9"
+       sodipodi:nodetypes="cccc" />
+  </g>
+</svg>
diff --git a/Documentation/index.rst b/Documentation/index.rst
index 43d8b01..5442ae7 100644
--- a/Documentation/index.rst
+++ b/Documentation/index.rst
@@ -23,3 +23,5 @@
    Sensor driver requirements <sensor_driver_requirements>
    Lens driver requirements <lens_driver_requirements>
    Python Bindings <python-bindings>
+   Camera Sensor Model <camera-sensor-model>
+   SoftwareISP Benchmarking <software-isp-benchmarking>
diff --git a/Documentation/meson.build b/Documentation/meson.build
index b2a5bf1..30d3952 100644
--- a/Documentation/meson.build
+++ b/Documentation/meson.build
@@ -15,6 +15,7 @@
     cdata.set('TOP_SRCDIR', meson.project_source_root())
     cdata.set('TOP_BUILDDIR', meson.project_build_root())
     cdata.set('OUTPUT_DIR', meson.current_build_dir())
+    cdata.set('WARN_AS_ERROR', get_option('doc_werror') ? 'YES' : 'NO')
 
     doxygen_predefined = []
     foreach key : config_h.keys()
@@ -49,7 +50,8 @@
                   output : 'api-html',
                   command : [doxygen, doxyfile],
                   install : true,
-                  install_dir : doc_install_dir)
+                  install_dir : doc_install_dir,
+                  install_tag : 'doc')
 endif
 
 #
@@ -63,6 +65,8 @@
 
 if sphinx.found()
     docs_sources = [
+        'camera-sensor-model.rst',
+        'code-of-conduct.rst',
         'coding-style.rst',
         'conf.py',
         'contributing.rst',
@@ -77,6 +81,7 @@
         'lens_driver_requirements.rst',
         'python-bindings.rst',
         'sensor_driver_requirements.rst',
+        'software-isp-benchmarking.rst',
        '../README.rst',
     ]
 
@@ -89,7 +94,8 @@
                   output : 'html',
                   build_by_default : true,
                   install : true,
-                  install_dir : doc_install_dir)
+                  install_dir : doc_install_dir,
+                  install_tag : 'doc')
 
     custom_target('documentation-linkcheck',
                   command : [sphinx, '-W', '-b', 'linkcheck', meson.current_source_dir(), '@OUTPUT@'],
diff --git a/Documentation/python-bindings.rst b/Documentation/python-bindings.rst
index bac5cd3..ed9f686 100644
--- a/Documentation/python-bindings.rst
+++ b/Documentation/python-bindings.rst
@@ -17,13 +17,13 @@
 Mostly these differences fall under two categories:
 
 1. Differences caused by the inherent differences between C++ and Python.
-These differences are usually caused by the use of threads or differences in
-C++ vs Python memory management.
+   These differences are usually caused by the use of threads or differences in
+   C++ vs Python memory management.
 
 2. Differences caused by the code being work-in-progress. It's not always
-trivial to create a binding in a satisfying way, and the current bindings
-contain simplified versions of the C++ API just to get forward. These
-differences are expected to eventually go away.
+   trivial to create a binding in a satisfying way, and the current bindings
+   contain simplified versions of the C++ API just to get forward. These
+   differences are expected to eventually go away.
 
 Coding Style
 ------------
diff --git a/Documentation/sensor_model.svg b/Documentation/sensor_model.svg
new file mode 100644
index 0000000..02dc55a
--- /dev/null
+++ b/Documentation/sensor_model.svg
@@ -0,0 +1,4870 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg5"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="sensor_model.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview7"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     inkscape:zoom="0.55815145"
+     inkscape:cx="535.6969"
+     inkscape:cy="472.9899"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="layer1"
+     showguides="false">
+    <inkscape:grid
+       type="xygrid"
+       id="grid184"
+       originx="0"
+       originy="0"
+       spacingy="1"
+       spacingx="1"
+       units="mm"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs2">
+    <marker
+       style="overflow:visible"
+       id="marker9374"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="TriangleStart"
+       markerWidth="5.32440803"
+       markerHeight="6.15538502"
+       viewBox="0 0 5.3244081 6.1553851"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         transform="scale(0.5)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 5.77,0 -2.88,5 V -5 Z"
+         id="path9372" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="TriangleStart"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="TriangleStart"
+       markerWidth="5.3244081"
+       markerHeight="6.155385"
+       viewBox="0 0 5.3244081 6.1553851"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         transform="scale(0.5)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 5.77,0 -2.88,5 V -5 Z"
+         id="path135" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="marker9374-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="TriangleStart"
+       markerWidth="5.3244081"
+       markerHeight="6.155385"
+       viewBox="0 0 5.3244081 6.1553851"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         transform="scale(0.5)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 5.77,0 -2.88,5 V -5 Z"
+         id="path9372-6" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="TriangleStart-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="TriangleStart"
+       markerWidth="5.3244081"
+       markerHeight="6.155385"
+       viewBox="0 0 5.3244081 6.1553851"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         transform="scale(0.5)"
+         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
+         d="M 5.77,0 -2.88,5 V -5 Z"
+         id="path135-6" />
+    </marker>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g1052"
+       transform="translate(23.655977)"
+       style="opacity:0.5"
+       inkscape:export-filename="Documentation/camera-sensor-model_digiscale.png"
+       inkscape:export-xdpi="90"
+       inkscape:export-ydpi="90">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-2"
+       transform="translate(13.062138,0.0898069)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-0"
+       transform="translate(44.963283,-0.03138127)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-93"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-26"
+       transform="translate(34.280458,-0.02885923)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-20"
+       transform="translate(66.243325,-0.12103306)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-75"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-92"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-28"
+       transform="translate(55.53615,-0.0663742)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-97"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-36"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9"
+       transform="translate(76.887707,-0.13032417)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-206"
+       transform="translate(2.5889069,0.05636587)"
+       style="opacity:0.502088">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-15"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-47"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-65"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-6"
+       transform="translate(23.672688,10.516298)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-937"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-25"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-2-4"
+       transform="translate(13.078849,10.606105)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-9-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-1-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-2-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-7-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-0-0"
+       transform="translate(44.979994,10.484917)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-93-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-6-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-0-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-6-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-26-8"
+       transform="translate(34.297169,10.487439)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-1-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-8-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-7-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-9-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-20-9"
+       transform="translate(66.260036,10.395265)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-2-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-3-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-75-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-92-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-28-9"
+       transform="translate(55.552861,10.449924)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-97-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-36-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-1-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-2-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-9"
+       transform="translate(76.904418,10.385974)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-206-7"
+       transform="translate(2.6056179,10.572664)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-15-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-5-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-47-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-65-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-22"
+       transform="translate(23.70412,21.058784)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-10"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-2-5"
+       transform="translate(13.110281,21.148591)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-9-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-1-49"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-2-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-7-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-0-1"
+       transform="translate(45.011426,21.027403)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-93-77"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-6-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-0-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-6-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-26-9"
+       transform="translate(34.328601,21.029925)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-1-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-8-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-7-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-9-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-20-3"
+       transform="translate(66.291468,20.937751)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-2-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-3-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-75-63"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-92-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-28-4"
+       transform="translate(55.584293,20.99241)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-97-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-36-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-1-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-2-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-3"
+       transform="translate(76.93585,20.92846)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-08"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-206-0"
+       transform="translate(2.6370499,21.11515)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-15-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-5-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-47-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-65-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-5"
+       transform="translate(23.740609,31.433065)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-61"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-15"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-2-48"
+       transform="translate(13.14677,31.522872)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-9-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-1-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-2-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-7-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-0-4"
+       transform="translate(45.047915,31.401684)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-93-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-6-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-0-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-6-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-26-6"
+       transform="translate(34.36509,31.404206)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-1-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-8-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-7-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-9-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-20-96"
+       transform="translate(66.327957,31.312032)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-2-21"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-3-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-75-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-92-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-28-7"
+       transform="translate(55.620782,31.366691)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-97-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-36-18"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-1-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-2-97"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-5"
+       transform="translate(76.972339,31.302741)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-83"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-206-8"
+       transform="translate(2.6735389,31.489431)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-15-96"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-5-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-47-33"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-65-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-8"
+       transform="translate(23.725354,41.251089)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-60"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-48"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-89"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-2-7"
+       transform="translate(13.131515,41.340896)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-9-76"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-1-43"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-2-03"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-7-09"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-0-2"
+       transform="translate(45.03266,41.219708)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-93-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-6-40"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-0-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-6-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-26-4"
+       transform="translate(34.349835,41.22223)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-1-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-8-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-7-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-9-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-20-4"
+       transform="translate(66.312702,41.130056)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-2-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-3-75"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-75-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-92-81"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-28-2"
+       transform="translate(55.605527,41.184715)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-97-89"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-36-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-1-68"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-2-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-2"
+       transform="translate(76.957084,41.120765)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-05"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-10"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-206-85"
+       transform="translate(2.5889069,41.307455)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-15-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-5-64"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-47-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-65-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-8-5"
+       transform="translate(23.730747,51.649149)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-60-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-48-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-8-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-89-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-2-7-4"
+       transform="translate(13.136908,51.738956)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-9-76-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-1-43-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-2-03-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-7-09-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-0-2-6"
+       transform="translate(45.038053,51.617768)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-93-5-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-6-40-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-0-5-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-6-9-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-26-4-8"
+       transform="translate(34.355228,51.62029)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-1-6-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-8-9-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-7-2-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-9-2-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-20-4-0"
+       transform="translate(66.318095,51.528116)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-2-7-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-3-75-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-75-4-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-92-81-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-28-2-9"
+       transform="translate(55.61092,51.582775)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-97-89-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-36-3-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-1-68-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-2-0-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <rect
+       style="opacity:0.5;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-31-1-3"
+       width="5.1440187"
+       height="5.0140224"
+       x="129.98665"
+       y="78.113022" />
+    <rect
+       style="opacity:0.5;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-5-9-05-8"
+       width="5.1440187"
+       height="5.0140224"
+       x="135.20201"
+       y="78.074852" />
+    <rect
+       style="opacity:0.5;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-3-4-1-0"
+       width="5.1440187"
+       height="5.0140224"
+       x="129.99101"
+       y="83.377647" />
+    <g
+       id="g1052-9-4"
+       transform="translate(87.507934,-0.18332859)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-9-5"
+       transform="translate(87.524645,10.332969)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-5-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-0-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-4-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-8-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-3-2"
+       transform="translate(87.556077,20.875455)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-9-8"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-08-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-8-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-5-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-5-6"
+       transform="translate(87.592566,31.249736)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-3-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-8-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-83-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-1-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-2-1"
+       transform="translate(87.577311,41.06776)"
+       style="opacity:1">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-1-32"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-05-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-1-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-10-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <rect
+       style="opacity:0.5;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-3-4-1-0-9"
+       width="5.1440187"
+       height="5.0140224"
+       x="140.61124"
+       y="83.324638" />
+    <rect
+       style="opacity:0.5;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-56-78-10-5"
+       width="5.1440187"
+       height="5.0140224"
+       x="135.21388"
+       y="83.3274" />
+    <rect
+       style="opacity:0.5;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-31-1-3-1"
+       width="5.1440187"
+       height="5.0140224"
+       x="140.60687"
+       y="77.967453" />
+    <g
+       id="g1052-9-9-7-22"
+       transform="translate(97.600308,0.08245514)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-5-4-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-0-99-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-4-4-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-8-5-41"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-3-9-6"
+       transform="translate(97.686888,10.559974)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-9-4-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-08-22-78"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-8-6-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-5-4-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-5-3-27"
+       transform="translate(97.723368,20.934255)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-3-3-36"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-8-7-47"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-83-3-9"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-1-2-7"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <g
+       id="g1052-9-2-8-2"
+       transform="translate(97.708118,30.752279)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-1-9-2"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-05-15-1"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-1-4-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-10-92-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <rect
+       style="opacity:0.5;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-5-9-05-8-4"
+       width="5.1440187"
+       height="5.0140224"
+       x="145.82224"
+       y="77.929283" />
+    <rect
+       style="opacity:0.5;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-56-78-10-5-9"
+       width="5.1440187"
+       height="5.0140224"
+       x="145.83411"
+       y="83.181831" />
+    <g
+       id="g1052-9-2-1-0"
+       transform="translate(97.884193,41.434681)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-31-1-32-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-05-1-3"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-9-05-1-3-5"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.074577"
+         y="37.001465" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-4-1-5-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-78-10-9-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <rect
+       style="opacity:0.5;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-3-4-1-0-9-6"
+       width="5.1440187"
+       height="5.0140224"
+       x="150.91812"
+       y="83.691559" />
+    <rect
+       style="opacity:0.5;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-31-1-3-1-0"
+       width="5.1440187"
+       height="5.0140224"
+       x="150.91376"
+       y="78.334373" />
+    <rect
+       style="opacity:0.5;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect288-56-78-10-5-9-2"
+       width="5.1440187"
+       height="5.0140224"
+       x="156.14098"
+       y="83.548752" />
+    <g
+       id="g1052-206-85-6"
+       transform="translate(2.5942999,51.705515)"
+       style="opacity:0.5">
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-15-0-6"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.024174"
+         y="26.5942" />
+      <rect
+         style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-5-5-64-4"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.23954"
+         y="26.556028" />
+      <rect
+         style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-3-47-6-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="53.028545"
+         y="31.858822" />
+      <rect
+         style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+         id="rect288-56-65-2-0"
+         width="5.1440187"
+         height="5.0140224"
+         x="58.251408"
+         y="31.808577" />
+    </g>
+    <rect
+       style="opacity:0.195644;fill:#000000;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect2827"
+       width="7.4583311"
+       height="61.587906"
+       x="47.724121"
+       y="26.798815" />
+    <rect
+       style="opacity:0.195644;fill:#000000;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect2829"
+       width="7.3512397"
+       height="61.724613"
+       x="161.43401"
+       y="26.690187" />
+    <rect
+       style="opacity:0.195644;fill:#000000;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect2831"
+       width="121.2337"
+       height="22.824373"
+       x="47.633518"
+       y="88.566978" />
+    <rect
+       style="opacity:0.195644;fill:#000000;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+       id="rect2833"
+       width="121.20582"
+       height="8.0138454"
+       x="47.77354"
+       y="18.34436" />
+    <rect
+       style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.8;stroke-dasharray:3.2, 0.8;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers stroke fill"
+       id="rect3461"
+       width="84.249771"
+       height="41.517296"
+       x="66.555992"
+       y="36.749054" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.865;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:1.73, 0.865;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 149.57761,77.267496 301.74652,88.379163"
+       id="path4533-3-7"
+       sodipodi:nodetypes="cc" />
+    <g
+       id="g4698"
+       transform="translate(55.540721,9.7505553)">
+      <g
+         id="g1052-9-9-7"
+         transform="translate(174.2039,11.30255)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-5-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-0-99"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-4-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-8-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-6-1"
+         transform="translate(121.02732,11.367907)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-4-7"
+         transform="translate(110.43348,11.457714)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-0-0-4"
+         transform="translate(142.33462,11.336526)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-93-7-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-6-8-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-0-6-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-6-8-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-8-0"
+         transform="translate(131.6518,11.339048)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-4-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-3-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-1-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-4-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-9-0"
+         transform="translate(163.61466,11.246874)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-2-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-0-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-6-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-8-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-9-0"
+         transform="translate(152.90749,11.301533)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-2-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-6-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-6-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-4-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-22-0"
+         transform="translate(121.05875,21.910393)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-5-9"
+         transform="translate(110.46491,22.0002)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-9-0"
+         transform="translate(131.68323,21.881534)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-7-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-7-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-6-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-7-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-0-1-4"
+         transform="translate(142.36605,21.879012)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-93-77-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-6-1-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-0-1-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-6-5-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-3-4"
+         transform="translate(163.64609,21.78936)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-6-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-5-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-63-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-9-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-4-4"
+         transform="translate(152.93892,21.844019)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-8-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-1-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-2-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-9-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-3-9"
+         transform="translate(174.29048,21.780069)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-9-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-08-22"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-8-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-5-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-5-1"
+         transform="translate(121.09524,32.284674)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-61-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-15-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-9-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-8-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-48-2"
+         transform="translate(110.5014,32.374481)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-1-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-0-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-3-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-0-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-0-4-8"
+         transform="translate(142.40254,32.253293)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-93-4-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-6-4-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-0-4-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-6-7-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-6-3"
+         transform="translate(131.71972,32.255815)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-3-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-1-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-7-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-5-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-96-6"
+         transform="translate(163.68258,32.163641)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-21-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-7-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-8-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-5-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-7-8"
+         transform="translate(152.97541,32.2183)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-4-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-18-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-5-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-97-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-5-3"
+         transform="translate(174.32696,32.15435)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-3-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-8-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-83-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-1-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-8-6"
+         transform="translate(121.07998,42.102698)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-60-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-48-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-8-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-89-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-7-8"
+         transform="translate(110.48614,42.192505)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-76-79"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-43-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-03-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-09-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-0-2-1"
+         transform="translate(142.38729,42.071317)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-93-5-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-6-40-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-0-5-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-6-9-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-4-0"
+         transform="translate(131.70446,42.073839)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-6-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-9-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-2-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-2-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-4-7"
+         transform="translate(163.66733,41.981665)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-7-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-75-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-4-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-81-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-2-8"
+         transform="translate(152.96015,42.036324)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-89-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-3-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-68-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-0-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-2-8"
+         transform="translate(174.31171,41.972374)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-1-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-05-15"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-1-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-10-92"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-9-7-5"
+         transform="translate(184.38619,11.471979)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-5-4-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-0-99-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-4-4-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-8-5-40"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-3-9-1"
+         transform="translate(184.47277,21.949498)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-9-4-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-08-22-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-8-6-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-5-4-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-5-3-1"
+         transform="translate(184.50925,32.323779)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-3-3-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-8-7-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-83-3-32"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-1-2-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-2-8-7"
+         transform="translate(184.494,42.141803)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-1-9-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-05-15-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-1-4-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-10-92-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+    </g>
+    <g
+       id="g4698-2"
+       transform="translate(103.99571,147.10736)">
+      <g
+         id="g1052-9-9-7-2"
+         transform="translate(126.23318,-40.652773)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-5-4-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-0-99-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-4-4-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-8-5-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-6-1-6"
+         transform="translate(72.684847,-40.246111)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-4-7-44"
+         transform="translate(62.091007,-40.156304)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-0-0-4-5"
+         transform="translate(93.992147,-40.277492)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-93-7-2-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-6-8-9-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-0-6-6-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-6-8-1-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-8-0-4"
+         transform="translate(83.309327,-40.27497)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-4-4-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-3-2-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-1-2-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-4-2-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-9-0-1"
+         transform="translate(115.27219,-40.367144)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-2-5-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-0-5-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-6-2-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-8-9-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-9-0-5"
+         transform="translate(104.56502,-40.312485)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-2-2-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-6-8-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-6-3-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-4-8-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-22-0-6"
+         transform="translate(72.716277,-29.703625)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-5-9-0"
+         transform="translate(62.122437,-29.613818)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-9-0-5"
+         transform="translate(83.340757,-29.732484)"
+         style="opacity:0.252344">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-7-5-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-7-0-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-6-2-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-7-9-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-0-1-4-5"
+         transform="translate(94.023577,-29.735006)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-93-77-9-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-6-1-9-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-0-1-3-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-6-5-6-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-3-4-6"
+         transform="translate(115.30362,-29.824658)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-6-3-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-5-5-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-63-1-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-9-7-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-4-4-7"
+         transform="translate(104.59645,-29.769999)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-8-3-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-1-1-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-2-4-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-9-6-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-5-1-0"
+         transform="translate(72.752767,-19.329344)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-61-2-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-15-8-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-9-8-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-8-9-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-48-2-6"
+         transform="translate(62.158927,-19.239537)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-1-8-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-0-8-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-3-8-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-0-6-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-0-4-8-2"
+         transform="translate(94.060067,-19.360725)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-93-4-3-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-6-4-8-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-0-4-3-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-6-7-3-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-6-3-2"
+         transform="translate(83.377247,-19.358203)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-3-8-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-1-0-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-7-4-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-5-7-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-96-6-0"
+         transform="translate(115.34011,-19.450377)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-21-8-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-7-9-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-8-0-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-5-6-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-7-8-2"
+         transform="translate(104.63294,-19.395718)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-4-7-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-18-9-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-5-0-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-97-3-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-5-3-2"
+         transform="translate(125.98449,-19.459668)">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-3-3-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-8-7-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-83-3-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-1-2-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-8-6-6"
+         transform="translate(72.737507,-9.5113204)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-60-5-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-48-2-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-8-6-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-89-5-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-7-8-2"
+         transform="translate(62.143667,-9.4215134)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-76-79-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-43-6-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-03-0-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-09-4-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-0-2-1-4"
+         transform="translate(94.044817,-9.5427014)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-93-5-0-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-6-40-4-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-0-5-8-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-6-9-7-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-4-0-1"
+         transform="translate(83.361987,-9.5401794)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-6-8-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-9-6-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-2-2-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-2-4-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-4-7-6"
+         transform="translate(115.32486,-9.6323534)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-7-9-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-75-3-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-4-9-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-81-2-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-9-0-1-2"
+         transform="translate(136.83954,-40.658221)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-2-5-3-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-0-5-3-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-6-2-3-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-8-9-8-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-3-4-6-0"
+         transform="translate(136.87097,-30.115735)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-6-3-5-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-5-5-9-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-63-1-9-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-9-7-0-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-3-4-6-0-7"
+         transform="translate(126.24242,-30.173373)"
+         style="opacity:0.252344">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-6-3-5-6-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-5-5-9-4-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-63-1-9-9-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-9-7-0-9-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-96-6-0-0"
+         transform="translate(136.90746,-19.741454)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-21-8-0-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-7-9-3-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-8-0-4-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-5-6-9-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-20-4-7-6-4"
+         transform="translate(136.89221,-9.9234312)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-2-7-9-9-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-3-75-3-8-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-75-4-9-1-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-92-81-2-3-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-2-8-3"
+         transform="translate(104.61768,-9.5776944)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-89-3-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-3-0-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-68-1-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-0-7-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-2-8-6"
+         transform="translate(125.96924,-9.6416444)"
+         style="opacity:0.15">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-1-9-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-05-15-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-1-4-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-10-92-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <rect
+         style="opacity:1;fill:none;fill-opacity:1;stroke:#040000;stroke-width:0.865;stroke-dasharray:3.46, 0.865;stroke-dashoffset:0;paint-order:markers stroke fill"
+         id="rect6693"
+         width="21.177956"
+         height="20.838879"
+         x="115.31686"
+         y="-14.213038" />
+      <rect
+         style="fill:none;fill-opacity:1;stroke:#040000;stroke-width:0.865;stroke-dasharray:3.46, 0.865;stroke-dashoffset:0;paint-order:markers stroke fill"
+         id="rect6693-4"
+         width="21.177956"
+         height="20.838879"
+         x="115.17234"
+         y="6.6754951" />
+      <rect
+         style="opacity:1;fill:none;fill-opacity:1;stroke:#040000;stroke-width:0.865;stroke-dasharray:3.46, 0.865;stroke-dashoffset:0;paint-order:markers stroke fill"
+         id="rect6693-8"
+         width="21.177956"
+         height="20.838879"
+         x="136.78856"
+         y="-14.086202" />
+      <rect
+         style="fill:none;fill-opacity:1;stroke:#040000;stroke-width:0.865;stroke-dasharray:3.46, 0.865;stroke-dashoffset:0;paint-order:markers stroke fill"
+         id="rect6693-1"
+         width="21.177956"
+         height="20.838879"
+         x="136.99756"
+         y="6.4101439" />
+      <rect
+         style="fill:none;fill-opacity:1;stroke:#040000;stroke-width:0.865;stroke-dasharray:3.46, 0.865;stroke-dashoffset:0;paint-order:markers stroke fill"
+         id="rect6693-6"
+         width="21.177956"
+         height="20.838879"
+         x="158.1272"
+         y="-14.220315" />
+      <rect
+         style="fill:none;fill-opacity:1;stroke:#040000;stroke-width:0.865;stroke-dasharray:3.46, 0.865;stroke-dashoffset:0;paint-order:markers stroke fill"
+         id="rect6693-85"
+         width="21.177956"
+         height="20.838879"
+         x="158.17397"
+         y="6.7189174" />
+      <rect
+         style="opacity:1;fill:none;fill-opacity:1;stroke:#040000;stroke-width:0.865;stroke-dasharray:3.46, 0.865;stroke-dashoffset:0;paint-order:markers stroke fill"
+         id="rect6693-6-2"
+         width="21.177956"
+         height="20.838879"
+         x="179.34555"
+         y="-14.454299" />
+      <rect
+         style="fill:none;fill-opacity:1;stroke:#040000;stroke-width:0.865;stroke-dasharray:3.46, 0.865;stroke-dashoffset:0;paint-order:markers stroke fill"
+         id="rect6693-85-1"
+         width="21.177956"
+         height="20.838879"
+         x="179.64828"
+         y="6.1961193" />
+    </g>
+    <path
+       style="opacity:1;fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.865;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:1.73, 0.865;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 66.659356,36.818342 218.82541,47.962052"
+       id="path4533"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.865;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:1.73, 0.865;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 150.73309,36.200165 302.902,47.311832"
+       id="path4533-3"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.865;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:1.73, 0.865;stroke-dashoffset:0;stroke-opacity:1"
+       d="M 67.986904,77.950336 220.15295,89.094046"
+       id="path4533-7"
+       sodipodi:nodetypes="cc" />
+    <g
+       id="g1">
+      <g
+         id="g1052-9-9-7-0-7"
+         transform="translate(108.68336,116.60508)"
+         style="opacity:0.300974">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-5-4-8-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-0-99-1-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-4-4-9-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-8-5-9-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-4-7-4-0"
+         transform="translate(77.180174,116.39954)"
+         style="opacity:0.300974">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-9-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-0-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-2-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-0-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-8-0-2-1"
+         transform="translate(87.639831,116.58827)"
+         style="opacity:1">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-4-4-0-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-3-2-7-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-1-2-3-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-4-2-1-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-9-0-7-9"
+         transform="translate(98.042911,116.4909)"
+         style="opacity:1">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-2-2-7-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-6-8-4-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-6-3-0-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-4-8-6-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-9-9-7-0-7-6"
+         transform="translate(108.66488,126.92391)"
+         style="opacity:0.300974">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-31-5-4-8-1-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-9-0-99-1-1-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-4-4-4-9-1-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-78-8-5-9-7-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-2-4-7-4-0-9"
+         transform="translate(77.161698,126.71837)"
+         style="opacity:0.300974">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-9-4-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-0-0-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-2-8-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-0-5-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-8-0-2-1-1"
+         transform="translate(87.741912,126.78654)"
+         style="opacity:1">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-1-4-4-0-6-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-8-3-2-7-6-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-7-1-2-3-2-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-9-4-2-1-1-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-9-0-7-9-8"
+         transform="translate(98.024435,126.80973)"
+         style="opacity:1">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-97-2-2-7-6-3"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-36-6-8-4-4-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-1-6-3-0-8-5"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-2-4-8-6-0-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <rect
+         style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.8;stroke-dasharray:3.2, 0.8;stroke-dashoffset:0;stroke-opacity:1;paint-order:markers stroke fill"
+         id="rect7388"
+         width="20.576803"
+         height="20.307524"
+         x="140.76538"
+         y="143.06564" />
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#TriangleStart)"
+       d="m 261.72591,93.236032 0.16234,31.497498"
+       id="path9137"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#TriangleStart-3)"
+       d="m 67.685641,168.64157 0.16234,16.82752"
+       id="path9137-7"
+       sodipodi:nodetypes="cc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9374)"
+       d="m 212.12608,153.9478 c -0.39202,0.18905 -3.47785,0.26026 -7.09894,0.25872 -8.60075,-0.004 -21.95111,-0.0141 -21.95111,-0.0141"
+       id="path9370"
+       sodipodi:nodetypes="csc" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="85.59259"
+       y="13.192001"
+       id="text9717"><tspan
+         sodipodi:role="line"
+         id="tspan9715"
+         style="stroke-width:0.264583px"
+         x="85.59259"
+         y="13.192001">Pixel array</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="230.03058"
+       y="35.165924"
+       id="text9717-7"><tspan
+         sodipodi:role="line"
+         id="tspan9715-6"
+         style="stroke-width:0.264583px"
+         x="230.03058"
+         y="35.165924">Analog crop</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="231.88487"
+       y="190.28648"
+       id="text9717-7-9"><tspan
+         sodipodi:role="line"
+         id="tspan9715-6-1"
+         style="stroke-width:0.264583px"
+         x="231.88487"
+         y="190.28648">Subsampling</tspan></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="125.35484"
+       y="177.69405"
+       id="text9717-7-9-1"><tspan
+         sodipodi:role="line"
+         id="tspan9715-6-1-9"
+         style="stroke-width:0.264583px"
+         x="125.35484"
+         y="177.69405">Digital crop</tspan><tspan
+         sodipodi:role="line"
+         style="stroke-width:0.264583px"
+         x="125.35484"
+         y="190.92317"
+         id="tspan9850" /></text>
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="43.724319"
+       y="202.93666"
+       id="text9717-7-9-1-5"><tspan
+         sodipodi:role="line"
+         id="tspan9715-6-1-9-3"
+         style="stroke-width:0.264583px"
+         x="43.724319"
+         y="202.93666">Media Bus</tspan><tspan
+         sodipodi:role="line"
+         style="stroke-width:0.264583px"
+         x="43.724319"
+         y="216.16579"
+         id="tspan9850-5" /></text>
+    <g
+       id="g2132"
+       transform="matrix(1.3821489,0,0,1.4039825,12.968023,-62.501729)"
+       style="stroke-width:0.717863">
+      <g
+         id="g1052-9-9-7-0-7-7"
+         transform="translate(-3.2072982,115.91871)"
+         style="opacity:0.300974;stroke-width:0.717863" />
+      <g
+         id="g1052-26-8-0-2-1-3"
+         transform="matrix(0.67497349,0,0,0.67524564,-2.9671558,128.89065)"
+         style="stroke-width:1.06333">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-1-4-4-0-6-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-5-8-3-2-7-6-0"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-3-7-1-2-3-2-62"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-56-9-4-2-1-1-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-9-0-7-9-1"
+         transform="matrix(0.67497349,0,0,0.67524564,4.0546474,128.8249)"
+         style="stroke-width:1.06333">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-97-2-2-7-6-8"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-5-36-6-8-4-4-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-3-1-6-3-0-8-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-56-2-4-8-6-0-20"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-26-8-0-2-1-1-3"
+         transform="matrix(0.67497349,0,0,0.67524564,-2.8982539,135.77698)"
+         style="stroke-width:1.06333">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-1-4-4-0-6-5-6"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-5-8-3-2-7-6-5-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-3-7-1-2-3-2-4-2"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-56-9-4-2-1-1-9-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+      <g
+         id="g1052-28-9-0-7-9-8-3"
+         transform="matrix(0.67497349,0,0,0.67524564,4.0421766,135.79264)"
+         style="stroke-width:1.06333">
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-97-2-2-7-6-3-1"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.024174"
+           y="26.5942" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-5-36-6-8-4-4-8-9"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.23954"
+           y="26.556028" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-3-1-6-3-0-8-5-4"
+           width="5.1440187"
+           height="5.0140224"
+           x="53.028545"
+           y="31.858822" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.393875;paint-order:markers stroke fill"
+           id="rect288-56-2-4-8-6-0-2-7"
+           width="5.1440187"
+           height="5.0140224"
+           x="58.251408"
+           y="31.808577" />
+      </g>
+    </g>
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#marker9374-3)"
+       d="m 123.82202,153.26143 c -0.39202,0.18905 -3.47785,0.26026 -7.09894,0.25872 -8.60075,-0.004 -21.951106,-0.0141 -21.951106,-0.0141"
+       id="path9370-4"
+       sodipodi:nodetypes="csc" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 42.161302,205.87857 50.15057,-0.10808"
+       id="path1" />
+    <path
+       style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       d="m 43.410067,193.21441 50.15057,-0.10808"
+       id="path1-6" />
+    <ellipse
+       style="opacity:1;fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="path2"
+       cx="263.63913"
+       cy="17.331734"
+       rx="8.0464878"
+       ry="7.6880746" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="261.26318"
+       y="21.157597"
+       id="text2"><tspan
+         sodipodi:role="line"
+         id="tspan2"
+         style="stroke-width:0.264583px"
+         x="261.26318"
+         y="21.157597">1</tspan></text>
+    <ellipse
+       style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="path2-7"
+       cx="264.95981"
+       cy="200.2849"
+       rx="8.0464878"
+       ry="7.6880746" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="262.07587"
+       y="204.11606"
+       id="text2-0"><tspan
+         sodipodi:role="line"
+         id="tspan2-9"
+         style="stroke-width:0.264583px"
+         x="262.07587"
+         y="204.11606">2</tspan></text>
+    <ellipse
+       style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="path2-6"
+       cx="44.883369"
+       cy="177.41577"
+       rx="8.0464878"
+       ry="7.6880746" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="41.983547"
+       y="181.24164"
+       id="text2-2"><tspan
+         sodipodi:role="line"
+         id="tspan2-6"
+         style="stroke-width:0.264583px"
+         x="41.983547"
+         y="181.24164">4</tspan></text>
+    <ellipse
+       style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-dasharray:none;paint-order:markers stroke fill"
+       id="path2-3"
+       cx="153.0576"
+       cy="191.86786"
+       rx="8.0464878"
+       ry="7.6880746" />
+    <text
+       xml:space="preserve"
+       style="font-style:normal;font-weight:normal;font-size:10.5833px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+       x="150.21071"
+       y="195.57201"
+       id="text2-6"><tspan
+         sodipodi:role="line"
+         id="tspan2-0"
+         style="stroke-width:0.264583px"
+         x="150.21071"
+         y="195.57201">3</tspan></text>
+  </g>
+</svg>
diff --git a/Documentation/skipping.svg b/Documentation/skipping.svg
new file mode 100644
index 0000000..7bef37c
--- /dev/null
+++ b/Documentation/skipping.svg
@@ -0,0 +1,1720 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+   width="297mm"
+   height="210mm"
+   viewBox="0 0 297 210"
+   version="1.1"
+   id="svg1"
+   inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
+   sodipodi:docname="skipping.svg"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:svg="http://www.w3.org/2000/svg">
+  <sodipodi:namedview
+     id="namedview1"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:showpageshadow="2"
+     inkscape:pageopacity="0.0"
+     inkscape:pagecheckerboard="0"
+     inkscape:deskcolor="#d1d1d1"
+     inkscape:document-units="mm"
+     showgrid="true"
+     showguides="true"
+     inkscape:zoom="0.91421356"
+     inkscape:cx="601.60998"
+     inkscape:cy="360.96599"
+     inkscape:window-width="1916"
+     inkscape:window-height="1040"
+     inkscape:window-x="0"
+     inkscape:window-y="38"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="g2">
+    <inkscape:grid
+       id="grid1"
+       units="px"
+       originx="0"
+       originy="0"
+       spacingx="0.26458334"
+       spacingy="0.26458333"
+       empcolor="#0000ff"
+       empopacity="0.25098039"
+       color="#0000ff"
+       opacity="0.1254902"
+       empspacing="5"
+       dotted="false"
+       gridanglex="30"
+       gridanglez="30"
+       visible="true" />
+  </sodipodi:namedview>
+  <defs
+     id="defs1">
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path4-0" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path4-4" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-3"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path4-9" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-9"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path4-7" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-8"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path4-0-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-3-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path4-9-3" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-6-6"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path4-4-2" />
+    </marker>
+    <marker
+       style="overflow:visible"
+       id="ArrowWideRounded-1-8-5"
+       refX="0"
+       refY="0"
+       orient="auto-start-reverse"
+       inkscape:stockid="Wide, rounded arrow"
+       markerWidth="1"
+       markerHeight="1"
+       viewBox="0 0 1 1"
+       inkscape:isstock="true"
+       inkscape:collect="always"
+       preserveAspectRatio="xMidYMid">
+      <path
+         style="fill:none;stroke:context-stroke;stroke-width:1;stroke-linecap:round"
+         d="M 3,-3 0,0 3,3"
+         transform="rotate(180,0.125,0)"
+         sodipodi:nodetypes="ccc"
+         id="path4-0-3-0" />
+    </marker>
+  </defs>
+  <g
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       id="g4698-2"
+       transform="translate(-26.194788,73.07432)">
+      <g
+         id="g1" />
+      <g
+         id="g2">
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3"
+           width="11.54506"
+           height="11.262992"
+           x="126.11332"
+           y="-33.444302" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6"
+           width="11.54506"
+           height="11.262992"
+           x="137.8185"
+           y="-33.530048" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3"
+           width="11.54506"
+           height="11.262992"
+           x="126.12312"
+           y="-21.618387" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3"
+           width="11.54506"
+           height="11.262992"
+           x="137.84511"
+           y="-21.731256" />
+        <g
+           id="g1052-2-4-7-44"
+           transform="matrix(2.2443659,0,0,2.2462986,-16.668792,-92.981079)"
+           style="stroke-width:0.445369">
+          <rect
+             style="fill:#00ff00;stroke:#040000;stroke-width:0.164972;paint-order:markers stroke fill"
+             id="rect288-9-7-0-3"
+             width="5.1440187"
+             height="5.0140224"
+             x="53.024174"
+             y="26.5942" />
+          <rect
+             style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.164972;paint-order:markers stroke fill"
+             id="rect288-5-1-4-4-9"
+             width="5.1440187"
+             height="5.0140224"
+             x="58.23954"
+             y="26.556028" />
+          <rect
+             style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.164972;paint-order:markers stroke fill"
+             id="rect288-3-2-4-8-7"
+             width="5.1440187"
+             height="5.0140224"
+             x="53.028545"
+             y="31.858822" />
+          <rect
+             style="fill:#00ff00;stroke:#040000;stroke-width:0.164972;paint-order:markers stroke fill"
+             id="rect288-56-7-3-0-2"
+             width="5.1440187"
+             height="5.0140224"
+             x="58.251408"
+             y="31.808577" />
+        </g>
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4-3"
+           width="11.54506"
+           height="11.262992"
+           x="126.18385"
+           y="-9.7627277" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0-5"
+           width="11.54506"
+           height="11.262992"
+           x="137.88902"
+           y="-9.8484716" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9-0"
+           width="11.54506"
+           height="11.262992"
+           x="126.19365"
+           y="2.0631855" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1-8"
+           width="11.54506"
+           height="11.262992"
+           x="137.91565"
+           y="1.9503218" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6-4"
+           width="11.54506"
+           height="11.262992"
+           x="102.40739"
+           y="-9.5609922" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2-1"
+           width="11.54506"
+           height="11.262992"
+           x="114.11258"
+           y="-9.6467371" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5-1"
+           width="11.54506"
+           height="11.262992"
+           x="102.41722"
+           y="2.2649202" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4-3"
+           width="11.54506"
+           height="11.262992"
+           x="114.13924"
+           y="2.1520538" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-0"
+           width="11.54506"
+           height="11.262992"
+           x="173.61343"
+           y="-33.721619" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-9"
+           width="11.54506"
+           height="11.262992"
+           x="185.3186"
+           y="-33.807365" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-0"
+           width="11.54506"
+           height="11.262992"
+           x="173.62323"
+           y="-21.895706" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-7"
+           width="11.54506"
+           height="11.262992"
+           x="185.34526"
+           y="-22.00857" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-6"
+           width="11.54506"
+           height="11.262992"
+           x="149.83696"
+           y="-33.519886" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-8"
+           width="11.54506"
+           height="11.262992"
+           x="161.54218"
+           y="-33.605633" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-7"
+           width="11.54506"
+           height="11.262992"
+           x="149.8468"
+           y="-21.693974" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-7"
+           width="11.54506"
+           height="11.262992"
+           x="161.5688"
+           y="-21.806837" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-4"
+           width="11.54506"
+           height="11.262992"
+           x="125.8679"
+           y="14.089965" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-0"
+           width="11.54506"
+           height="11.262992"
+           x="137.57307"
+           y="14.004221" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-1"
+           width="11.54506"
+           height="11.262992"
+           x="125.8777"
+           y="25.91588" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-2"
+           width="11.54506"
+           height="11.262992"
+           x="137.59967"
+           y="25.803013" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-0"
+           width="11.54506"
+           height="11.262992"
+           x="102.38698"
+           y="14.184427" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-1"
+           width="11.54506"
+           height="11.262992"
+           x="114.09217"
+           y="14.098681" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-5"
+           width="11.54506"
+           height="11.262992"
+           x="102.39677"
+           y="26.010342" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-2"
+           width="11.54506"
+           height="11.262992"
+           x="114.1188"
+           y="25.897474" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-0-0"
+           width="11.54506"
+           height="11.262992"
+           x="173.368"
+           y="13.812649" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-9-7"
+           width="11.54506"
+           height="11.262992"
+           x="185.07317"
+           y="13.726904" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-0-1"
+           width="11.54506"
+           height="11.262992"
+           x="173.37778"
+           y="25.638563" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-7-6"
+           width="11.54506"
+           height="11.262992"
+           x="185.09981"
+           y="25.525694" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-6-4"
+           width="11.54506"
+           height="11.262992"
+           x="149.59154"
+           y="14.014381" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-8-9"
+           width="11.54506"
+           height="11.262992"
+           x="161.29671"
+           y="13.928636" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-7-5"
+           width="11.54506"
+           height="11.262992"
+           x="149.60135"
+           y="25.840294" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-7-3"
+           width="11.54506"
+           height="11.262992"
+           x="161.32336"
+           y="25.727428" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4-3-6"
+           width="11.54506"
+           height="11.262992"
+           x="173.68398"
+           y="-10.040043" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0-5-5"
+           width="11.54506"
+           height="11.262992"
+           x="185.38916"
+           y="-10.125788" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9-0-3"
+           width="11.54506"
+           height="11.262992"
+           x="173.69376"
+           y="1.7858695" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1-8-7"
+           width="11.54506"
+           height="11.262992"
+           x="185.4158"
+           y="1.6730031" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6-4-9"
+           width="11.54506"
+           height="11.262992"
+           x="149.90752"
+           y="-9.8383083" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2-1-7"
+           width="11.54506"
+           height="11.262992"
+           x="161.6127"
+           y="-9.9240551" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5-1-2"
+           width="11.54506"
+           height="11.262992"
+           x="149.91733"
+           y="1.9876043" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4-3-6"
+           width="11.54506"
+           height="11.262992"
+           x="161.63936"
+           y="1.8747379" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-0-3"
+           width="11.54506"
+           height="11.262992"
+           x="221.3436"
+           y="-34.173023" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-9-6"
+           width="11.54506"
+           height="11.262992"
+           x="233.04878"
+           y="-34.25877" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-0-3"
+           width="11.54506"
+           height="11.262992"
+           x="221.35339"
+           y="-22.347115" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-7-60"
+           width="11.54506"
+           height="11.262992"
+           x="233.07542"
+           y="-22.459978" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-6-44"
+           width="11.54506"
+           height="11.262992"
+           x="197.56715"
+           y="-33.971294" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-8-96"
+           width="11.54506"
+           height="11.262992"
+           x="209.27232"
+           y="-34.057037" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-7-6"
+           width="11.54506"
+           height="11.262992"
+           x="197.57697"
+           y="-22.145384" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-7-4"
+           width="11.54506"
+           height="11.262992"
+           x="209.29898"
+           y="-22.258247" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-0-0-1"
+           width="11.54506"
+           height="11.262992"
+           x="221.09816"
+           y="13.361241" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-9-7-9"
+           width="11.54506"
+           height="11.262992"
+           x="232.80333"
+           y="13.275496" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-0-1-8"
+           width="11.54506"
+           height="11.262992"
+           x="221.10796"
+           y="25.187153" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-7-6-0"
+           width="11.54506"
+           height="11.262992"
+           x="232.82999"
+           y="25.074286" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-6-4-4"
+           width="11.54506"
+           height="11.262992"
+           x="197.32172"
+           y="13.562973" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-8-9-9"
+           width="11.54506"
+           height="11.262992"
+           x="209.02689"
+           y="13.477229" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-7-5-0"
+           width="11.54506"
+           height="11.262992"
+           x="197.33153"
+           y="25.388887" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-7-3-5"
+           width="11.54506"
+           height="11.262992"
+           x="209.05351"
+           y="25.27602" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4-3-6-5"
+           width="11.54506"
+           height="11.262992"
+           x="221.41415"
+           y="-10.491449" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0-5-5-0"
+           width="11.54506"
+           height="11.262992"
+           x="233.11932"
+           y="-10.577197" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9-0-3-1"
+           width="11.54506"
+           height="11.262992"
+           x="221.42395"
+           y="1.3344631" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1-8-7-6"
+           width="11.54506"
+           height="11.262992"
+           x="233.14598"
+           y="1.2215967" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6-4-9-0"
+           width="11.54506"
+           height="11.262992"
+           x="197.63768"
+           y="-10.289718" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2-1-7-2"
+           width="11.54506"
+           height="11.262992"
+           x="209.34288"
+           y="-10.375462" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5-1-2-8"
+           width="11.54506"
+           height="11.262992"
+           x="197.64748"
+           y="1.5361952" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4-3-6-8"
+           width="11.54506"
+           height="11.262992"
+           x="209.36951"
+           y="1.4233288" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4-3-3"
+           width="11.54506"
+           height="11.262992"
+           x="126.15435"
+           y="37.763531" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0-5-8"
+           width="11.54506"
+           height="11.262992"
+           x="137.85956"
+           y="37.677784" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9-0-6"
+           width="11.54506"
+           height="11.262992"
+           x="126.16419"
+           y="49.589439" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1-8-1"
+           width="11.54506"
+           height="11.262992"
+           x="137.88618"
+           y="49.476574" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6-4-6"
+           width="11.54506"
+           height="11.262992"
+           x="102.37791"
+           y="37.96526" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2-1-0"
+           width="11.54506"
+           height="11.262992"
+           x="114.08312"
+           y="37.879513" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5-1-9"
+           width="11.54506"
+           height="11.262992"
+           x="102.3877"
+           y="49.791176" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4-3-1"
+           width="11.54506"
+           height="11.262992"
+           x="114.10973"
+           y="49.678307" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-4-9"
+           width="11.54506"
+           height="11.262992"
+           x="125.83842"
+           y="61.616215" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-0-1"
+           width="11.54506"
+           height="11.262992"
+           x="137.54358"
+           y="61.530472" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-1-5"
+           width="11.54506"
+           height="11.262992"
+           x="125.69932"
+           y="73.643074" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-2-0"
+           width="11.54506"
+           height="11.262992"
+           x="137.4213"
+           y="73.530205" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-0-9"
+           width="11.54506"
+           height="11.262992"
+           x="102.35748"
+           y="61.710678" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-1-4"
+           width="11.54506"
+           height="11.262992"
+           x="114.06268"
+           y="61.624939" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-5-9"
+           width="11.54506"
+           height="11.262992"
+           x="102.2184"
+           y="73.737526" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-2-6"
+           width="11.54506"
+           height="11.262992"
+           x="113.94043"
+           y="73.624664" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-0-0-8"
+           width="11.54506"
+           height="11.262992"
+           x="173.3385"
+           y="61.338898" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-9-7-2"
+           width="11.54506"
+           height="11.262992"
+           x="185.04369"
+           y="61.253159" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-0-1-4"
+           width="11.54506"
+           height="11.262992"
+           x="173.19939"
+           y="73.365753" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-7-6-6"
+           width="11.54506"
+           height="11.262992"
+           x="184.92143"
+           y="73.252884" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-6-4-5"
+           width="11.54506"
+           height="11.262992"
+           x="149.56206"
+           y="61.540638" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-8-9-3"
+           width="11.54506"
+           height="11.262992"
+           x="161.26723"
+           y="61.454887" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-7-5-4"
+           width="11.54506"
+           height="11.262992"
+           x="149.42297"
+           y="73.56749" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-7-3-7"
+           width="11.54506"
+           height="11.262992"
+           x="161.14496"
+           y="73.454613" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4-3-6-4"
+           width="11.54506"
+           height="11.262992"
+           x="173.6545"
+           y="37.486214" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0-5-5-7"
+           width="11.54506"
+           height="11.262992"
+           x="185.35968"
+           y="37.400467" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9-0-3-14"
+           width="11.54506"
+           height="11.262992"
+           x="173.66429"
+           y="49.312122" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1-8-7-0"
+           width="11.54506"
+           height="11.262992"
+           x="185.38632"
+           y="49.199261" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6-4-9-4"
+           width="11.54506"
+           height="11.262992"
+           x="149.87804"
+           y="37.687943" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2-1-7-8"
+           width="11.54506"
+           height="11.262992"
+           x="161.58322"
+           y="37.6022" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5-1-2-3"
+           width="11.54506"
+           height="11.262992"
+           x="149.88785"
+           y="49.513859" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4-3-6-3"
+           width="11.54506"
+           height="11.262992"
+           x="161.60985"
+           y="49.40099" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-0-0-1-4"
+           width="11.54506"
+           height="11.262992"
+           x="221.06868"
+           y="60.887497" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-9-7-9-6"
+           width="11.54506"
+           height="11.262992"
+           x="232.77385"
+           y="60.801754" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-0-1-8-1"
+           width="11.54506"
+           height="11.262992"
+           x="220.92955"
+           y="72.914345" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-7-6-0-5"
+           width="11.54506"
+           height="11.262992"
+           x="232.65158"
+           y="72.801476" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-6-4-4-5"
+           width="11.54506"
+           height="11.262992"
+           x="197.29221"
+           y="61.089226" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-8-9-9-4"
+           width="11.54506"
+           height="11.262992"
+           x="208.99741"
+           y="61.003483" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-7-5-0-6"
+           width="11.54506"
+           height="11.262992"
+           x="197.15314"
+           y="73.116074" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-7-3-5-6"
+           width="11.54506"
+           height="11.262992"
+           x="208.87514"
+           y="73.003212" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4-3-6-5-9"
+           width="11.54506"
+           height="11.262992"
+           x="221.38467"
+           y="37.034805" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0-5-5-0-6"
+           width="11.54506"
+           height="11.262992"
+           x="233.08984"
+           y="36.949059" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9-0-3-1-7"
+           width="11.54506"
+           height="11.262992"
+           x="221.39447"
+           y="48.860718" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1-8-7-6-5"
+           width="11.54506"
+           height="11.262992"
+           x="233.11649"
+           y="48.747845" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6-4-9-0-6"
+           width="11.54506"
+           height="11.262992"
+           x="197.6082"
+           y="37.236534" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2-1-7-2-4"
+           width="11.54506"
+           height="11.262992"
+           x="209.31339"
+           y="37.150791" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5-1-2-8-3"
+           width="11.54506"
+           height="11.262992"
+           x="197.618"
+           y="49.062447" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4-3-6-8-0"
+           width="11.54506"
+           height="11.262992"
+           x="209.34003"
+           y="48.949585" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-0-3-8"
+           width="11.54506"
+           height="11.262992"
+           x="268.99387"
+           y="-34.440758" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-9-6-0"
+           width="11.54506"
+           height="11.262992"
+           x="280.69904"
+           y="-34.526497" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-0-3-9"
+           width="11.54506"
+           height="11.262992"
+           x="269.00366"
+           y="-22.614843" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-7-60-6"
+           width="11.54506"
+           height="11.262992"
+           x="280.72571"
+           y="-22.727707" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-6-44-7"
+           width="11.54506"
+           height="11.262992"
+           x="245.21744"
+           y="-34.239029" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-8-96-2"
+           width="11.54506"
+           height="11.262992"
+           x="256.92261"
+           y="-34.324772" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-7-6-1"
+           width="11.54506"
+           height="11.262992"
+           x="245.22725"
+           y="-22.413116" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-7-4-4"
+           width="11.54506"
+           height="11.262992"
+           x="256.94925"
+           y="-22.525974" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-0-0-1-48"
+           width="11.54506"
+           height="11.262992"
+           x="268.74844"
+           y="13.09351" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-9-7-9-4"
+           width="11.54506"
+           height="11.262992"
+           x="280.45361"
+           y="13.007765" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-0-1-8-9"
+           width="11.54506"
+           height="11.262992"
+           x="268.75824"
+           y="24.919422" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-7-6-0-8"
+           width="11.54506"
+           height="11.262992"
+           x="280.48026"
+           y="24.806559" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-6-4-4-4"
+           width="11.54506"
+           height="11.262992"
+           x="244.97197"
+           y="13.29524" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-8-9-9-41"
+           width="11.54506"
+           height="11.262992"
+           x="256.67715"
+           y="13.209496" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-7-5-0-9"
+           width="11.54506"
+           height="11.262992"
+           x="244.98181"
+           y="25.121157" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-7-3-5-68"
+           width="11.54506"
+           height="11.262992"
+           x="256.7038"
+           y="25.008287" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4-3-6-5-0"
+           width="11.54506"
+           height="11.262992"
+           x="269.06442"
+           y="-10.759184" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0-5-5-0-9"
+           width="11.54506"
+           height="11.262992"
+           x="280.76959"
+           y="-10.844928" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9-0-3-1-3"
+           width="11.54506"
+           height="11.262992"
+           x="269.07422"
+           y="1.0667289" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1-8-7-6-6"
+           width="11.54506"
+           height="11.262992"
+           x="280.79623"
+           y="0.95386255" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6-4-9-0-3"
+           width="11.54506"
+           height="11.262992"
+           x="245.28796"
+           y="-10.557449" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2-1-7-2-8"
+           width="11.54506"
+           height="11.262992"
+           x="256.99316"
+           y="-10.643196" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5-1-2-8-9"
+           width="11.54506"
+           height="11.262992"
+           x="245.29776"
+           y="1.2684637" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4-3-6-8-5"
+           width="11.54506"
+           height="11.262992"
+           x="257.01981"
+           y="1.1555973" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-937-0-3-0-0-1-4-0"
+           width="11.54506"
+           height="11.262992"
+           x="268.71893"
+           y="60.619762" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-4-7-6-9-7-9-6-1"
+           width="11.54506"
+           height="11.262992"
+           x="280.42413"
+           y="60.534019" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-5-5-3-0-1-8-1-7"
+           width="11.54506"
+           height="11.262992"
+           x="268.57983"
+           y="72.646606" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-25-8-3-7-6-0-5-1"
+           width="11.54506"
+           height="11.262992"
+           x="280.30188"
+           y="72.533737" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-7-0-3-6-4-4-5-9"
+           width="11.54506"
+           height="11.262992"
+           x="244.94249"
+           y="60.821495" />
+        <rect
+           style="fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-4-4-9-8-9-9-4-8"
+           width="11.54506"
+           height="11.262992"
+           x="256.64767"
+           y="60.735752" />
+        <rect
+           style="fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-4-8-7-7-5-0-6-2"
+           width="11.54506"
+           height="11.262992"
+           x="244.80341"
+           y="72.848343" />
+        <rect
+           style="fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-3-0-2-7-3-5-6-6"
+           width="11.54506"
+           height="11.262992"
+           x="256.52542"
+           y="72.735474" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-6-4-3-6-5-9-7"
+           width="11.54506"
+           height="11.262992"
+           x="269.03494"
+           y="36.767071" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-10-0-5-5-0-6-5"
+           width="11.54506"
+           height="11.262992"
+           x="280.74011"
+           y="36.681328" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-6-9-0-3-1-7-9"
+           width="11.54506"
+           height="11.262992"
+           x="269.04474"
+           y="48.592983" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-1-1-8-7-6-5-3"
+           width="11.54506"
+           height="11.262992"
+           x="280.76675"
+           y="48.480114" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-9-9-6-4-9-0-6-9"
+           width="11.54506"
+           height="11.262992"
+           x="245.25848"
+           y="36.968803" />
+        <rect
+           style="opacity:0.15;fill:#0100ff;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-5-1-49-2-1-7-2-4-9"
+           width="11.54506"
+           height="11.262992"
+           x="256.96365"
+           y="36.883057" />
+        <rect
+           style="opacity:0.15;fill:#ff000a;fill-opacity:1;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-3-2-0-5-1-2-8-3-7"
+           width="11.54506"
+           height="11.262992"
+           x="245.26828"
+           y="48.794716" />
+        <rect
+           style="opacity:0.15;fill:#00ff00;stroke:#040000;stroke-width:0.370417;paint-order:markers stroke fill"
+           id="rect288-56-7-9-4-3-6-8-0-9"
+           width="11.54506"
+           height="11.262992"
+           x="256.99033"
+           y="48.681854" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.250001;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded)"
+           d="m 107.31925,-51.577366 c 4.74513,-3.867921 8.45747,-3.127986 11.65845,-0.106568"
+           id="path2"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.250001;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1)"
+           d="m 155.69201,-52.103201 c 4.74513,-3.867922 8.45748,-3.127987 11.65845,-0.106568"
+           id="path2-0"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.250001;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-8)"
+           d="m 205.6246,-51.786864 c 4.74513,-3.867923 8.45746,-3.127986 11.65845,-0.106568"
+           id="path2-0-9"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.250001;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-1-8-5)"
+           d="m 252.68356,-50.911879 c 4.74513,-3.867922 8.45748,-3.127987 11.65845,-0.106568"
+           id="path2-0-9-9"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.250001;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded)"
+           d="m 119.2485,-56.261218 c 10.02527,-3.47338 19.6883,-7.598722 35.75846,-0.191608"
+           id="path3"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.250001;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-6)"
+           d="m 167.15143,-55.799841 c 10.02528,-3.473382 19.68831,-7.598723 35.75847,-0.191609"
+           id="path3-9"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.250001;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-6-6)"
+           d="m 217.31498,-57.659731 c 10.02527,-3.473382 19.6883,-7.598723 35.75847,-0.191609"
+           id="path3-9-2"
+           sodipodi:nodetypes="cc" />
+        <ellipse
+           id="path5"
+           style="fill:#f6e9e9;stroke:#000000;stroke-width:0.264583"
+           cx="88.19117"
+           cy="-29.671202"
+           rx="0.020938689"
+           ry="0.020956721" />
+        <ellipse
+           id="path6"
+           style="fill:#f6e9e9;stroke:#000000;stroke-width:0.264583"
+           cx="88.29512"
+           cy="-17.772333"
+           rx="0.020938689"
+           ry="0.020956721" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded)"
+           d="m 88.210313,-29.749351 c -3.901538,3.593858 -2.316274,7.754777 0.03417,11.994772"
+           id="path7"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-3)"
+           d="m 88.476097,20.705783 c -3.901539,3.593858 -2.316274,7.754778 0.03417,11.994772"
+           id="path7-5"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#ArrowWideRounded-3-5)"
+           d="m 88.077894,70.899225 c -3.901538,3.593858 -2.316273,7.754778 0.03417,11.994772"
+           id="path7-5-0"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#ArrowWideRounded)"
+           d="M 85.21112,-18.062254 C 77.854142,-3.5491421 78.306424,9.1772046 85.12651,20.446596"
+           id="path8"
+           sodipodi:nodetypes="cc" />
+        <path
+           style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#ArrowWideRounded-9)"
+           d="m 84.721251,32.988117 c -7.356992,14.513113 -6.90471,27.239459 -0.08461,38.508851"
+           id="path8-8"
+           sodipodi:nodetypes="cc" />
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="47.435753"
+           y="-48.114857"
+           id="text8"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             id="tspan8"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="47.435753"
+             y="-48.114857">x_even_inc = 1</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="47.435753"
+             y="-40.193825"
+             id="tspan14" /><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="47.435753"
+             y="-32.272789"
+             id="tspan9" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="33.799152"
+           y="-23.751602"
+           id="text8-5"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="33.799152"
+             y="-23.751602"
+             id="tspan13">y_even_inc = 1</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="33.799152"
+             y="-15.830566"
+             id="tspan9-8" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="105.17001"
+           y="-40.766998"
+           id="text8-5-9"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="105.17001"
+             y="-40.766998"
+             id="tspan13-1"> 0     1     2     3    4     5     6    7     8     9    10   11  12   13   14   15</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="105.17001"
+             y="-32.845963"
+             id="tspan9-8-2" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="47.782906"
+           y="-57.348003"
+           id="text8-3"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             id="tspan8-4"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="47.782906"
+             y="-57.348003">x_odd_inc = 3 </tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="47.782906"
+             y="-49.426968"
+             id="tspan10" /><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="47.782906"
+             y="-41.505932"
+             id="tspan9-0" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="35.099522"
+           y="-13.290938"
+           id="text8-3-5"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="35.099522"
+             y="-13.290938"
+             id="tspan12">y_odd_inc = 3</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="35.099522"
+             y="-5.3699036"
+             id="tspan10-1" /><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="35.099522"
+             y="2.5511286"
+             id="tspan9-0-4" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="95.25663"
+           y="-25.301243"
+           id="text8-5-9-2"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.25663"
+             y="-25.301243"
+             id="tspan13-1-6">0</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.25663"
+             y="-17.380207"
+             id="tspan9-8-2-0" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="94.882782"
+           y="-12.704006"
+           id="text8-5-9-2-0"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="94.882782"
+             y="-12.704006"
+             id="tspan13-1-6-8">1</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="94.882782"
+             y="-4.7829742"
+             id="tspan9-8-2-0-6" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="95.313683"
+           y="-1.2592223"
+           id="text8-5-9-2-6"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.313683"
+             y="-1.2592223"
+             id="tspan13-1-6-0">2</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.313683"
+             y="6.6618114"
+             id="tspan9-8-2-0-5" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="95.326363"
+           y="9.9864616"
+           id="text8-5-9-2-5"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.326363"
+             y="9.9864616"
+             id="tspan13-1-6-04">3</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.326363"
+             y="17.907495"
+             id="tspan9-8-2-0-9" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="95.3517"
+           y="22.179893"
+           id="text8-5-9-2-09"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.3517"
+             y="22.179893"
+             id="tspan13-1-6-6">4</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.3517"
+             y="30.100925"
+             id="tspan9-8-2-0-60" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="95.307358"
+           y="33.597816"
+           id="text8-5-9-2-8"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.307358"
+             y="33.597816"
+             id="tspan13-1-6-68">5</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.307358"
+             y="41.518852"
+             id="tspan9-8-2-0-53" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="95.25663"
+           y="46.90731"
+           id="text8-5-9-2-7"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.25663"
+             y="46.90731"
+             id="tspan13-1-6-80">6</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.25663"
+             y="54.828346"
+             id="tspan9-8-2-0-8" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="95.23764"
+           y="58.228878"
+           id="text8-5-9-2-9"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.23764"
+             y="58.228878"
+             id="tspan13-1-6-7">7</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.23764"
+             y="66.14991"
+             id="tspan9-8-2-0-2" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="95.294655"
+           y="70.441017"
+           id="text8-5-9-2-80"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.294655"
+             y="70.441017"
+             id="tspan13-1-6-62">8</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.294655"
+             y="78.362045"
+             id="tspan9-8-2-0-65" /></text>
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;line-height:125%;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+           x="95.288322"
+           y="82.355179"
+           id="text8-5-9-2-1"
+           transform="scale(0.99956973,1.0004305)"><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.288322"
+             y="82.355179"
+             id="tspan13-1-6-71">9</tspan><tspan
+             sodipodi:role="line"
+             style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:6.33683px;font-family:Sans;-inkscape-font-specification:'Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583px"
+             x="95.288322"
+             y="90.276215"
+             id="tspan9-8-2-0-3" /></text>
+      </g>
+    </g>
+  </g>
+</svg>
diff --git a/Documentation/software-isp-benchmarking.rst b/Documentation/software-isp-benchmarking.rst
new file mode 100644
index 0000000..b303313
--- /dev/null
+++ b/Documentation/software-isp-benchmarking.rst
@@ -0,0 +1,77 @@
+.. SPDX-License-Identifier: CC-BY-SA-4.0
+
+.. _software-isp-benchmarking:
+
+Software ISP benchmarking
+=========================
+
+The Software ISP is particularly sensitive to performance regressions therefore
+it is a good idea to always benchmark the Software ISP before and after making
+changes to it and ensure that there are no performance regressions.
+
+DebayerCpu class builtin benchmark
+----------------------------------
+
+The DebayerCpu class has a builtin benchmark. This benchmark measures the time
+spent on processing (collecting statistics and debayering) only, it does not
+measure the time spent on capturing or outputting the frames.
+
+The builtin benchmark always runs. So this can be used by simply running "cam"
+or "qcam" with a pipeline using the Software ISP.
+
+When it runs it will skip measuring the first 30 frames to allow the caches and
+the CPU temperature (turbo-ing) to warm-up and then it measures 30 fps and shows
+the total and per frame processing time using an info level log message:
+
+.. code-block:: text
+
+   INFO Debayer debayer_cpu.cpp:907 Processed 30 frames in 244317us, 8143 us/frame
+
+To get stable measurements it is advised to disable any other processes which
+may cause significant CPU usage (e.g. disable wifi, bluetooth and browsers).
+When possible it is also advisable to disable CPU turbo-ing and
+frequency-scaling.
+
+For example when benchmarking on a Lenovo ThinkPad X1 Yoga Gen 8, with the
+charger plugged in, the CPU can be fixed to run at 2 GHz using:
+
+.. code-block:: shell
+
+   sudo x86_energy_perf_policy --turbo-enable 0
+   sudo cpupower frequency-set -d 2GHz -u 2GHz
+
+with these settings the builtin bench reports a processing time of ~7.8ms/frame
+on this laptop for FHD SGRBG10 (unpacked) bayer data.
+
+Measuring power consumption
+---------------------------
+
+Since the Software ISP is often used on mobile devices it is also important to
+measure power consumption and ensure that that does not regress.
+
+For example to measure power consumption on a Lenovo ThinkPad X1 Yoga Gen 8 it
+needs to be running on battery and it should be configured with its
+platform-profile (/sys/firmware/acpi/platform_profile) set to balanced and with
+its default turbo and frequency-scaling behavior to match real world usage.
+
+Then start qcam to capture a FHD picture at 30 fps and position the qcam window
+so that it is fully visible. After this run the following command to monitor the
+power consumption:
+
+.. code-block:: shell
+
+   watch -n 10 cat /sys/class/power_supply/BAT0/power_now /sys/class/hwmon/hwmon6/fan?_input
+
+Note this not only measures the power consumption in µW it also monitors the
+speed of this laptop's 2 fans. This is important because depending on the
+ambient temperature the 2 fans may spin up while testing and this will cause an
+additional power consumption of approx. 0.5 W messing up the measurement.
+
+After starting qcam + the watch command let the laptop sit without using it for
+2 minutes for the readings to stabilize. Then check that the fans have not
+turned on and manually take a couple of consecutive power readings and average
+these.
+
+On the example Lenovo ThinkPad X1 Yoga Gen 8 laptop this results in a measured
+power consumption of approx. 13 W while running qcam versus approx. 4-5 W while
+setting idle with its OLED panel on.
diff --git a/README.rst b/README.rst
index 6eed723..1da7a3d 100644
--- a/README.rst
+++ b/README.rst
@@ -30,7 +30,7 @@
 
 To fetch the sources, build and install:
 
-::
+.. code::
 
   git clone https://git.libcamera.org/libcamera/libcamera.git
   cd libcamera
@@ -47,7 +47,7 @@
         Either {g++, clang}
 
 Meson Build system: [required]
-        meson (>= 0.57) ninja-build pkg-config
+        meson (>= 0.60) ninja-build pkg-config
 
 for the libcamera core: [required]
         libyaml-dev python3-yaml python3-ply python3-jinja2
@@ -75,6 +75,9 @@
 for gstreamer: [optional]
         libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
 
+for Python bindings: [optional]
+        libpython3-dev pybind11-dev
+
 for cam: [optional]
         libevent-dev is required to support cam, however the following
         optional dependencies bring more functionality to the cam test
@@ -85,7 +88,7 @@
         - libsdl2-dev: Enables the SDL sink
 
 for qcam: [optional]
-        qtbase5-dev libqt5core5a libqt5gui5 libqt5widgets5 qttools5-dev-tools libtiff-dev
+        libtiff-dev qtbase5-dev qttools5-dev-tools
 
 for tracing with lttng: [optional]
         liblttng-ust-dev python3-jinja2 lttng-tools
@@ -93,8 +96,11 @@
 for android: [optional]
         libexif-dev libjpeg-dev
 
+for Python bindings: [optional]
+        pybind11-dev
+
 for lc-compliance: [optional]
-        libevent-dev
+        libevent-dev libgtest-dev
 
 for abi-compat.sh: [optional]
         abi-compliance-checker
@@ -117,10 +123,13 @@
 Using GStreamer plugin
 ~~~~~~~~~~~~~~~~~~~~~~
 
-To use GStreamer plugin from source tree, set the following environment so that
-GStreamer can find it. This isn't necessary when libcamera is installed.
+To use the GStreamer plugin from the source tree, use the meson ``devenv``
+command.  This will create a new shell instance with the ``GST_PLUGIN_PATH``
+environment set accordingly.
 
-  export GST_PLUGIN_PATH=$(pwd)/build/src/gstreamer
+.. code::
+
+  meson devenv -C build
 
 The debugging tool ``gst-launch-1.0`` can be used to construct a pipeline and
 test it. The following pipeline will stream from the camera named "Camera 1"
@@ -128,7 +137,7 @@
 
 .. code::
 
-  gst-launch-1.0 libcamerasrc camera-name="Camera 1" ! glimagesink
+  gst-launch-1.0 libcamerasrc camera-name="Camera 1" ! queue ! glimagesink
 
 To show the first camera found you can omit the camera-name property, or you
 can list the cameras and their capabilities using:
@@ -143,7 +152,7 @@
 .. code::
 
   gst-launch-1.0 libcamerasrc ! 'video/x-raw,width=1280,height=720' ! \
-        glimagesink
+       queue ! glimagesink
 
 The libcamerasrc element has two log categories, named libcamera-provider (for
 the video device provider) and libcamerasrc (for the operation of the camera).
@@ -159,7 +168,7 @@
 
    gst-launch-1.0 libcamerasrc ! \
         video/x-raw,colorimetry=bt709,format=NV12,width=1280,height=720,framerate=30/1 ! \
-        jpegenc ! multipartmux ! \
+        queue ! jpegenc ! multipartmux ! \
         tcpserversink host=0.0.0.0 port=5000
 
 Which can be received on another device over the network with:
@@ -186,8 +195,8 @@
 
 This can be solved in two ways:
 
-1) Don't install meson again if it is already installed system-wide.
+1. Don't install meson again if it is already installed system-wide.
 
-2) If a version of meson which is different from the system-wide version is
-already installed, uninstall that meson using pip3, and install again without
-the --user argument.
+2. If a version of meson which is different from the system-wide version is
+   already installed, uninstall that meson using pip3, and install again without
+   the --user argument.
diff --git a/include/libcamera/base/backtrace.h b/include/libcamera/base/backtrace.h
index 752034d..699ddd9 100644
--- a/include/libcamera/base/backtrace.h
+++ b/include/libcamera/base/backtrace.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * backtrace.h - Call stack backtraces
+ * Call stack backtraces
  */
 
 #pragma once
diff --git a/include/libcamera/base/bound_method.h b/include/libcamera/base/bound_method.h
index c027524..dd3488e 100644
--- a/include/libcamera/base/bound_method.h
+++ b/include/libcamera/base/bound_method.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * bound_method.h - Method bind and invocation
+ * Method bind and invocation
  */
 
 #pragma once
diff --git a/include/libcamera/base/class.h b/include/libcamera/base/class.h
index 571eecf..a808422 100644
--- a/include/libcamera/base/class.h
+++ b/include/libcamera/base/class.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * class.h - Utilities and helpers for classes
+ * Utilities and helpers for classes
  */
 
 #pragma once
diff --git a/include/libcamera/base/compiler.h b/include/libcamera/base/compiler.h
index 02564f2..fda8fdf 100644
--- a/include/libcamera/base/compiler.h
+++ b/include/libcamera/base/compiler.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * compiler.h - Compiler support
+ * Compiler support
  */
 
 #pragma once
diff --git a/include/libcamera/base/event_dispatcher.h b/include/libcamera/base/event_dispatcher.h
index 184f1b1..e9a09c6 100644
--- a/include/libcamera/base/event_dispatcher.h
+++ b/include/libcamera/base/event_dispatcher.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event_dispatcher.h - Event dispatcher
+ * Event dispatcher
  */
 
 #pragma once
diff --git a/include/libcamera/base/event_dispatcher_poll.h b/include/libcamera/base/event_dispatcher_poll.h
index b784030..1f7e05c 100644
--- a/include/libcamera/base/event_dispatcher_poll.h
+++ b/include/libcamera/base/event_dispatcher_poll.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event_dispatcher_poll.h - Poll-based event dispatcher
+ * Poll-based event dispatcher
  */
 
 #pragma once
diff --git a/include/libcamera/base/event_notifier.h b/include/libcamera/base/event_notifier.h
index e5c0594..158f2d4 100644
--- a/include/libcamera/base/event_notifier.h
+++ b/include/libcamera/base/event_notifier.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event_notifier.h - File descriptor event notifier
+ * File descriptor event notifier
  */
 
 #pragma once
diff --git a/include/libcamera/base/file.h b/include/libcamera/base/file.h
index 0cdc2ed..5637934 100644
--- a/include/libcamera/base/file.h
+++ b/include/libcamera/base/file.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * file.h - File I/O operations
+ * File I/O operations
  */
 
 #pragma once
diff --git a/include/libcamera/base/flags.h b/include/libcamera/base/flags.h
index a1b404b..af4f6e3 100644
--- a/include/libcamera/base/flags.h
+++ b/include/libcamera/base/flags.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * flags.h - Type-safe enum-based bitfields
+ * Type-safe enum-based bitfields
  */
 
 #pragma once
diff --git a/include/libcamera/base/log.h b/include/libcamera/base/log.h
index dcaacbe..df27df0 100644
--- a/include/libcamera/base/log.h
+++ b/include/libcamera/base/log.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * log.h - Logging infrastructure
+ * Logging infrastructure
  */
 
 #pragma once
@@ -48,6 +48,7 @@
 extern const LogCategory &_LOG_CATEGORY(name)();
 
 #define LOG_DEFINE_CATEGORY(name)					\
+LOG_DECLARE_CATEGORY(name)						\
 const LogCategory &_LOG_CATEGORY(name)()				\
 {									\
 	/* The instance will be deleted by the Logger destructor. */	\
diff --git a/include/libcamera/base/message.h b/include/libcamera/base/message.h
index b939af6..4b23203 100644
--- a/include/libcamera/base/message.h
+++ b/include/libcamera/base/message.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * message.h - Message queue support
+ * Message queue support
  */
 
 #pragma once
diff --git a/include/libcamera/base/mutex.h b/include/libcamera/base/mutex.h
index 52441c5..fa9a8d0 100644
--- a/include/libcamera/base/mutex.h
+++ b/include/libcamera/base/mutex.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * mutex.h - Mutex classes with clang thread safety annotation
+ * Mutex classes with clang thread safety annotation
  */
 
 #pragma once
diff --git a/include/libcamera/base/object.h b/include/libcamera/base/object.h
index 9333363..508773c 100644
--- a/include/libcamera/base/object.h
+++ b/include/libcamera/base/object.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * object.h - Base object
+ * Base object
  */
 
 #pragma once
@@ -49,6 +49,8 @@
 protected:
 	virtual void message(Message *msg);
 
+	bool assertThreadBound(const char *message);
+
 private:
 	friend class SignalBase;
 	friend class Thread;
diff --git a/include/libcamera/base/private.h b/include/libcamera/base/private.h
index 163012b..8670c40 100644
--- a/include/libcamera/base/private.h
+++ b/include/libcamera/base/private.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * private.h - Private Header Validation
+ * Private Header Validation
  *
  * A selection of internal libcamera headers are installed as part
  * of the libcamera package to allow sharing of a select subset of
diff --git a/include/libcamera/base/semaphore.h b/include/libcamera/base/semaphore.h
index f105231..59d4aa4 100644
--- a/include/libcamera/base/semaphore.h
+++ b/include/libcamera/base/semaphore.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * semaphore.h - General-purpose counting semaphore
+ * General-purpose counting semaphore
  */
 
 #pragma once
diff --git a/include/libcamera/base/shared_fd.h b/include/libcamera/base/shared_fd.h
index e53a8b8..61fe11c 100644
--- a/include/libcamera/base/shared_fd.h
+++ b/include/libcamera/base/shared_fd.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * shared_fd.h - File descriptor wrapper with shared ownership
+ * File descriptor wrapper with shared ownership
  */
 
 #pragma once
diff --git a/include/libcamera/base/signal.h b/include/libcamera/base/signal.h
index 841e4b4..849fbbd 100644
--- a/include/libcamera/base/signal.h
+++ b/include/libcamera/base/signal.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * signal.h - Signal & slot implementation
+ * Signal & slot implementation
  */
 
 #pragma once
@@ -13,10 +13,11 @@
 #include <vector>
 
 #include <libcamera/base/bound_method.h>
-#include <libcamera/base/object.h>
 
 namespace libcamera {
 
+class Object;
+
 class SignalBase
 {
 public:
diff --git a/include/libcamera/base/span.h b/include/libcamera/base/span.h
index 88d2e3d..c3e63f6 100644
--- a/include/libcamera/base/span.h
+++ b/include/libcamera/base/span.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * span.h - C++20 std::span<> implementation for C++11
+ * C++20 std::span<> implementation for C++11
  */
 
 #pragma once
diff --git a/include/libcamera/base/thread.h b/include/libcamera/base/thread.h
index 9d00f10..4f33de6 100644
--- a/include/libcamera/base/thread.h
+++ b/include/libcamera/base/thread.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * thread.h - Thread support
+ * Thread support
  */
 
 #pragma once
diff --git a/include/libcamera/base/thread_annotations.h b/include/libcamera/base/thread_annotations.h
index 25b3c7b..81930f0 100644
--- a/include/libcamera/base/thread_annotations.h
+++ b/include/libcamera/base/thread_annotations.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * thread_annotation.h - Macro of Clang thread safety analysis
+ * Macro of Clang thread safety analysis
  */
 
 #pragma once
diff --git a/include/libcamera/base/timer.h b/include/libcamera/base/timer.h
index 759b68a..5ef4595 100644
--- a/include/libcamera/base/timer.h
+++ b/include/libcamera/base/timer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * timer.h - Generic timer
+ * Generic timer
  */
 
 #pragma once
diff --git a/include/libcamera/base/unique_fd.h b/include/libcamera/base/unique_fd.h
index ae4d96b..c9a3b5d 100644
--- a/include/libcamera/base/unique_fd.h
+++ b/include/libcamera/base/unique_fd.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * unique_fd.h - File descriptor wrapper that owns a file descriptor.
+ * File descriptor wrapper that owns a file descriptor.
  */
 
 #pragma once
diff --git a/include/libcamera/base/utils.h b/include/libcamera/base/utils.h
index 37d9af6..4ae02dc 100644
--- a/include/libcamera/base/utils.h
+++ b/include/libcamera/base/utils.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * utils.h - Miscellaneous utility functions
+ * Miscellaneous utility functions
  */
 
 #pragma once
@@ -369,6 +369,12 @@
 
 double strtod(const char *__restrict nptr, char **__restrict endptr);
 
+template<class Enum>
+constexpr std::underlying_type_t<Enum> to_underlying(Enum e) noexcept
+{
+	return static_cast<std::underlying_type_t<Enum>>(e);
+}
+
 } /* namespace utils */
 
 #ifndef __DOXYGEN__
diff --git a/include/libcamera/camera.h b/include/libcamera/camera.h
index 004bc89..94cee7b 100644
--- a/include/libcamera/camera.h
+++ b/include/libcamera/camera.h
@@ -2,13 +2,14 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * camera.h - Camera object interface
+ * Camera object interface
  */
 
 #pragma once
 
 #include <initializer_list>
 #include <memory>
+#include <optional>
 #include <set>
 #include <stdint.h>
 #include <string>
@@ -19,9 +20,10 @@
 #include <libcamera/base/signal.h>
 
 #include <libcamera/controls.h>
+#include <libcamera/geometry.h>
+#include <libcamera/orientation.h>
 #include <libcamera/request.h>
 #include <libcamera/stream.h>
-#include <libcamera/transform.h>
 
 namespace libcamera {
 
@@ -30,6 +32,30 @@
 class PipelineHandler;
 class Request;
 
+class SensorConfiguration
+{
+public:
+	unsigned int bitDepth = 0;
+
+	Rectangle analogCrop;
+
+	struct {
+		unsigned int binX = 1;
+		unsigned int binY = 1;
+	} binning;
+
+	struct {
+		unsigned int xOddInc = 1;
+		unsigned int xEvenInc = 1;
+		unsigned int yOddInc = 1;
+		unsigned int yEvenInc = 1;
+	} skipping;
+
+	Size outputSize;
+
+	bool isValid() const;
+};
+
 class CameraConfiguration
 {
 public:
@@ -66,7 +92,8 @@
 	bool empty() const;
 	std::size_t size() const;
 
-	Transform transform;
+	std::optional<SensorConfiguration> sensorConfig;
+	Orientation orientation;
 
 protected:
 	CameraConfiguration();
diff --git a/include/libcamera/camera_manager.h b/include/libcamera/camera_manager.h
index 1a891ca..b50df78 100644
--- a/include/libcamera/camera_manager.h
+++ b/include/libcamera/camera_manager.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * camera_manager.h - Camera management
+ * Camera management
  */
 
 #pragma once
diff --git a/include/libcamera/color_space.h b/include/libcamera/color_space.h
index 6d6c282..7b483cd 100644
--- a/include/libcamera/color_space.h
+++ b/include/libcamera/color_space.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Raspberry Pi Ltd
  *
- * color_space.h - color space definitions
+ * color space definitions
  */
 
 #pragma once
diff --git a/include/libcamera/control_ids.h.in b/include/libcamera/control_ids.h.in
index 0718a88..293ba96 100644
--- a/include/libcamera/control_ids.h.in
+++ b/include/libcamera/control_ids.h.in
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_ids.h - Control ID list
+ * Control ID list
  *
  * This file is auto-generated. Do not edit.
  */
@@ -10,7 +10,9 @@
 #pragma once
 
 #include <array>
+#include <map>
 #include <stdint.h>
+#include <string>
 
 #include <libcamera/controls.h>
 
@@ -26,11 +28,7 @@
 
 extern const ControlIdMap controls;
 
-namespace draft {
-
-${draft_controls}
-
-} /* namespace draft */
+${vendor_controls}
 
 } /* namespace controls */
 
diff --git a/include/libcamera/controls.h b/include/libcamera/controls.h
index cf94205..7c2bb28 100644
--- a/include/libcamera/controls.h
+++ b/include/libcamera/controls.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * controls.h - Control handling
+ * Control handling
  */
 
 #pragma once
@@ -352,6 +352,11 @@
 	using ControlListMap = std::unordered_map<unsigned int, ControlValue>;
 
 public:
+	enum class MergePolicy {
+		KeepExisting = 0,
+		OverwriteExisting,
+	};
+
 	ControlList();
 	ControlList(const ControlIdMap &idmap, const ControlValidator *validator = nullptr);
 	ControlList(const ControlInfoMap &infoMap, const ControlValidator *validator = nullptr);
@@ -368,7 +373,7 @@
 	std::size_t size() const { return controls_.size(); }
 
 	void clear() { controls_.clear(); }
-	void merge(const ControlList &source);
+	void merge(const ControlList &source, MergePolicy policy = MergePolicy::KeepExisting);
 
 	bool contains(unsigned int id) const;
 
diff --git a/include/libcamera/fence.h b/include/libcamera/fence.h
index c0c916c..598336c 100644
--- a/include/libcamera/fence.h
+++ b/include/libcamera/fence.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * internal/fence.h - Synchronization fence
+ * Synchronization fence
  */
 
 #pragma once
diff --git a/include/libcamera/formats.h.in b/include/libcamera/formats.h.in
index ead5287..6ae7634 100644
--- a/include/libcamera/formats.h.in
+++ b/include/libcamera/formats.h.in
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * formats.h - Formats
+ * Formats
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/include/libcamera/framebuffer.h b/include/libcamera/framebuffer.h
index 6124482..5ae2270 100644
--- a/include/libcamera/framebuffer.h
+++ b/include/libcamera/framebuffer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * framebuffer.h - Frame buffer handling
+ * Frame buffer handling
  */
 
 #pragma once
diff --git a/include/libcamera/framebuffer_allocator.h b/include/libcamera/framebuffer_allocator.h
index 45ff232..f3896bf 100644
--- a/include/libcamera/framebuffer_allocator.h
+++ b/include/libcamera/framebuffer_allocator.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * framebuffer_allocator.h - FrameBuffer allocator
+ * FrameBuffer allocator
  */
 
 #pragma once
diff --git a/include/libcamera/geometry.h b/include/libcamera/geometry.h
index d7fdbe7..3e6f0f5 100644
--- a/include/libcamera/geometry.h
+++ b/include/libcamera/geometry.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * geometry.h - Geometry-related classes
+ * Geometry-related classes
  */
 
 #pragma once
diff --git a/include/libcamera/internal/bayer_format.h b/include/libcamera/internal/bayer_format.h
index 78ba396..5c14bb5 100644
--- a/include/libcamera/internal/bayer_format.h
+++ b/include/libcamera/internal/bayer_format.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * bayer_format.h - Bayer Pixel Format
+ * Bayer Pixel Format
  */
 
 #pragma once
@@ -34,6 +34,8 @@
 		None = 0,
 		CSI2 = 1,
 		IPU3 = 2,
+		PISP1 = 3,
+		PISP2 = 4,
 	};
 
 	constexpr BayerFormat()
diff --git a/include/libcamera/internal/byte_stream_buffer.h b/include/libcamera/internal/byte_stream_buffer.h
index 0f4fce6..5b1c10a 100644
--- a/include/libcamera/internal/byte_stream_buffer.h
+++ b/include/libcamera/internal/byte_stream_buffer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * byte_stream_buffer.h - Byte stream buffer
+ * Byte stream buffer
  */
 
 #pragma once
diff --git a/include/libcamera/internal/camera.h b/include/libcamera/internal/camera.h
index 38dd94f..0add042 100644
--- a/include/libcamera/internal/camera.h
+++ b/include/libcamera/internal/camera.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera.h - Camera private data
+ * Camera private data
  */
 
 #pragma once
diff --git a/include/libcamera/internal/camera_controls.h b/include/libcamera/internal/camera_controls.h
index ee6d382..4a5a3eb 100644
--- a/include/libcamera/internal/camera_controls.h
+++ b/include/libcamera/internal/camera_controls.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_controls.h - Camera controls
+ * Camera controls
  */
 
 #pragma once
diff --git a/include/libcamera/internal/camera_lens.h b/include/libcamera/internal/camera_lens.h
index 277417d..5a4b993 100644
--- a/include/libcamera/internal/camera_lens.h
+++ b/include/libcamera/internal/camera_lens.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_lens.h - A camera lens controller
+ * A camera lens controller
  */
 #pragma once
 
diff --git a/include/libcamera/internal/camera_manager.h b/include/libcamera/internal/camera_manager.h
index 33ebe06..af9ed60 100644
--- a/include/libcamera/internal/camera_manager.h
+++ b/include/libcamera/internal/camera_manager.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2023, Ideas on Board Oy.
  *
- * camera_manager.h - Camera manager private data
+ * Camera manager private data
  */
 
 #pragma once
@@ -44,6 +44,7 @@
 private:
 	int init();
 	void createPipelineHandlers();
+	void pipelineFactoryMatch(const PipelineHandlerFactoryBase *factory);
 	void cleanup() LIBCAMERA_TSA_EXCLUDES(mutex_);
 
 	/*
diff --git a/include/libcamera/internal/camera_sensor.h b/include/libcamera/internal/camera_sensor.h
index 02c77ab..fc44ab9 100644
--- a/include/libcamera/internal/camera_sensor.h
+++ b/include/libcamera/internal/camera_sensor.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_sensor.h - A camera sensor
+ * A camera sensor
  */
 
 #pragma once
@@ -17,21 +17,25 @@
 #include <libcamera/control_ids.h>
 #include <libcamera/controls.h>
 #include <libcamera/geometry.h>
+#include <libcamera/orientation.h>
 #include <libcamera/transform.h>
 
 #include <libcamera/ipa/core_ipa_interface.h>
 
+#include "libcamera/internal/bayer_format.h"
 #include "libcamera/internal/formats.h"
 #include "libcamera/internal/v4l2_subdevice.h"
 
 namespace libcamera {
 
-class BayerFormat;
 class CameraLens;
 class MediaEntity;
+class SensorConfiguration;
 
 struct CameraSensorProperties;
 
+enum class Orientation;
+
 class CameraSensor : protected Loggable
 {
 public:
@@ -42,15 +46,15 @@
 
 	const std::string &model() const { return model_; }
 	const std::string &id() const { return id_; }
+
 	const MediaEntity *entity() const { return entity_; }
+	V4L2Subdevice *device() { return subdev_.get(); }
+
+	CameraLens *focusLens() { return focusLens_.get(); }
+
 	const std::vector<unsigned int> &mbusCodes() const { return mbusCodes_; }
 	std::vector<Size> sizes(unsigned int mbusCode) const;
 	Size resolution() const;
-	const std::vector<controls::draft::TestPatternModeEnum> &testPatternModes() const
-	{
-		return testPatternModes_;
-	}
-	int setTestPatternMode(controls::draft::TestPatternModeEnum mode);
 
 	V4L2SubdeviceFormat getFormat(const std::vector<unsigned int> &mbusCodes,
 				      const Size &size) const;
@@ -58,20 +62,24 @@
 		      Transform transform = Transform::Identity);
 	int tryFormat(V4L2SubdeviceFormat *format) const;
 
+	int applyConfiguration(const SensorConfiguration &config,
+			       Transform transform = Transform::Identity,
+			       V4L2SubdeviceFormat *sensorFormat = nullptr);
+
+	const ControlList &properties() const { return properties_; }
+	int sensorInfo(IPACameraSensorInfo *info) const;
+	Transform computeTransform(Orientation *orientation) const;
+	BayerFormat::Order bayerOrder(Transform t) const;
+
 	const ControlInfoMap &controls() const;
 	ControlList getControls(const std::vector<uint32_t> &ids);
 	int setControls(ControlList *ctrls);
 
-	V4L2Subdevice *device() { return subdev_.get(); }
-
-	const ControlList &properties() const { return properties_; }
-	int sensorInfo(IPACameraSensorInfo *info) const;
-
-	void updateControlInfo();
-
-	CameraLens *focusLens() { return focusLens_.get(); }
-
-	Transform validateTransform(Transform *transform) const;
+	const std::vector<controls::draft::TestPatternModeEnum> &testPatternModes() const
+	{
+		return testPatternModes_;
+	}
+	int setTestPatternMode(controls::draft::TestPatternModeEnum mode);
 
 protected:
 	std::string logPrefix() const override;
@@ -85,8 +93,8 @@
 	void initStaticProperties();
 	void initTestPatternModes();
 	int initProperties();
-	int applyTestPatternMode(controls::draft::TestPatternModeEnum mode);
 	int discoverAncillaryDevices();
+	int applyTestPatternMode(controls::draft::TestPatternModeEnum mode);
 
 	const MediaEntity *entity_;
 	std::unique_ptr<V4L2Subdevice> subdev_;
@@ -107,6 +115,8 @@
 	Rectangle activeArea_;
 	const BayerFormat *bayerFormat_;
 	bool supportFlips_;
+	bool flipsAlterBayerOrder_;
+	Orientation mountingOrientation_;
 
 	ControlList properties_;
 
diff --git a/include/libcamera/internal/camera_sensor_properties.h b/include/libcamera/internal/camera_sensor_properties.h
index 1ee3cb9..480ac12 100644
--- a/include/libcamera/internal/camera_sensor_properties.h
+++ b/include/libcamera/internal/camera_sensor_properties.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_sensor_properties.h - Database of camera sensor properties
+ * Database of camera sensor properties
  */
 
 #pragma once
diff --git a/include/libcamera/internal/control_serializer.h b/include/libcamera/internal/control_serializer.h
index a38ca6b..8a63ae4 100644
--- a/include/libcamera/internal/control_serializer.h
+++ b/include/libcamera/internal/control_serializer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_serializer.h - Control (de)serializer
+ * Control (de)serializer
  */
 
 #pragma once
diff --git a/include/libcamera/internal/control_validator.h b/include/libcamera/internal/control_validator.h
index 26412d8..260602f 100644
--- a/include/libcamera/internal/control_validator.h
+++ b/include/libcamera/internal/control_validator.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_validator.h - Control validator
+ * Control validator
  */
 
 #pragma once
diff --git a/include/libcamera/internal/converter.h b/include/libcamera/internal/converter.h
index 834ec5a..5d74db6 100644
--- a/include/libcamera/internal/converter.h
+++ b/include/libcamera/internal/converter.h
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Laurent Pinchart
  * Copyright 2022 NXP
  *
- * converter.h - Generic format converter interface
+ * Generic format converter interface
  */
 
 #pragma once
diff --git a/include/libcamera/internal/converter/converter_v4l2_m2m.h b/include/libcamera/internal/converter/converter_v4l2_m2m.h
index 815916d..1126050 100644
--- a/include/libcamera/internal/converter/converter_v4l2_m2m.h
+++ b/include/libcamera/internal/converter/converter_v4l2_m2m.h
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Laurent Pinchart
  * Copyright 2022 NXP
  *
- * converter_v4l2_m2m.h - V4l2 M2M Format converter interface
+ * V4l2 M2M Format converter interface
  */
 
 #pragma once
@@ -47,7 +47,7 @@
 
 	int configure(const StreamConfiguration &inputCfg,
 		      const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfg);
-	int exportBuffers(unsigned int ouput, unsigned int count,
+	int exportBuffers(unsigned int output, unsigned int count,
 			  std::vector<std::unique_ptr<FrameBuffer>> *buffers);
 
 	int start();
diff --git a/include/libcamera/internal/delayed_controls.h b/include/libcamera/internal/delayed_controls.h
index aef3707..e8d3014 100644
--- a/include/libcamera/internal/delayed_controls.h
+++ b/include/libcamera/internal/delayed_controls.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * delayed_controls.h - Helper to deal with controls that take effect with a delay
+ * Helper to deal with controls that take effect with a delay
  */
 
 #pragma once
diff --git a/include/libcamera/internal/device_enumerator.h b/include/libcamera/internal/device_enumerator.h
index 72ec9a6..db3532a 100644
--- a/include/libcamera/internal/device_enumerator.h
+++ b/include/libcamera/internal/device_enumerator.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * device_enumerator.h - API to enumerate and find media devices
+ * API to enumerate and find media devices
  */
 
 #pragma once
diff --git a/include/libcamera/internal/device_enumerator_sysfs.h b/include/libcamera/internal/device_enumerator_sysfs.h
index 3e84b83..a5bfc71 100644
--- a/include/libcamera/internal/device_enumerator_sysfs.h
+++ b/include/libcamera/internal/device_enumerator_sysfs.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * device_enumerator_sysfs.h - sysfs-based device enumerator
+ * sysfs-based device enumerator
  */
 
 #pragma once
diff --git a/include/libcamera/internal/device_enumerator_udev.h b/include/libcamera/internal/device_enumerator_udev.h
index 1b3360d..1378c19 100644
--- a/include/libcamera/internal/device_enumerator_udev.h
+++ b/include/libcamera/internal/device_enumerator_udev.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018-2019, Google Inc.
  *
- * device_enumerator_udev.h - udev-based device enumerator
+ * udev-based device enumerator
  */
 
 #pragma once
diff --git a/include/libcamera/internal/dma_buf_allocator.h b/include/libcamera/internal/dma_buf_allocator.h
new file mode 100644
index 0000000..36ec169
--- /dev/null
+++ b/include/libcamera/internal/dma_buf_allocator.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * Helper class for dma-buf allocations.
+ */
+
+#pragma once
+
+#include <stddef.h>
+
+#include <libcamera/base/flags.h>
+#include <libcamera/base/unique_fd.h>
+
+namespace libcamera {
+
+class DmaBufAllocator
+{
+public:
+	enum class DmaBufAllocatorFlag {
+		CmaHeap = 1 << 0,
+		SystemHeap = 1 << 1,
+		UDmaBuf = 1 << 2,
+	};
+
+	using DmaBufAllocatorFlags = Flags<DmaBufAllocatorFlag>;
+
+	DmaBufAllocator(DmaBufAllocatorFlags flags = DmaBufAllocatorFlag::CmaHeap);
+	~DmaBufAllocator();
+	bool isValid() const { return providerHandle_.isValid(); }
+	UniqueFD alloc(const char *name, std::size_t size);
+
+private:
+	UniqueFD allocFromHeap(const char *name, std::size_t size);
+	UniqueFD allocFromUDmaBuf(const char *name, std::size_t size);
+	UniqueFD providerHandle_;
+	DmaBufAllocatorFlag type_;
+};
+
+LIBCAMERA_FLAGS_ENABLE_OPERATORS(DmaBufAllocator::DmaBufAllocatorFlag)
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/formats.h b/include/libcamera/internal/formats.h
index 5b16c0a..71895cd 100644
--- a/include/libcamera/internal/formats.h
+++ b/include/libcamera/internal/formats.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * formats.h - libcamera image formats
+ * libcamera image formats
  */
 
 #pragma once
diff --git a/include/libcamera/internal/framebuffer.h b/include/libcamera/internal/framebuffer.h
index 1f42a4f..e6698a4 100644
--- a/include/libcamera/internal/framebuffer.h
+++ b/include/libcamera/internal/framebuffer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * framebuffer.h - Internal frame buffer handling
+ * Internal frame buffer handling
  */
 
 #pragma once
diff --git a/include/libcamera/internal/ipa_data_serializer.h b/include/libcamera/internal/ipa_data_serializer.h
index 085f1fe..337c948 100644
--- a/include/libcamera/internal/ipa_data_serializer.h
+++ b/include/libcamera/internal/ipa_data_serializer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * ipa_data_serializer.h - Image Processing Algorithm data serializer
+ * Image Processing Algorithm data serializer
  */
 
 #pragma once
diff --git a/include/libcamera/internal/ipa_manager.h b/include/libcamera/internal/ipa_manager.h
index bf82356..c6f74e1 100644
--- a/include/libcamera/internal/ipa_manager.h
+++ b/include/libcamera/internal/ipa_manager.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_manager.h - Image Processing Algorithm module manager
+ * Image Processing Algorithm module manager
  */
 
 #pragma once
diff --git a/include/libcamera/internal/ipa_module.h b/include/libcamera/internal/ipa_module.h
index 8038bde..7c49d3f 100644
--- a/include/libcamera/internal/ipa_module.h
+++ b/include/libcamera/internal/ipa_module.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_module.h - Image Processing Algorithm module
+ * Image Processing Algorithm module
  */
 
 #pragma once
diff --git a/include/libcamera/internal/ipa_proxy.h b/include/libcamera/internal/ipa_proxy.h
index 781c8b6..ed6a5bc 100644
--- a/include/libcamera/internal/ipa_proxy.h
+++ b/include/libcamera/internal/ipa_proxy.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_proxy.h - Image Processing Algorithm proxy
+ * Image Processing Algorithm proxy
  */
 
 #pragma once
diff --git a/include/libcamera/internal/ipc_pipe.h b/include/libcamera/internal/ipc_pipe.h
index ab5dd67..a456075 100644
--- a/include/libcamera/internal/ipc_pipe.h
+++ b/include/libcamera/internal/ipc_pipe.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * ipc_pipe.h - Image Processing Algorithm IPC module for IPA proxies
+ * Image Processing Algorithm IPC module for IPA proxies
  */
 
 #pragma once
diff --git a/include/libcamera/internal/ipc_pipe_unixsocket.h b/include/libcamera/internal/ipc_pipe_unixsocket.h
index 004d953..4a0f6d5 100644
--- a/include/libcamera/internal/ipc_pipe_unixsocket.h
+++ b/include/libcamera/internal/ipc_pipe_unixsocket.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * ipc_pipe_unixsocket.h - Image Processing Algorithm IPC module using unix socket
+ * Image Processing Algorithm IPC module using unix socket
  */
 
 #pragma once
diff --git a/include/libcamera/internal/ipc_unixsocket.h b/include/libcamera/internal/ipc_unixsocket.h
index 3963d18..48bb7a9 100644
--- a/include/libcamera/internal/ipc_unixsocket.h
+++ b/include/libcamera/internal/ipc_unixsocket.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipc_unixsocket.h - IPC mechanism based on Unix sockets
+ * IPC mechanism based on Unix sockets
  */
 
 #pragma once
diff --git a/include/libcamera/internal/mapped_framebuffer.h b/include/libcamera/internal/mapped_framebuffer.h
index fb39adb..6aaabf5 100644
--- a/include/libcamera/internal/mapped_framebuffer.h
+++ b/include/libcamera/internal/mapped_framebuffer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * mapped_framebuffer.h - Frame buffer memory mapping support
+ * Frame buffer memory mapping support
  */
 
 #pragma once
diff --git a/include/libcamera/internal/media_device.h b/include/libcamera/internal/media_device.h
index eb8cfde..bf2e475 100644
--- a/include/libcamera/internal/media_device.h
+++ b/include/libcamera/internal/media_device.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * media_device.h - Media device handler
+ * Media device handler
  */
 
 #pragma once
diff --git a/include/libcamera/internal/media_object.h b/include/libcamera/internal/media_object.h
index b157296..c9d7751 100644
--- a/include/libcamera/internal/media_object.h
+++ b/include/libcamera/internal/media_object.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * media_object.h - Media Device objects: entities, pads and links.
+ * Media Device objects: entities, pads and links.
  */
 
 #pragma once
diff --git a/include/libcamera/internal/meson.build b/include/libcamera/internal/meson.build
index 7f1f344..9713ea1 100644
--- a/include/libcamera/internal/meson.build
+++ b/include/libcamera/internal/meson.build
@@ -25,6 +25,7 @@
     'device_enumerator.h',
     'device_enumerator_sysfs.h',
     'device_enumerator_udev.h',
+    'dma_buf_allocator.h',
     'formats.h',
     'framebuffer.h',
     'ipa_manager.h',
@@ -38,6 +39,7 @@
     'process.h',
     'pub_key.h',
     'request.h',
+    'shared_mem_object.h',
     'source_paths.h',
     'sysfs.h',
     'v4l2_device.h',
@@ -48,3 +50,4 @@
 ])
 
 subdir('converter')
+subdir('software_isp')
diff --git a/include/libcamera/internal/pipeline_handler.h b/include/libcamera/internal/pipeline_handler.h
index c96944f..746a34f 100644
--- a/include/libcamera/internal/pipeline_handler.h
+++ b/include/libcamera/internal/pipeline_handler.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * pipeline_handler.h - Pipeline handler infrastructure
+ * Pipeline handler infrastructure
  */
 
 #pragma once
@@ -114,6 +114,7 @@
 	const std::string &name() const { return name_; }
 
 	static std::vector<PipelineHandlerFactoryBase *> &factories();
+	static const PipelineHandlerFactoryBase *getFactoryByName(const std::string &name);
 
 private:
 	static void registerType(PipelineHandlerFactoryBase *factory);
@@ -140,7 +141,7 @@
 	}
 };
 
-#define REGISTER_PIPELINE_HANDLER(handler) \
-static PipelineHandlerFactory<handler> global_##handler##Factory(#handler);
+#define REGISTER_PIPELINE_HANDLER(handler, name) \
+	static PipelineHandlerFactory<handler> global_##handler##Factory(name);
 
 } /* namespace libcamera */
diff --git a/include/libcamera/internal/process.h b/include/libcamera/internal/process.h
index 95e67e1..b1d07a5 100644
--- a/include/libcamera/internal/process.h
+++ b/include/libcamera/internal/process.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * process.h - Process object
+ * Process object
  */
 
 #pragma once
diff --git a/include/libcamera/internal/pub_key.h b/include/libcamera/internal/pub_key.h
index 8653a91..c8cc04c 100644
--- a/include/libcamera/internal/pub_key.h
+++ b/include/libcamera/internal/pub_key.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * pub_key.h - Public key signature verification
+ * Public key signature verification
  */
 
 #pragma once
diff --git a/include/libcamera/internal/request.h b/include/libcamera/internal/request.h
index 3454cf5..f5d9806 100644
--- a/include/libcamera/internal/request.h
+++ b/include/libcamera/internal/request.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * request.h - Request class private data
+ * Request class private data
  */
 
 #pragma once
diff --git a/include/libcamera/internal/shared_mem_object.h b/include/libcamera/internal/shared_mem_object.h
new file mode 100644
index 0000000..2ab0189
--- /dev/null
+++ b/include/libcamera/internal/shared_mem_object.h
@@ -0,0 +1,127 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023 Raspberry Pi Ltd
+ * Copyright (C) 2024 Andrei Konovalov
+ * Copyright (C) 2024 Dennis Bonke
+ *
+ * Helpers for shared memory allocations
+ */
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+#include <string>
+#include <sys/mman.h>
+#include <type_traits>
+#include <utility>
+
+#include <libcamera/base/class.h>
+#include <libcamera/base/shared_fd.h>
+#include <libcamera/base/span.h>
+
+namespace libcamera {
+
+class SharedMem
+{
+public:
+	SharedMem();
+
+	SharedMem(const std::string &name, std::size_t size);
+	SharedMem(SharedMem &&rhs);
+
+	virtual ~SharedMem();
+
+	SharedMem &operator=(SharedMem &&rhs);
+
+	const SharedFD &fd() const
+	{
+		return fd_;
+	}
+
+	Span<uint8_t> mem() const
+	{
+		return mem_;
+	}
+
+	explicit operator bool() const
+	{
+		return !mem_.empty();
+	}
+
+private:
+	LIBCAMERA_DISABLE_COPY(SharedMem)
+
+	SharedFD fd_;
+
+	Span<uint8_t> mem_;
+};
+
+template<class T, typename = std::enable_if_t<std::is_standard_layout<T>::value>>
+class SharedMemObject : public SharedMem
+{
+public:
+	static constexpr std::size_t kSize = sizeof(T);
+
+	SharedMemObject()
+		: SharedMem(), obj_(nullptr)
+	{
+	}
+
+	template<class... Args>
+	SharedMemObject(const std::string &name, Args &&...args)
+		: SharedMem(name, kSize), obj_(nullptr)
+	{
+		if (mem().empty())
+			return;
+
+		obj_ = new (mem().data()) T(std::forward<Args>(args)...);
+	}
+
+	SharedMemObject(SharedMemObject<T> &&rhs)
+		: SharedMem(std::move(rhs))
+	{
+		this->obj_ = rhs.obj_;
+		rhs.obj_ = nullptr;
+	}
+
+	~SharedMemObject()
+	{
+		if (obj_)
+			obj_->~T();
+	}
+
+	SharedMemObject<T> &operator=(SharedMemObject<T> &&rhs)
+	{
+		SharedMem::operator=(std::move(rhs));
+		this->obj_ = rhs.obj_;
+		rhs.obj_ = nullptr;
+		return *this;
+	}
+
+	T *operator->()
+	{
+		return obj_;
+	}
+
+	const T *operator->() const
+	{
+		return obj_;
+	}
+
+	T &operator*()
+	{
+		return *obj_;
+	}
+
+	const T &operator*() const
+	{
+		return *obj_;
+	}
+
+private:
+	LIBCAMERA_DISABLE_COPY(SharedMemObject)
+
+	T *obj_;
+};
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/software_isp/debayer_params.h b/include/libcamera/internal/software_isp/debayer_params.h
new file mode 100644
index 0000000..7d8fdd4
--- /dev/null
+++ b/include/libcamera/internal/software_isp/debayer_params.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, 2024 Red Hat Inc.
+ *
+ * Authors:
+ * Hans de Goede <hdegoede@redhat.com>
+ *
+ * DebayerParams header
+ */
+
+#pragma once
+
+#include <array>
+#include <stdint.h>
+
+namespace libcamera {
+
+struct DebayerParams {
+	static constexpr unsigned int kRGBLookupSize = 256;
+
+	using ColorLookupTable = std::array<uint8_t, kRGBLookupSize>;
+
+	ColorLookupTable red;
+	ColorLookupTable green;
+	ColorLookupTable blue;
+};
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/software_isp/meson.build b/include/libcamera/internal/software_isp/meson.build
new file mode 100644
index 0000000..508dddd
--- /dev/null
+++ b/include/libcamera/internal/software_isp/meson.build
@@ -0,0 +1,7 @@
+# SPDX-License-Identifier: CC0-1.0
+
+libcamera_internal_headers += files([
+    'debayer_params.h',
+    'software_isp.h',
+    'swisp_stats.h',
+])
diff --git a/include/libcamera/internal/software_isp/software_isp.h b/include/libcamera/internal/software_isp/software_isp.h
new file mode 100644
index 0000000..c5338c0
--- /dev/null
+++ b/include/libcamera/internal/software_isp/software_isp.h
@@ -0,0 +1,99 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ *
+ * Simple software ISP implementation
+ */
+
+#pragma once
+
+#include <functional>
+#include <initializer_list>
+#include <map>
+#include <memory>
+#include <string>
+#include <tuple>
+#include <vector>
+
+#include <libcamera/base/class.h>
+#include <libcamera/base/log.h>
+#include <libcamera/base/signal.h>
+#include <libcamera/base/thread.h>
+
+#include <libcamera/geometry.h>
+#include <libcamera/pixel_format.h>
+
+#include <libcamera/ipa/soft_ipa_interface.h>
+#include <libcamera/ipa/soft_ipa_proxy.h>
+
+#include "libcamera/internal/camera_sensor.h"
+#include "libcamera/internal/dma_buf_allocator.h"
+#include "libcamera/internal/pipeline_handler.h"
+#include "libcamera/internal/shared_mem_object.h"
+#include "libcamera/internal/software_isp/debayer_params.h"
+
+namespace libcamera {
+
+class DebayerCpu;
+class FrameBuffer;
+class PixelFormat;
+struct StreamConfiguration;
+
+LOG_DECLARE_CATEGORY(SoftwareIsp)
+
+class SoftwareIsp
+{
+public:
+	SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor);
+	~SoftwareIsp();
+
+	int loadConfiguration([[maybe_unused]] const std::string &filename) { return 0; }
+
+	bool isValid() const;
+
+	std::vector<PixelFormat> formats(PixelFormat input);
+
+	SizeRange sizes(PixelFormat inputFormat, const Size &inputSize);
+
+	std::tuple<unsigned int, unsigned int>
+	strideAndFrameSize(const PixelFormat &outputFormat, const Size &size);
+
+	int configure(const StreamConfiguration &inputCfg,
+		      const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs,
+		      const ControlInfoMap &sensorControls);
+
+	int exportBuffers(unsigned int output, unsigned int count,
+			  std::vector<std::unique_ptr<FrameBuffer>> *buffers);
+
+	void processStats(const ControlList &sensorControls);
+
+	int start();
+	void stop();
+
+	int queueBuffers(FrameBuffer *input,
+			 const std::map<unsigned int, FrameBuffer *> &outputs);
+
+	void process(FrameBuffer *input, FrameBuffer *output);
+
+	Signal<FrameBuffer *> inputBufferReady;
+	Signal<FrameBuffer *> outputBufferReady;
+	Signal<> ispStatsReady;
+	Signal<const ControlList &> setSensorControls;
+
+private:
+	void saveIspParams();
+	void setSensorCtrls(const ControlList &sensorControls);
+	void statsReady();
+	void inputReady(FrameBuffer *input);
+	void outputReady(FrameBuffer *output);
+
+	std::unique_ptr<DebayerCpu> debayer_;
+	Thread ispWorkerThread_;
+	SharedMemObject<DebayerParams> sharedParams_;
+	DebayerParams debayerParams_;
+	DmaBufAllocator dmaHeap_;
+
+	std::unique_ptr<ipa::soft::IPAProxySoft> ipa_;
+};
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/software_isp/swisp_stats.h b/include/libcamera/internal/software_isp/swisp_stats.h
new file mode 100644
index 0000000..ae11f11
--- /dev/null
+++ b/include/libcamera/internal/software_isp/swisp_stats.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ *
+ * Statistics data format used by the software ISP and software IPA
+ */
+
+#pragma once
+
+#include <array>
+#include <stdint.h>
+
+namespace libcamera {
+
+/**
+ * \brief Struct that holds the statistics for the Software ISP
+ *
+ * The struct value types are large enough to not overflow.
+ * Should they still overflow for some reason, no check is performed and they
+ * wrap around.
+ */
+struct SwIspStats {
+	/**
+	 * \brief Holds the sum of all sampled red pixels
+	 */
+	uint64_t sumR_;
+	/**
+	 * \brief Holds the sum of all sampled green pixels
+	 */
+	uint64_t sumG_;
+	/**
+	 * \brief Holds the sum of all sampled blue pixels
+	 */
+	uint64_t sumB_;
+	/**
+	 * \brief Number of bins in the yHistogram
+	 */
+	static constexpr unsigned int kYHistogramSize = 64;
+	/**
+	 * \brief Type of the histogram.
+	 */
+	using Histogram = std::array<uint32_t, kYHistogramSize>;
+	/**
+	 * \brief A histogram of luminance values
+	 */
+	Histogram yHistogram;
+};
+
+} /* namespace libcamera */
diff --git a/include/libcamera/internal/source_paths.h b/include/libcamera/internal/source_paths.h
index be6f153..14e6471 100644
--- a/include/libcamera/internal/source_paths.h
+++ b/include/libcamera/internal/source_paths.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * source_paths.h - Identify libcamera source and build paths
+ * Identify libcamera source and build paths
  */
 
 #pragma once
diff --git a/include/libcamera/internal/sysfs.h b/include/libcamera/internal/sysfs.h
index 917457b..aca60fb 100644
--- a/include/libcamera/internal/sysfs.h
+++ b/include/libcamera/internal/sysfs.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * sysfs.h - Miscellaneous utility functions to access sysfs
+ * Miscellaneous utility functions to access sysfs
  */
 
 #pragma once
diff --git a/include/libcamera/internal/tracepoints.h.in b/include/libcamera/internal/tracepoints.h.in
index d0fc136..f096209 100644
--- a/include/libcamera/internal/tracepoints.h.in
+++ b/include/libcamera/internal/tracepoints.h.in
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) {{year}}, Google Inc.
  *
- * tracepoints.h - Tracepoints with lttng
+ * Tracepoints with lttng
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/include/libcamera/internal/v4l2_device.h b/include/libcamera/internal/v4l2_device.h
index 50d4adb..f5aa502 100644
--- a/include/libcamera/internal/v4l2_device.h
+++ b/include/libcamera/internal/v4l2_device.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_device.h - Common base for V4L2 video devices and subdevices
+ * Common base for V4L2 video devices and subdevices
  */
 
 #pragma once
diff --git a/include/libcamera/internal/v4l2_pixelformat.h b/include/libcamera/internal/v4l2_pixelformat.h
index 44439ff..c836346 100644
--- a/include/libcamera/internal/v4l2_pixelformat.h
+++ b/include/libcamera/internal/v4l2_pixelformat.h
@@ -3,7 +3,7 @@
  * Copyright (C) 2019, Google Inc.
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * v4l2_pixelformat.h - V4L2 Pixel Format
+ * V4L2 Pixel Format
  */
 
 #pragma once
diff --git a/include/libcamera/internal/v4l2_subdevice.h b/include/libcamera/internal/v4l2_subdevice.h
index 69862de..194382f 100644
--- a/include/libcamera/internal/v4l2_subdevice.h
+++ b/include/libcamera/internal/v4l2_subdevice.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_subdevice.h - V4L2 Subdevice
+ * V4L2 Subdevice
  */
 
 #pragma once
@@ -29,6 +29,26 @@
 
 class MediaDevice;
 
+class MediaBusFormatInfo
+{
+public:
+	enum class Type {
+		Image,
+		Metadata,
+		EmbeddedData,
+	};
+
+	bool isValid() const { return code != 0; }
+
+	static const MediaBusFormatInfo &info(uint32_t code);
+
+	const char *name;
+	uint32_t code;
+	Type type;
+	unsigned int bitsPerPixel;
+	PixelFormatInfo::ColourEncoding colourEncoding;
+};
+
 struct V4L2SubdeviceCapability final : v4l2_subdev_capability {
 	bool isReadOnly() const
 	{
@@ -36,17 +56,16 @@
 	}
 	bool hasStreams() const
 	{
-		return capabilities & V4L2_SUBDEV_CAP_MPLEXED;
+		return capabilities & V4L2_SUBDEV_CAP_STREAMS;
 	}
 };
 
 struct V4L2SubdeviceFormat {
-	uint32_t mbus_code;
+	uint32_t code;
 	Size size;
 	std::optional<ColorSpace> colorSpace;
 
 	const std::string toString() const;
-	uint8_t bitsPerPixel() const;
 };
 
 std::ostream &operator<<(std::ostream &out, const V4L2SubdeviceFormat &f);
@@ -61,12 +80,39 @@
 		ActiveFormat = V4L2_SUBDEV_FORMAT_ACTIVE,
 	};
 
-	class Routing : public std::vector<struct v4l2_subdev_route>
-	{
-	public:
-		std::string toString() const;
+	struct Stream {
+		Stream()
+			: pad(0), stream(0)
+		{
+		}
+
+		Stream(unsigned int p, unsigned int s)
+			: pad(p), stream(s)
+		{
+		}
+
+		unsigned int pad;
+		unsigned int stream;
 	};
 
+	struct Route {
+		Route()
+			: flags(0)
+		{
+		}
+
+		Route(const Stream &snk, const Stream &src, uint32_t f)
+			: sink(snk), source(src), flags(f)
+		{
+		}
+
+		Stream sink;
+		Stream source;
+		uint32_t flags;
+	};
+
+	using Routing = std::vector<Route>;
+
 	explicit V4L2Subdevice(const MediaEntity *entity);
 	~V4L2Subdevice();
 
@@ -74,17 +120,39 @@
 
 	const MediaEntity *entity() const { return entity_; }
 
-	int getSelection(unsigned int pad, unsigned int target,
+	int getSelection(const Stream &stream, unsigned int target,
 			 Rectangle *rect);
-	int setSelection(unsigned int pad, unsigned int target,
+	int getSelection(unsigned int pad, unsigned int target, Rectangle *rect)
+	{
+		return getSelection({ pad, 0 }, target, rect);
+	}
+	int setSelection(const Stream &stream, unsigned int target,
 			 Rectangle *rect);
+	int setSelection(unsigned int pad, unsigned int target, Rectangle *rect)
+	{
+		return setSelection({ pad, 0 }, target, rect);
+	}
 
-	Formats formats(unsigned int pad);
+	Formats formats(const Stream &stream);
+	Formats formats(unsigned int pad)
+	{
+		return formats({ pad, 0 });
+	}
 
+	int getFormat(const Stream &stream, V4L2SubdeviceFormat *format,
+		      Whence whence = ActiveFormat);
 	int getFormat(unsigned int pad, V4L2SubdeviceFormat *format,
+		      Whence whence = ActiveFormat)
+	{
+		return getFormat({ pad, 0 }, format, whence);
+	}
+	int setFormat(const Stream &stream, V4L2SubdeviceFormat *format,
 		      Whence whence = ActiveFormat);
 	int setFormat(unsigned int pad, V4L2SubdeviceFormat *format,
-		      Whence whence = ActiveFormat);
+		      Whence whence = ActiveFormat)
+	{
+		return setFormat({ pad, 0 }, format, whence);
+	}
 
 	int getRouting(Routing *routing, Whence whence = ActiveFormat);
 	int setRouting(Routing *routing, Whence whence = ActiveFormat);
@@ -104,14 +172,28 @@
 	std::optional<ColorSpace>
 	toColorSpace(const v4l2_mbus_framefmt &format) const;
 
-	std::vector<unsigned int> enumPadCodes(unsigned int pad);
-	std::vector<SizeRange> enumPadSizes(unsigned int pad,
+	std::vector<unsigned int> enumPadCodes(const Stream &stream);
+	std::vector<SizeRange> enumPadSizes(const Stream &stream,
 					    unsigned int code);
 
+	int getRoutingLegacy(Routing *routing, Whence whence);
+	int setRoutingLegacy(Routing *routing, Whence whence);
+
 	const MediaEntity *entity_;
 
 	std::string model_;
 	struct V4L2SubdeviceCapability caps_;
 };
 
+bool operator==(const V4L2Subdevice::Stream &lhs, const V4L2Subdevice::Stream &rhs);
+static inline bool operator!=(const V4L2Subdevice::Stream &lhs,
+			      const V4L2Subdevice::Stream &rhs)
+{
+	return !(lhs == rhs);
+}
+
+std::ostream &operator<<(std::ostream &out, const V4L2Subdevice::Stream &stream);
+std::ostream &operator<<(std::ostream &out, const V4L2Subdevice::Route &route);
+std::ostream &operator<<(std::ostream &out, const V4L2Subdevice::Routing &routing);
+
 } /* namespace libcamera */
diff --git a/include/libcamera/internal/v4l2_videodevice.h b/include/libcamera/internal/v4l2_videodevice.h
index d157a44..9057be0 100644
--- a/include/libcamera/internal/v4l2_videodevice.h
+++ b/include/libcamera/internal/v4l2_videodevice.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_videodevice.h - V4L2 Video Device
+ * V4L2 Video Device
  */
 
 #pragma once
diff --git a/include/libcamera/internal/yaml_parser.h b/include/libcamera/internal/yaml_parser.h
index 8ca71df..e38a2df 100644
--- a/include/libcamera/internal/yaml_parser.h
+++ b/include/libcamera/internal/yaml_parser.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * yaml_parser.h - libcamera YAML parsing helper
+ * libcamera YAML parsing helper
  */
 
 #pragma once
@@ -161,34 +161,23 @@
 
 	std::size_t size() const;
 
-#ifndef __DOXYGEN__
-	template<typename T,
-		 std::enable_if_t<
-			 std::is_same_v<bool, T> ||
-			 std::is_same_v<double, T> ||
-			 std::is_same_v<int8_t, T> ||
-			 std::is_same_v<uint8_t, T> ||
-			 std::is_same_v<int16_t, T> ||
-			 std::is_same_v<uint16_t, T> ||
-			 std::is_same_v<int32_t, T> ||
-			 std::is_same_v<uint32_t, T> ||
-			 std::is_same_v<std::string, T> ||
-			 std::is_same_v<Size, T>> * = nullptr>
-#else
 	template<typename T>
-#endif
-	std::optional<T> get() const;
-
-	template<typename T>
-	T get(const T &defaultValue) const
+	std::optional<T> get() const
 	{
-		return get<T>().value_or(defaultValue);
+		return Getter<T>{}.get(*this);
+	}
+
+	template<typename T, typename U>
+	T get(U &&defaultValue) const
+	{
+		return get<T>().value_or(std::forward<U>(defaultValue));
 	}
 
 #ifndef __DOXYGEN__
 	template<typename T,
 		 std::enable_if_t<
 			 std::is_same_v<bool, T> ||
+			 std::is_same_v<float, T> ||
 			 std::is_same_v<double, T> ||
 			 std::is_same_v<int8_t, T> ||
 			 std::is_same_v<uint8_t, T> ||
@@ -214,6 +203,8 @@
 private:
 	LIBCAMERA_DISABLE_COPY_AND_MOVE(YamlObject)
 
+	template<typename T>
+	friend struct Getter;
 	friend class YamlParserContext;
 
 	enum class Type {
@@ -222,6 +213,11 @@
 		Value,
 	};
 
+	template<typename T>
+	struct Getter {
+		std::optional<T> get(const YamlObject &obj) const;
+	};
+
 	Type type_;
 
 	std::string value_;
diff --git a/include/libcamera/ipa/ipa_controls.h b/include/libcamera/ipa/ipa_controls.h
index e5da194..5fd1339 100644
--- a/include/libcamera/ipa/ipa_controls.h
+++ b/include/libcamera/ipa/ipa_controls.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_controls.h - IPA Control handling
+ * IPA Control handling
  */
 
 #pragma once
diff --git a/include/libcamera/ipa/ipa_interface.h b/include/libcamera/ipa/ipa_interface.h
index 8884f0e..53cf537 100644
--- a/include/libcamera/ipa/ipa_interface.h
+++ b/include/libcamera/ipa/ipa_interface.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_interface.h - Image Processing Algorithm interface
+ * Image Processing Algorithm interface
  */
 
 #pragma once
@@ -33,8 +33,8 @@
 	virtual ~IPAInterface() = default;
 };
 
-} /* namespace libcamera */
-
 extern "C" {
 libcamera::IPAInterface *ipaCreate();
 }
+
+} /* namespace libcamera */
diff --git a/include/libcamera/ipa/ipa_module_info.h b/include/libcamera/ipa/ipa_module_info.h
index b19b00f..3507a6d 100644
--- a/include/libcamera/ipa/ipa_module_info.h
+++ b/include/libcamera/ipa/ipa_module_info.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_module_info.h - Image Processing Algorithm module information
+ * Image Processing Algorithm module information
  */
 
 #pragma once
diff --git a/include/libcamera/ipa/meson.build b/include/libcamera/ipa/meson.build
index e72803b..3352d08 100644
--- a/include/libcamera/ipa/meson.build
+++ b/include/libcamera/ipa/meson.build
@@ -65,32 +65,32 @@
     'ipu3': 'ipu3.mojom',
     'rkisp1': 'rkisp1.mojom',
     'rpi/vc4': 'raspberrypi.mojom',
+    'simple': 'soft.mojom',
     'vimc': 'vimc.mojom',
 }
 
-ipa_mojom_files = []
-ipa_mojoms = []
-
 #
 # Generate headers from templates.
 #
 
 # TODO Define per-pipeline ControlInfoMap with yaml?
 
+ipa_mojoms = []
+mojoms_built = []
 foreach pipeline, file : pipeline_ipa_mojom_mapping
     name = file.split('.')[0]
 
-    # Ensure we do not build duplicate mojom modules
-    if file in ipa_mojom_files
+    # Avoid building duplicate mojom interfaces with the same interface file
+    if name in mojoms_built
         continue
     endif
 
-    ipa_mojom_files += file
-
     if pipeline not in pipelines
         continue
     endif
 
+    mojoms_built += name
+
     # {interface}.mojom-module
     mojom = custom_target(name + '_mojom_module',
                           input : file,
@@ -155,6 +155,12 @@
     libcamera_generated_ipa_headers += [header, serializer, proxy_header]
 endforeach
 
+ipa_mojom_files = []
+foreach pipeline, file : pipeline_ipa_mojom_mapping
+    if file not in ipa_mojom_files
+        ipa_mojom_files += file
+    endif
+endforeach
 ipa_mojom_files = files(ipa_mojom_files)
 
 # Pass this to the documentation generator in src/libcamera/ipa
diff --git a/include/libcamera/ipa/raspberrypi.mojom b/include/libcamera/ipa/raspberrypi.mojom
index ab4c4e2..5986c43 100644
--- a/include/libcamera/ipa/raspberrypi.mojom
+++ b/include/libcamera/ipa/raspberrypi.mojom
@@ -22,6 +22,9 @@
 struct InitParams {
 	bool lensPresent;
 	libcamera.IPACameraSensorInfo sensorInfo;
+	/* PISP specific */
+	libcamera.SharedFD fe;
+	libcamera.SharedFD be;
 };
 
 struct InitResult {
@@ -219,13 +222,14 @@
 	 * \fn prepareIspComplete()
 	 * \brief Signal completion of \a prepareIsp
 	 * \param[in] buffers Bayer and embedded buffers actioned.
+	 * \param[in] stitchSwapBuffers Whether the stitch block buffers need to be swapped.
 	 *
 	 * This asynchronous event is signalled to the pipeline handler once
 	 * the \a prepareIsp signal has completed, and the ISP is ready to start
 	 * processing the frame. The embedded data buffer may be recycled after
 	 * this event.
 	 */
-	prepareIspComplete(BufferIds buffers);
+	prepareIspComplete(BufferIds buffers, bool stitchSwapBuffers);
 
 	/**
 	 * \fn processStatsComplete()
diff --git a/include/libcamera/ipa/soft.mojom b/include/libcamera/ipa/soft.mojom
new file mode 100644
index 0000000..3aa2066
--- /dev/null
+++ b/include/libcamera/ipa/soft.mojom
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/*
+ * \todo Document the interface and remove the related EXCLUDE_PATTERNS entry.
+ */
+
+module ipa.soft;
+
+import "include/libcamera/ipa/core.mojom";
+
+interface IPASoftInterface {
+	init(libcamera.IPASettings settings,
+	     libcamera.SharedFD fdStats,
+	     libcamera.SharedFD fdParams,
+	     libcamera.ControlInfoMap sensorCtrlInfoMap)
+		=> (int32 ret);
+	start() => (int32 ret);
+	stop();
+	configure(libcamera.ControlInfoMap sensorCtrlInfoMap)
+		=> (int32 ret);
+
+	[async] processStats(libcamera.ControlList sensorControls);
+};
+
+interface IPASoftEventInterface {
+	setSensorControls(libcamera.ControlList sensorControls);
+	setIspParams();
+};
diff --git a/include/libcamera/logging.h b/include/libcamera/logging.h
index cd842f6..e334d87 100644
--- a/include/libcamera/logging.h
+++ b/include/libcamera/logging.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * logging.h - Logging infrastructure
+ * Logging infrastructure
  */
 
 #pragma once
diff --git a/include/libcamera/meson.build b/include/libcamera/meson.build
index 408b7ac..84c6c4c 100644
--- a/include/libcamera/meson.build
+++ b/include/libcamera/meson.build
@@ -12,6 +12,7 @@
     'framebuffer_allocator.h',
     'geometry.h',
     'logging.h',
+    'orientation.h',
     'pixel_format.h',
     'request.h',
     'stream.h',
@@ -31,20 +32,58 @@
 
 libcamera_headers_install_dir = get_option('includedir') / libcamera_include_dir
 
-# control_ids.h and property_ids.h
-control_source_files = [
-    'control_ids',
-    'property_ids',
-]
+controls_map = {
+    'controls': {
+        'draft': 'control_ids_draft.yaml',
+        'core': 'control_ids_core.yaml',
+        'rpi/vc4': 'control_ids_rpi.yaml',
+    },
+
+    'properties': {
+        'draft': 'property_ids_draft.yaml',
+        'core': 'property_ids_core.yaml',
+    }
+}
 
 control_headers = []
+controls_files = []
+properties_files = []
 
-foreach header : control_source_files
-    input_files = files('../../src/libcamera/' + header +'.yaml', header + '.h.in')
+foreach mode, entry : controls_map
+    files_list = []
+    input_files = []
+    foreach vendor, header : entry
+        if vendor != 'core' and vendor != 'draft'
+            if vendor not in pipelines
+                continue
+            endif
+        endif
+
+        if header in files_list
+            continue
+        endif
+
+        files_list += header
+        input_files += files('../../src/libcamera/' + header)
+    endforeach
+
+    outfile = ''
+    if mode == 'controls'
+        outfile = 'control_ids.h'
+        controls_files += files_list
+    else
+        outfile = 'property_ids.h'
+        properties_files += files_list
+    endif
+
+    template_file = files(outfile + '.in')
+    ranges_file = files('../../src/libcamera/control_ranges.yaml')
     control_headers += custom_target(header + '_h',
                                      input : input_files,
-                                     output : header + '.h',
-                                     command : [gen_controls, '-o', '@OUTPUT@', '@INPUT@'],
+                                     output : outfile,
+                                     command : [gen_controls, '-o', '@OUTPUT@',
+                                                '--mode', mode, '-t', template_file,
+                                                '-r', ranges_file, '@INPUT@'],
                                      install : true,
                                      install_dir : libcamera_headers_install_dir)
 endforeach
diff --git a/include/libcamera/orientation.h b/include/libcamera/orientation.h
new file mode 100644
index 0000000..a3b40e6
--- /dev/null
+++ b/include/libcamera/orientation.h
@@ -0,0 +1,30 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Ideas On Board Oy
+ *
+ * Image orientation
+ */
+
+#pragma once
+
+#include <iostream>
+
+namespace libcamera {
+
+enum class Orientation {
+	/* EXIF tag 274 starts from '1' */
+	Rotate0 = 1,
+	Rotate0Mirror,
+	Rotate180,
+	Rotate180Mirror,
+	Rotate90Mirror,
+	Rotate270,
+	Rotate270Mirror,
+	Rotate90,
+};
+
+Orientation orientationFromRotation(int angle, bool *success = nullptr);
+
+std::ostream &operator<<(std::ostream &out, const Orientation &orientation);
+
+} /* namespace libcamera */
diff --git a/include/libcamera/pixel_format.h b/include/libcamera/pixel_format.h
index d49c5f7..ea60fe7 100644
--- a/include/libcamera/pixel_format.h
+++ b/include/libcamera/pixel_format.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * pixel_format.h - libcamera Pixel Format
+ * libcamera Pixel Format
  */
 
 #pragma once
diff --git a/include/libcamera/property_ids.h.in b/include/libcamera/property_ids.h.in
index ff01940..e6edabc 100644
--- a/include/libcamera/property_ids.h.in
+++ b/include/libcamera/property_ids.h.in
@@ -2,14 +2,16 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * property_ids.h - Property ID list
+ * Property ID list
  *
  * This file is auto-generated. Do not edit.
  */
 
 #pragma once
 
+#include <map>
 #include <stdint.h>
+#include <string>
 
 #include <libcamera/controls.h>
 
@@ -23,14 +25,10 @@
 
 ${controls}
 
-namespace draft {
-
-${draft_controls}
-
-} /* namespace draft */
-
 extern const ControlIdMap properties;
 
+${vendor_controls}
+
 } /* namespace properties */
 
 } /* namespace libcamera */
diff --git a/include/libcamera/request.h b/include/libcamera/request.h
index dffde15..2c78d9b 100644
--- a/include/libcamera/request.h
+++ b/include/libcamera/request.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * request.h - Capture request handling
+ * Capture request handling
  */
 
 #pragma once
diff --git a/include/libcamera/stream.h b/include/libcamera/stream.h
index 4e94187..d510238 100644
--- a/include/libcamera/stream.h
+++ b/include/libcamera/stream.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * stream.h - Video stream for a Camera
+ * Video stream for a Camera
  */
 
 #pragma once
diff --git a/include/libcamera/transform.h b/include/libcamera/transform.h
index 2a6838c..a88f809 100644
--- a/include/libcamera/transform.h
+++ b/include/libcamera/transform.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * transform.h - 2D plane transforms
+ * 2D plane transforms
  */
 
 #pragma once
@@ -11,6 +11,8 @@
 
 namespace libcamera {
 
+enum class Orientation;
+
 enum class Transform : int {
 	Identity = 0,
 	Rot0 = Identity,
@@ -70,6 +72,9 @@
 
 Transform transformFromRotation(int angle, bool *success = nullptr);
 
+Transform operator/(const Orientation &o1, const Orientation &o2);
+Orientation operator*(const Orientation &o, const Transform &t);
+
 const char *transformToString(Transform t);
 
 } /* namespace libcamera */
diff --git a/include/libcamera/version.h.in b/include/libcamera/version.h.in
index 6e24d0a..50bf100 100644
--- a/include/libcamera/version.h.in
+++ b/include/libcamera/version.h.in
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * version.h - Library version information
+ * Library version information
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/include/linux/README b/include/linux/README
index 9f61517..b779530 100644
--- a/include/linux/README
+++ b/include/linux/README
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: CC0-1.0
 
-Files in this directory are imported from v5.19 of the Linux kernel. Do not
+Files in this directory are imported from v6.10-rc1 of the Linux kernel. Do not
 modify them manually.
diff --git a/include/linux/dma-buf.h b/include/linux/dma-buf.h
index b1523cb..5a6fda6 100644
--- a/include/linux/dma-buf.h
+++ b/include/linux/dma-buf.h
@@ -85,6 +85,88 @@
 
 #define DMA_BUF_NAME_LEN	32
 
+/**
+ * struct dma_buf_export_sync_file - Get a sync_file from a dma-buf
+ *
+ * Userspace can perform a DMA_BUF_IOCTL_EXPORT_SYNC_FILE to retrieve the
+ * current set of fences on a dma-buf file descriptor as a sync_file.  CPU
+ * waits via poll() or other driver-specific mechanisms typically wait on
+ * whatever fences are on the dma-buf at the time the wait begins.  This
+ * is similar except that it takes a snapshot of the current fences on the
+ * dma-buf for waiting later instead of waiting immediately.  This is
+ * useful for modern graphics APIs such as Vulkan which assume an explicit
+ * synchronization model but still need to inter-operate with dma-buf.
+ *
+ * The intended usage pattern is the following:
+ *
+ *  1. Export a sync_file with flags corresponding to the expected GPU usage
+ *     via DMA_BUF_IOCTL_EXPORT_SYNC_FILE.
+ *
+ *  2. Submit rendering work which uses the dma-buf.  The work should wait on
+ *     the exported sync file before rendering and produce another sync_file
+ *     when complete.
+ *
+ *  3. Import the rendering-complete sync_file into the dma-buf with flags
+ *     corresponding to the GPU usage via DMA_BUF_IOCTL_IMPORT_SYNC_FILE.
+ *
+ * Unlike doing implicit synchronization via a GPU kernel driver's exec ioctl,
+ * the above is not a single atomic operation.  If userspace wants to ensure
+ * ordering via these fences, it is the respnosibility of userspace to use
+ * locks or other mechanisms to ensure that no other context adds fences or
+ * submits work between steps 1 and 3 above.
+ */
+struct dma_buf_export_sync_file {
+	/**
+	 * @flags: Read/write flags
+	 *
+	 * Must be DMA_BUF_SYNC_READ, DMA_BUF_SYNC_WRITE, or both.
+	 *
+	 * If DMA_BUF_SYNC_READ is set and DMA_BUF_SYNC_WRITE is not set,
+	 * the returned sync file waits on any writers of the dma-buf to
+	 * complete.  Waiting on the returned sync file is equivalent to
+	 * poll() with POLLIN.
+	 *
+	 * If DMA_BUF_SYNC_WRITE is set, the returned sync file waits on
+	 * any users of the dma-buf (read or write) to complete.  Waiting
+	 * on the returned sync file is equivalent to poll() with POLLOUT.
+	 * If both DMA_BUF_SYNC_WRITE and DMA_BUF_SYNC_READ are set, this
+	 * is equivalent to just DMA_BUF_SYNC_WRITE.
+	 */
+	__u32 flags;
+	/** @fd: Returned sync file descriptor */
+	__s32 fd;
+};
+
+/**
+ * struct dma_buf_import_sync_file - Insert a sync_file into a dma-buf
+ *
+ * Userspace can perform a DMA_BUF_IOCTL_IMPORT_SYNC_FILE to insert a
+ * sync_file into a dma-buf for the purposes of implicit synchronization
+ * with other dma-buf consumers.  This allows clients using explicitly
+ * synchronized APIs such as Vulkan to inter-op with dma-buf consumers
+ * which expect implicit synchronization such as OpenGL or most media
+ * drivers/video.
+ */
+struct dma_buf_import_sync_file {
+	/**
+	 * @flags: Read/write flags
+	 *
+	 * Must be DMA_BUF_SYNC_READ, DMA_BUF_SYNC_WRITE, or both.
+	 *
+	 * If DMA_BUF_SYNC_READ is set and DMA_BUF_SYNC_WRITE is not set,
+	 * this inserts the sync_file as a read-only fence.  Any subsequent
+	 * implicitly synchronized writes to this dma-buf will wait on this
+	 * fence but reads will not.
+	 *
+	 * If DMA_BUF_SYNC_WRITE is set, this inserts the sync_file as a
+	 * write fence.  All subsequent implicitly synchronized access to
+	 * this dma-buf will wait on this fence.
+	 */
+	__u32 flags;
+	/** @fd: Sync file descriptor */
+	__s32 fd;
+};
+
 #define DMA_BUF_BASE		'b'
 #define DMA_BUF_IOCTL_SYNC	_IOW(DMA_BUF_BASE, 0, struct dma_buf_sync)
 
@@ -94,5 +176,7 @@
 #define DMA_BUF_SET_NAME	_IOW(DMA_BUF_BASE, 1, const char *)
 #define DMA_BUF_SET_NAME_A	_IOW(DMA_BUF_BASE, 1, __u32)
 #define DMA_BUF_SET_NAME_B	_IOW(DMA_BUF_BASE, 1, __u64)
+#define DMA_BUF_IOCTL_EXPORT_SYNC_FILE	_IOWR(DMA_BUF_BASE, 2, struct dma_buf_export_sync_file)
+#define DMA_BUF_IOCTL_IMPORT_SYNC_FILE	_IOW(DMA_BUF_BASE, 3, struct dma_buf_import_sync_file)
 
 #endif
diff --git a/include/linux/drm_fourcc.h b/include/linux/drm_fourcc.h
index 1496e09..d8e9994 100644
--- a/include/linux/drm_fourcc.h
+++ b/include/linux/drm_fourcc.h
@@ -54,7 +54,7 @@
  * Format modifiers may change any property of the buffer, including the number
  * of planes and/or the required allocation size. Format modifiers are
  * vendor-namespaced, and as such the relationship between a fourcc code and a
- * modifier is specific to the modifer being used. For example, some modifiers
+ * modifier is specific to the modifier being used. For example, some modifiers
  * may preserve meaning - such as number of planes - from the fourcc code,
  * whereas others may not.
  *
@@ -79,7 +79,7 @@
  *   format.
  * - Higher-level programs interfacing with KMS/GBM/EGL/Vulkan/etc: these users
  *   see modifiers as opaque tokens they can check for equality and intersect.
- *   These users musn't need to know to reason about the modifier value
+ *   These users mustn't need to know to reason about the modifier value
  *   (i.e. they are not expected to extract information out of the modifier).
  *
  * Vendors should document their modifier usage in as much detail as
@@ -88,6 +88,18 @@
  *
  * The authoritative list of format modifier codes is found in
  * `include/uapi/drm/drm_fourcc.h`
+ *
+ * Open Source User Waiver
+ * -----------------------
+ *
+ * Because this is the authoritative source for pixel formats and modifiers
+ * referenced by GL, Vulkan extensions and other standards and hence used both
+ * by open source and closed source driver stacks, the usual requirement for an
+ * upstream in-kernel or open source userspace user does not apply.
+ *
+ * To ensure, as much as feasible, compatibility across stacks and avoid
+ * confusion with incompatible enumerations stakeholders for all relevant driver
+ * stacks should approve additions.
  */
 
 #define fourcc_code(a, b, c, d) ((__u32)(a) | ((__u32)(b) << 8) | \
@@ -99,18 +111,42 @@
 #define DRM_FORMAT_INVALID	0
 
 /* color index */
+#define DRM_FORMAT_C1		fourcc_code('C', '1', ' ', ' ') /* [7:0] C0:C1:C2:C3:C4:C5:C6:C7 1:1:1:1:1:1:1:1 eight pixels/byte */
+#define DRM_FORMAT_C2		fourcc_code('C', '2', ' ', ' ') /* [7:0] C0:C1:C2:C3 2:2:2:2 four pixels/byte */
+#define DRM_FORMAT_C4		fourcc_code('C', '4', ' ', ' ') /* [7:0] C0:C1 4:4 two pixels/byte */
 #define DRM_FORMAT_C8		fourcc_code('C', '8', ' ', ' ') /* [7:0] C */
 
-/* 8 bpp Red */
+/* 1 bpp Darkness (inverse relationship between channel value and brightness) */
+#define DRM_FORMAT_D1		fourcc_code('D', '1', ' ', ' ') /* [7:0] D0:D1:D2:D3:D4:D5:D6:D7 1:1:1:1:1:1:1:1 eight pixels/byte */
+
+/* 2 bpp Darkness (inverse relationship between channel value and brightness) */
+#define DRM_FORMAT_D2		fourcc_code('D', '2', ' ', ' ') /* [7:0] D0:D1:D2:D3 2:2:2:2 four pixels/byte */
+
+/* 4 bpp Darkness (inverse relationship between channel value and brightness) */
+#define DRM_FORMAT_D4		fourcc_code('D', '4', ' ', ' ') /* [7:0] D0:D1 4:4 two pixels/byte */
+
+/* 8 bpp Darkness (inverse relationship between channel value and brightness) */
+#define DRM_FORMAT_D8		fourcc_code('D', '8', ' ', ' ') /* [7:0] D */
+
+/* 1 bpp Red (direct relationship between channel value and brightness) */
+#define DRM_FORMAT_R1		fourcc_code('R', '1', ' ', ' ') /* [7:0] R0:R1:R2:R3:R4:R5:R6:R7 1:1:1:1:1:1:1:1 eight pixels/byte */
+
+/* 2 bpp Red (direct relationship between channel value and brightness) */
+#define DRM_FORMAT_R2		fourcc_code('R', '2', ' ', ' ') /* [7:0] R0:R1:R2:R3 2:2:2:2 four pixels/byte */
+
+/* 4 bpp Red (direct relationship between channel value and brightness) */
+#define DRM_FORMAT_R4		fourcc_code('R', '4', ' ', ' ') /* [7:0] R0:R1 4:4 two pixels/byte */
+
+/* 8 bpp Red (direct relationship between channel value and brightness) */
 #define DRM_FORMAT_R8		fourcc_code('R', '8', ' ', ' ') /* [7:0] R */
 
-/* 10 bpp Red */
+/* 10 bpp Red (direct relationship between channel value and brightness) */
 #define DRM_FORMAT_R10		fourcc_code('R', '1', '0', ' ') /* [15:0] x:R 6:10 little endian */
 
-/* 12 bpp Red */
+/* 12 bpp Red (direct relationship between channel value and brightness) */
 #define DRM_FORMAT_R12		fourcc_code('R', '1', '2', ' ') /* [15:0] x:R 4:12 little endian */
 
-/* 16 bpp Red */
+/* 16 bpp Red (direct relationship between channel value and brightness) */
 #define DRM_FORMAT_R16		fourcc_code('R', '1', '6', ' ') /* [15:0] R little endian */
 
 /* 16 bpp RG */
@@ -174,6 +210,10 @@
 #define DRM_FORMAT_RGBA1010102	fourcc_code('R', 'A', '3', '0') /* [31:0] R:G:B:A 10:10:10:2 little endian */
 #define DRM_FORMAT_BGRA1010102	fourcc_code('B', 'A', '3', '0') /* [31:0] B:G:R:A 10:10:10:2 little endian */
 
+/* 48 bpp RGB */
+#define DRM_FORMAT_RGB161616 fourcc_code('R', 'G', '4', '8') /* [47:0] R:G:B 16:16:16 little endian */
+#define DRM_FORMAT_BGR161616 fourcc_code('B', 'G', '4', '8') /* [47:0] B:G:R 16:16:16 little endian */
+
 /* 64 bpp RGB */
 #define DRM_FORMAT_XRGB16161616	fourcc_code('X', 'R', '4', '8') /* [63:0] x:R:G:B 16:16:16:16 little endian */
 #define DRM_FORMAT_XBGR16161616	fourcc_code('X', 'B', '4', '8') /* [63:0] x:B:G:R 16:16:16:16 little endian */
@@ -287,6 +327,8 @@
  * index 1 = Cr:Cb plane, [39:0] Cr1:Cb1:Cr0:Cb0 little endian
  */
 #define DRM_FORMAT_NV15		fourcc_code('N', 'V', '1', '5') /* 2x2 subsampled Cr:Cb plane */
+#define DRM_FORMAT_NV20		fourcc_code('N', 'V', '2', '0') /* 2x1 subsampled Cr:Cb plane */
+#define DRM_FORMAT_NV30		fourcc_code('N', 'V', '3', '0') /* non-subsampled Cr:Cb plane */
 
 /*
  * 2 plane YCbCr MSB aligned
@@ -448,6 +490,7 @@
 #define DRM_FORMAT_MOD_VENDOR_ALLWINNER 0x09
 #define DRM_FORMAT_MOD_VENDOR_AMLOGIC 0x0a
 #define DRM_FORMAT_MOD_VENDOR_MIPI 0x0b
+#define DRM_FORMAT_MOD_VENDOR_RPI 0x0c
 
 /* add more to the end as needed */
 
@@ -567,7 +610,7 @@
  * This is a tiled layout using 4Kb tiles in row-major layout.
  * Within the tile pixels are laid out in 16 256 byte units / sub-tiles which
  * are arranged in four groups (two wide, two high) with column-major layout.
- * Each group therefore consits out of four 256 byte units, which are also laid
+ * Each group therefore consists out of four 256 byte units, which are also laid
  * out as 2x2 column-major.
  * 256 byte units are made out of four 64 byte blocks of pixels, producing
  * either a square block or a 2:1 unit.
@@ -626,7 +669,7 @@
  *
  * The main surface is Y-tiled and is at plane index 0 whereas CCS is linear
  * and at index 1. The clear color is stored at index 2, and the pitch should
- * be ignored. The clear color structure is 256 bits. The first 128 bits
+ * be 64 bytes aligned. The clear color structure is 256 bits. The first 128 bits
  * represents Raw Clear Color Red, Green, Blue and Alpha color each represented
  * by 32 bits. The raw clear color is consumed by the 3d engine and generates
  * the converted clear color of size 64 bits. The first 32 bits store the Lower
@@ -679,14 +722,57 @@
  * outside of the GEM object in a reserved memory area dedicated for the
  * storage of the CCS data for all RC/RC_CC/MC compressible GEM objects. The
  * main surface pitch is required to be a multiple of four Tile 4 widths. The
- * clear color is stored at plane index 1 and the pitch should be ignored. The
- * format of the 256 bits of clear color data matches the one used for the
- * I915_FORMAT_MOD_Y_TILED_GEN12_RC_CCS_CC modifier, see its description
+ * clear color is stored at plane index 1 and the pitch should be 64 bytes
+ * aligned. The format of the 256 bits of clear color data matches the one used
+ * for the I915_FORMAT_MOD_Y_TILED_GEN12_RC_CCS_CC modifier, see its description
  * for details.
  */
 #define I915_FORMAT_MOD_4_TILED_DG2_RC_CCS_CC fourcc_mod_code(INTEL, 12)
 
 /*
+ * Intel Color Control Surfaces (CCS) for display ver. 14 render compression.
+ *
+ * The main surface is tile4 and at plane index 0, the CCS is linear and
+ * at index 1. A 64B CCS cache line corresponds to an area of 4x1 tiles in
+ * main surface. In other words, 4 bits in CCS map to a main surface cache
+ * line pair. The main surface pitch is required to be a multiple of four
+ * tile4 widths.
+ */
+#define I915_FORMAT_MOD_4_TILED_MTL_RC_CCS fourcc_mod_code(INTEL, 13)
+
+/*
+ * Intel Color Control Surfaces (CCS) for display ver. 14 media compression
+ *
+ * The main surface is tile4 and at plane index 0, the CCS is linear and
+ * at index 1. A 64B CCS cache line corresponds to an area of 4x1 tiles in
+ * main surface. In other words, 4 bits in CCS map to a main surface cache
+ * line pair. The main surface pitch is required to be a multiple of four
+ * tile4 widths. For semi-planar formats like NV12, CCS planes follow the
+ * Y and UV planes i.e., planes 0 and 1 are used for Y and UV surfaces,
+ * planes 2 and 3 for the respective CCS.
+ */
+#define I915_FORMAT_MOD_4_TILED_MTL_MC_CCS fourcc_mod_code(INTEL, 14)
+
+/*
+ * Intel Color Control Surface with Clear Color (CCS) for display ver. 14 render
+ * compression.
+ *
+ * The main surface is tile4 and is at plane index 0 whereas CCS is linear
+ * and at index 1. The clear color is stored at index 2, and the pitch should
+ * be ignored. The clear color structure is 256 bits. The first 128 bits
+ * represents Raw Clear Color Red, Green, Blue and Alpha color each represented
+ * by 32 bits. The raw clear color is consumed by the 3d engine and generates
+ * the converted clear color of size 64 bits. The first 32 bits store the Lower
+ * Converted Clear Color value and the next 32 bits store the Higher Converted
+ * Clear Color value when applicable. The Converted Clear Color values are
+ * consumed by the DE. The last 64 bits are used to store Color Discard Enable
+ * and Depth Clear Value Valid which are ignored by the DE. A CCS cache line
+ * corresponds to an area of 4x1 tiles in the main surface. The main surface
+ * pitch is required to be a multiple of 4 tile widths.
+ */
+#define I915_FORMAT_MOD_4_TILED_MTL_RC_CCS_CC fourcc_mod_code(INTEL, 15)
+
+/*
  * IPU3 Bayer packing layout
  *
  * The IPU3 raw Bayer formats use a custom packing layout where there are no
@@ -795,6 +881,35 @@
  */
 #define DRM_FORMAT_MOD_VIVANTE_SPLIT_SUPER_TILED fourcc_mod_code(VIVANTE, 4)
 
+/*
+ * Vivante TS (tile-status) buffer modifiers. They can be combined with all of
+ * the color buffer tiling modifiers defined above. When TS is present it's a
+ * separate buffer containing the clear/compression status of each tile. The
+ * modifiers are defined as VIVANTE_MOD_TS_c_s, where c is the color buffer
+ * tile size in bytes covered by one entry in the status buffer and s is the
+ * number of status bits per entry.
+ * We reserve the top 8 bits of the Vivante modifier space for tile status
+ * clear/compression modifiers, as future cores might add some more TS layout
+ * variations.
+ */
+#define VIVANTE_MOD_TS_64_4               (1ULL << 48)
+#define VIVANTE_MOD_TS_64_2               (2ULL << 48)
+#define VIVANTE_MOD_TS_128_4              (3ULL << 48)
+#define VIVANTE_MOD_TS_256_4              (4ULL << 48)
+#define VIVANTE_MOD_TS_MASK               (0xfULL << 48)
+
+/*
+ * Vivante compression modifiers. Those depend on a TS modifier being present
+ * as the TS bits get reinterpreted as compression tags instead of simple
+ * clear markers when compression is enabled.
+ */
+#define VIVANTE_MOD_COMP_DEC400           (1ULL << 52)
+#define VIVANTE_MOD_COMP_MASK             (0xfULL << 52)
+
+/* Masking out the extension bits will yield the base modifier. */
+#define VIVANTE_MOD_EXT_MASK              (VIVANTE_MOD_TS_MASK | \
+                                           VIVANTE_MOD_COMP_MASK)
+
 /* NVIDIA frame buffer modifiers */
 
 /*
@@ -1068,7 +1183,7 @@
  */
 
 /*
- * The top 4 bits (out of the 56 bits alloted for specifying vendor specific
+ * The top 4 bits (out of the 56 bits allotted for specifying vendor specific
  * modifiers) denote the category for modifiers. Currently we have three
  * categories of modifiers ie AFBC, MISC and AFRC. We can have a maximum of
  * sixteen different categories.
@@ -1384,7 +1499,7 @@
  * Amlogic FBC Memory Saving mode
  *
  * Indicates the storage is packed when pixel size is multiple of word
- * boudaries, i.e. 8bit should be stored in this mode to save allocation
+ * boundaries, i.e. 8bit should be stored in this mode to save allocation
  * memory.
  *
  * This mode reduces body layout to 3072 bytes per 64x32 superblock with
@@ -1440,6 +1555,7 @@
 #define AMD_FMT_MOD_TILE_VER_GFX9 1
 #define AMD_FMT_MOD_TILE_VER_GFX10 2
 #define AMD_FMT_MOD_TILE_VER_GFX10_RBPLUS 3
+#define AMD_FMT_MOD_TILE_VER_GFX11 4
 
 /*
  * 64K_S is the same for GFX9/GFX10/GFX10_RBPLUS and hence has GFX9 as canonical
@@ -1455,6 +1571,7 @@
 #define AMD_FMT_MOD_TILE_GFX9_64K_S_X 25
 #define AMD_FMT_MOD_TILE_GFX9_64K_D_X 26
 #define AMD_FMT_MOD_TILE_GFX9_64K_R_X 27
+#define AMD_FMT_MOD_TILE_GFX11_256K_R_X 31
 
 #define AMD_FMT_MOD_DCC_BLOCK_64B 0
 #define AMD_FMT_MOD_DCC_BLOCK_128B 1
@@ -1554,6 +1671,9 @@
  */
 #define MIPI_FORMAT_MOD_CSI2_PACKED fourcc_mod_code(MIPI, 1)
 
+#define PISP_FORMAT_MOD_COMPRESS_MODE1 fourcc_mod_code(RPI, 1)
+#define PISP_FORMAT_MOD_COMPRESS_MODE2 fourcc_mod_code(RPI, 2)
+
 #if defined(__cplusplus)
 }
 #endif
diff --git a/include/linux/intel-ipu3.h b/include/linux/intel-ipu3.h
index 5c298ec..8c192f3 100644
--- a/include/linux/intel-ipu3.h
+++ b/include/linux/intel-ipu3.h
@@ -626,8 +626,11 @@
  * @b:	white balance gain for B channel.
  * @gb:	white balance gain for Gb channel.
  *
- * Precision u3.13, range [0, 8). White balance correction is done by applying
- * a multiplicative gain to each color channels prior to BNR.
+ * For BNR parameters WB gain factor for the three channels [Ggr, Ggb, Gb, Gr].
+ * Their precision is U3.13 and the range is (0, 8) and the actual gain is
+ * Gx + 1, it is typically Gx = 1.
+ *
+ * Pout = {Pin * (1 + Gx)}.
  */
 struct ipu3_uapi_bnr_static_config_wb_gains_config {
 	__u16 gr;
@@ -2482,11 +2485,9 @@
  *		&ipu3_uapi_yuvp1_y_ee_nr_config
  * @yds:	y down scaler config. See &ipu3_uapi_yuvp1_yds_config
  * @chnr:	chroma noise reduction config. See &ipu3_uapi_yuvp1_chnr_config
- * @reserved1: reserved
  * @yds2:	y channel down scaler config. See &ipu3_uapi_yuvp1_yds_config
  * @tcc:	total color correction config as defined in struct
  *		&ipu3_uapi_yuvp2_tcc_static_config
- * @reserved2: reserved
  * @anr:	advanced noise reduction config.See &ipu3_uapi_anr_config
  * @awb_fr:	AWB filter response config. See ipu3_uapi_awb_fr_config
  * @ae:	auto exposure config  As specified by &ipu3_uapi_ae_config
@@ -2721,7 +2722,6 @@
  * @acc_ae: 0 = no update, 1 = update.
  * @acc_af: 0 = no update, 1 = update.
  * @acc_awb: 0 = no update, 1 = update.
- * @__acc_osys: 0 = no update, 1 = update.
  * @reserved3: Not used.
  * @lin_vmem_params: 0 = no update, 1 = update.
  * @tnr3_vmem_params: 0 = no update, 1 = update.
diff --git a/include/linux/media-bus-format.h b/include/linux/media-bus-format.h
index 0dfc11e..d4c1d99 100644
--- a/include/linux/media-bus-format.h
+++ b/include/linux/media-bus-format.h
@@ -34,7 +34,7 @@
 
 #define MEDIA_BUS_FMT_FIXED			0x0001
 
-/* RGB - next is	0x101e */
+/* RGB - next is	0x1026 */
 #define MEDIA_BUS_FMT_RGB444_1X12		0x1016
 #define MEDIA_BUS_FMT_RGB444_2X8_PADHI_BE	0x1001
 #define MEDIA_BUS_FMT_RGB444_2X8_PADHI_LE	0x1002
@@ -46,8 +46,12 @@
 #define MEDIA_BUS_FMT_RGB565_2X8_BE		0x1007
 #define MEDIA_BUS_FMT_RGB565_2X8_LE		0x1008
 #define MEDIA_BUS_FMT_RGB666_1X18		0x1009
+#define MEDIA_BUS_FMT_RGB666_2X9_BE		0x1025
+#define MEDIA_BUS_FMT_BGR666_1X18		0x1023
 #define MEDIA_BUS_FMT_RBG888_1X24		0x100e
 #define MEDIA_BUS_FMT_RGB666_1X24_CPADHI	0x1015
+#define MEDIA_BUS_FMT_BGR666_1X24_CPADHI	0x1024
+#define MEDIA_BUS_FMT_RGB565_1X24_CPADHI	0x1022
 #define MEDIA_BUS_FMT_RGB666_1X7X3_SPWG		0x1010
 #define MEDIA_BUS_FMT_BGR888_1X24		0x1013
 #define MEDIA_BUS_FMT_BGR888_3X8		0x101b
@@ -59,13 +63,17 @@
 #define MEDIA_BUS_FMT_RGB888_3X8_DELTA		0x101d
 #define MEDIA_BUS_FMT_RGB888_1X7X4_SPWG		0x1011
 #define MEDIA_BUS_FMT_RGB888_1X7X4_JEIDA	0x1012
+#define MEDIA_BUS_FMT_RGB666_1X30_CPADLO	0x101e
+#define MEDIA_BUS_FMT_RGB888_1X30_CPADLO	0x101f
 #define MEDIA_BUS_FMT_ARGB8888_1X32		0x100d
 #define MEDIA_BUS_FMT_RGB888_1X32_PADHI		0x100f
 #define MEDIA_BUS_FMT_RGB101010_1X30		0x1018
+#define MEDIA_BUS_FMT_RGB666_1X36_CPADLO	0x1020
+#define MEDIA_BUS_FMT_RGB888_1X36_CPADLO	0x1021
 #define MEDIA_BUS_FMT_RGB121212_1X36		0x1019
 #define MEDIA_BUS_FMT_RGB161616_1X48		0x101a
 
-/* YUV (including grey) - next is	0x202e */
+/* YUV (including grey) - next is	0x202f */
 #define MEDIA_BUS_FMT_Y8_1X8			0x2001
 #define MEDIA_BUS_FMT_UV8_1X8			0x2015
 #define MEDIA_BUS_FMT_UYVY8_1_5X8		0x2002
@@ -88,6 +96,7 @@
 #define MEDIA_BUS_FMT_YUYV12_2X12		0x201e
 #define MEDIA_BUS_FMT_YVYU12_2X12		0x201f
 #define MEDIA_BUS_FMT_Y14_1X14			0x202d
+#define MEDIA_BUS_FMT_Y16_1X16			0x202e
 #define MEDIA_BUS_FMT_UYVY8_1X16		0x200f
 #define MEDIA_BUS_FMT_VYUY8_1X16		0x2010
 #define MEDIA_BUS_FMT_YUYV8_1X16		0x2011
@@ -165,4 +174,13 @@
  */
 #define MEDIA_BUS_FMT_METADATA_FIXED		0x7001
 
+/* Generic line based metadata formats for serial buses. Next is 0x8008. */
+#define MEDIA_BUS_FMT_META_8			0x8001
+#define MEDIA_BUS_FMT_META_10			0x8002
+#define MEDIA_BUS_FMT_META_12			0x8003
+#define MEDIA_BUS_FMT_META_14			0x8004
+#define MEDIA_BUS_FMT_META_16			0x8005
+#define MEDIA_BUS_FMT_META_20			0x8006
+#define MEDIA_BUS_FMT_META_24			0x8007
+
 #endif /* __LINUX_MEDIA_BUS_FORMAT_H */
diff --git a/include/linux/media.h b/include/linux/media.h
index e3123d1..b5a77bb 100644
--- a/include/linux/media.h
+++ b/include/linux/media.h
@@ -20,7 +20,6 @@
 #ifndef __LINUX_MEDIA_H
 #define __LINUX_MEDIA_H
 
-#include <stdint.h>
 #include <linux/ioctl.h>
 #include <linux/types.h>
 
@@ -141,8 +140,8 @@
 #define MEDIA_ENT_F_DV_ENCODER			(MEDIA_ENT_F_BASE + 0x6002)
 
 /* Entity flags */
-#define MEDIA_ENT_FL_DEFAULT			(1 << 0)
-#define MEDIA_ENT_FL_CONNECTOR			(1 << 1)
+#define MEDIA_ENT_FL_DEFAULT			(1U << 0)
+#define MEDIA_ENT_FL_CONNECTOR			(1U << 1)
 
 /* OR with the entity id value to find the next entity */
 #define MEDIA_ENT_ID_FLAG_NEXT			(1U << 31)
@@ -204,9 +203,9 @@
 	};
 };
 
-#define MEDIA_PAD_FL_SINK			(1 << 0)
-#define MEDIA_PAD_FL_SOURCE			(1 << 1)
-#define MEDIA_PAD_FL_MUST_CONNECT		(1 << 2)
+#define MEDIA_PAD_FL_SINK			(1U << 0)
+#define MEDIA_PAD_FL_SOURCE			(1U << 1)
+#define MEDIA_PAD_FL_MUST_CONNECT		(1U << 2)
 
 struct media_pad_desc {
 	__u32 entity;		/* entity ID */
@@ -215,14 +214,14 @@
 	__u32 reserved[2];
 };
 
-#define MEDIA_LNK_FL_ENABLED			(1 << 0)
-#define MEDIA_LNK_FL_IMMUTABLE			(1 << 1)
-#define MEDIA_LNK_FL_DYNAMIC			(1 << 2)
+#define MEDIA_LNK_FL_ENABLED			(1U << 0)
+#define MEDIA_LNK_FL_IMMUTABLE			(1U << 1)
+#define MEDIA_LNK_FL_DYNAMIC			(1U << 2)
 
 #define MEDIA_LNK_FL_LINK_TYPE			(0xf << 28)
-#  define MEDIA_LNK_FL_DATA_LINK		(0 << 28)
-#  define MEDIA_LNK_FL_INTERFACE_LINK		(1 << 28)
-#  define MEDIA_LNK_FL_ANCILLARY_LINK		(2 << 28)
+#  define MEDIA_LNK_FL_DATA_LINK		(0U << 28)
+#  define MEDIA_LNK_FL_INTERFACE_LINK		(1U << 28)
+#  define MEDIA_LNK_FL_ANCILLARY_LINK		(2U << 28)
 
 struct media_link_desc {
 	struct media_pad_desc source;
@@ -277,7 +276,7 @@
  * struct media_device_info.
  */
 #define MEDIA_V2_ENTITY_HAS_FLAGS(media_version) \
-	((media_version) >= ((4 << 16) | (19 << 8) | 0))
+	((media_version) >= ((4U << 16) | (19U << 8) | 0U))
 
 struct media_v2_entity {
 	__u32 id;
@@ -312,7 +311,7 @@
  * struct media_device_info.
  */
 #define MEDIA_V2_PAD_HAS_INDEX(media_version) \
-	((media_version) >= ((4 << 16) | (19 << 8) | 0))
+	((media_version) >= ((4U << 16) | (19U << 8) | 0U))
 
 struct media_v2_pad {
 	__u32 id;
@@ -415,7 +414,7 @@
 #define MEDIA_INTF_T_ALSA_TIMER                (MEDIA_INTF_T_ALSA_BASE + 7)
 
 /* Obsolete symbol for media_version, no longer used in the kernel */
-#define MEDIA_API_VERSION			((0 << 16) | (1 << 8) | 0)
+#define MEDIA_API_VERSION			((0U << 16) | (1U << 8) | 0U)
 
 
 #endif /* __LINUX_MEDIA_H */
diff --git a/include/linux/rkisp1-config.h b/include/linux/rkisp1-config.h
index ec7cde8..f87c6bd 100644
--- a/include/linux/rkisp1-config.h
+++ b/include/linux/rkisp1-config.h
@@ -175,16 +175,21 @@
 /**
  * enum rkisp1_cif_isp_version - ISP variants
  *
- * @RKISP1_V10: used at least in rk3288 and rk3399
- * @RKISP1_V11: declared in the original vendor code, but not used
- * @RKISP1_V12: used at least in rk3326 and px30
- * @RKISP1_V13: used at least in rk1808
+ * @RKISP1_V10: Used at least in RK3288 and RK3399.
+ * @RKISP1_V11: Declared in the original vendor code, but not used. Same number
+ *	of entries in grids and histogram as v10.
+ * @RKISP1_V12: Used at least in RK3326 and PX30.
+ * @RKISP1_V13: Used at least in RK1808. Same number of entries in grids and
+ *	histogram as v12.
+ * @RKISP1_V_IMX8MP: Used in at least i.MX8MP. Same number of entries in grids
+ *	and histogram as v10.
  */
 enum rkisp1_cif_isp_version {
 	RKISP1_V10 = 10,
 	RKISP1_V11,
 	RKISP1_V12,
 	RKISP1_V13,
+	RKISP1_V_IMX8MP,
 };
 
 enum rkisp1_cif_isp_histogram_mode {
@@ -584,10 +589,9 @@
  * as is reported by the hw_revision field of the struct media_device_info
  * that is returned by ioctl MEDIA_IOC_DEVICE_INFO.
  *
- * Versions <= V11 have RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10
- * entries, versions >= V12 have RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V12
- * entries. RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES is equal to the maximum
- * of the two.
+ * V10 has RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10 entries, V12 has
+ * RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V12 entries.
+ * RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES is equal to the maximum of the two.
  */
 struct rkisp1_cif_isp_goc_config {
 	__u32 mode;
@@ -607,10 +611,10 @@
  * as is reported by the hw_revision field of the struct media_device_info
  * that is returned by ioctl MEDIA_IOC_DEVICE_INFO.
  *
- * Versions <= V11 have RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V10
- * entries, versions >= V12 have RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V12
- * entries. RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE is equal to the maximum
- * of the two.
+ * V10 has RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V10 entries, V12 has
+ * RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V12 entries.
+ * RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE is equal to the maximum of the
+ * two.
  */
 struct rkisp1_cif_isp_hst_config {
 	__u32 mode;
@@ -902,9 +906,9 @@
  * as is reported by the hw_revision field of the struct media_device_info
  * that is returned by ioctl MEDIA_IOC_DEVICE_INFO.
  *
- * Versions <= V11 have RKISP1_CIF_ISP_AE_MEAN_MAX_V10 entries,
- * versions >= V12 have RKISP1_CIF_ISP_AE_MEAN_MAX_V12 entries.
- * RKISP1_CIF_ISP_AE_MEAN_MAX is equal to the maximum of the two.
+ * V10 has RKISP1_CIF_ISP_AE_MEAN_MAX_V10 entries, V12 has
+ * RKISP1_CIF_ISP_AE_MEAN_MAX_V12 entries. RKISP1_CIF_ISP_AE_MEAN_MAX is equal
+ * to the maximum of the two.
  *
  * Image is divided into 5x5 blocks on V10 and 9x9 blocks on V12.
  */
@@ -944,21 +948,21 @@
  *	       integer part.
  *
  * The window of the measurements area is divided to 5x5 sub-windows for
- * V10/V11 and to 9x9 sub-windows for V12. The histogram is then computed for
- * each sub-window independently and the final result is a weighted average of
- * the histogram measurements on all sub-windows. The window of the
- * measurements area and the weight of each sub-window are configurable using
+ * V10 and to 9x9 sub-windows for V12. The histogram is then computed for each
+ * sub-window independently and the final result is a weighted average of the
+ * histogram measurements on all sub-windows. The window of the measurements
+ * area and the weight of each sub-window are configurable using
  * struct @rkisp1_cif_isp_hst_config.
  *
- * The histogram contains 16 bins in V10/V11 and 32 bins in V12/V13.
+ * The histogram contains 16 bins in V10 and 32 bins in V12.
  *
  * The number of entries of @hist_bins depends on the hardware revision
  * as is reported by the hw_revision field of the struct media_device_info
  * that is returned by ioctl MEDIA_IOC_DEVICE_INFO.
  *
- * Versions <= V11 have RKISP1_CIF_ISP_HIST_BIN_N_MAX_V10 entries,
- * versions >= V12 have RKISP1_CIF_ISP_HIST_BIN_N_MAX_V12 entries.
- * RKISP1_CIF_ISP_HIST_BIN_N_MAX is equal to the maximum of the two.
+ * V10 has RKISP1_CIF_ISP_HIST_BIN_N_MAX_V10 entries, V12 has
+ * RKISP1_CIF_ISP_HIST_BIN_N_MAX_V12 entries. RKISP1_CIF_ISP_HIST_BIN_N_MAX is
+ * equal to the maximum of the two.
  */
 struct rkisp1_cif_isp_hist_stat {
 	__u32 hist_bins[RKISP1_CIF_ISP_HIST_BIN_N_MAX];
diff --git a/include/linux/udmabuf.h b/include/linux/udmabuf.h
new file mode 100644
index 0000000..76cc7de
--- /dev/null
+++ b/include/linux/udmabuf.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
+#ifndef _LINUX_UDMABUF_H
+#define _LINUX_UDMABUF_H
+
+#include <linux/types.h>
+#include <linux/ioctl.h>
+
+#define UDMABUF_FLAGS_CLOEXEC	0x01
+
+struct udmabuf_create {
+	__u32 memfd;
+	__u32 flags;
+	__u64 offset;
+	__u64 size;
+};
+
+struct udmabuf_create_item {
+	__u32 memfd;
+	__u32 __pad;
+	__u64 offset;
+	__u64 size;
+};
+
+struct udmabuf_create_list {
+	__u32 flags;
+	__u32 count;
+	struct udmabuf_create_item list[];
+};
+
+#define UDMABUF_CREATE       _IOW('u', 0x42, struct udmabuf_create)
+#define UDMABUF_CREATE_LIST  _IOW('u', 0x43, struct udmabuf_create_list)
+
+#endif /* _LINUX_UDMABUF_H */
diff --git a/include/linux/v4l2-common.h b/include/linux/v4l2-common.h
index 14de173..c3ca11e 100644
--- a/include/linux/v4l2-common.h
+++ b/include/linux/v4l2-common.h
@@ -10,45 +10,6 @@
  *
  * Copyright (C) 2012 Nokia Corporation
  * Contact: Sakari Ailus <sakari.ailus@iki.fi>
- *
- *  This program is free software; you can redistribute it and/or modify
- *  it under the terms of the GNU General Public License as published by
- *  the Free Software Foundation; either version 2 of the License, or
- *  (at your option) any later version.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU General Public License for more details.
- *
- *  Alternatively you can redistribute this file under the terms of the
- *  BSD license as stated below:
- *
- *  Redistribution and use in source and binary forms, with or without
- *  modification, are permitted provided that the following conditions
- *  are met:
- *  1. Redistributions of source code must retain the above copyright
- *     notice, this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright
- *     notice, this list of conditions and the following disclaimer in
- *     the documentation and/or other materials provided with the
- *     distribution.
- *  3. The names of its contributors may not be used to endorse or promote
- *     products derived from this software without specific prior written
- *     permission.
- *
- *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
- *  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
  */
 
 #ifndef __V4L2_COMMON__
diff --git a/include/linux/v4l2-controls.h b/include/linux/v4l2-controls.h
index 9d2a823..1e6e816 100644
--- a/include/linux/v4l2-controls.h
+++ b/include/linux/v4l2-controls.h
@@ -4,44 +4,6 @@
  *
  *  Copyright (C) 1999-2012 the contributors
  *
- *  This program is free software; you can redistribute it and/or modify
- *  it under the terms of the GNU General Public License as published by
- *  the Free Software Foundation; either version 2 of the License, or
- *  (at your option) any later version.
- *
- *  This program is distributed in the hope that it will be useful,
- *  but WITHOUT ANY WARRANTY; without even the implied warranty of
- *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- *  GNU General Public License for more details.
- *
- *  Alternatively you can redistribute this file under the terms of the
- *  BSD license as stated below:
- *
- *  Redistribution and use in source and binary forms, with or without
- *  modification, are permitted provided that the following conditions
- *  are met:
- *  1. Redistributions of source code must retain the above copyright
- *     notice, this list of conditions and the following disclaimer.
- *  2. Redistributions in binary form must reproduce the above copyright
- *     notice, this list of conditions and the following disclaimer in
- *     the documentation and/or other materials provided with the
- *     distribution.
- *  3. The names of its contributors may not be used to endorse or promote
- *     products derived from this software without specific prior written
- *     permission.
- *
- *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
- *  TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
- *  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
- *  LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
- *  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
- *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- *
  *  The contents of this header was split off from videodev2.h. All control
  *  definitions should be added to this header, which is included by
  *  videodev2.h.
@@ -153,8 +115,10 @@
 
 /* USER-class private control IDs */
 
-/* The base for the meye driver controls. See linux/meye.h for the list
- * of controls. We reserve 16 controls for this driver. */
+/*
+ * The base for the meye driver controls. This driver was removed, but
+ * we keep this define in case any software still uses it.
+ */
 #define V4L2_CID_USER_MEYE_BASE			(V4L2_CID_USER_BASE + 0x1000)
 
 /* The base for the bttv driver controls.
@@ -229,6 +193,30 @@
  */
 #define V4L2_CID_USER_ISL7998X_BASE		(V4L2_CID_USER_BASE + 0x1180)
 
+/*
+ * The base for DW100 driver controls.
+ * We reserve 16 controls for this driver.
+ */
+#define V4L2_CID_USER_DW100_BASE		(V4L2_CID_USER_BASE + 0x1190)
+
+/*
+ * The base for Aspeed driver controls.
+ * We reserve 16 controls for this driver.
+ */
+#define V4L2_CID_USER_ASPEED_BASE		(V4L2_CID_USER_BASE + 0x11a0)
+
+/*
+ * The base for Nuvoton NPCM driver controls.
+ * We reserve 16 controls for this driver.
+ */
+#define V4L2_CID_USER_NPCM_BASE			(V4L2_CID_USER_BASE + 0x11b0)
+
+/*
+ * The base for THine THP7312 driver controls.
+ * We reserve 32 controls for this driver.
+ */
+#define V4L2_CID_USER_THP7312_BASE		(V4L2_CID_USER_BASE + 0x11c0)
+
 /* MPEG-class control IDs */
 /* The MPEG controls are applicable to all codec controls
  * and the 'MPEG' part of the define is historical */
@@ -828,6 +816,88 @@
 #define V4L2_CID_MPEG_VIDEO_DEC_DISPLAY_DELAY		(V4L2_CID_CODEC_BASE + 653)
 #define V4L2_CID_MPEG_VIDEO_DEC_DISPLAY_DELAY_ENABLE	(V4L2_CID_CODEC_BASE + 654)
 
+#define V4L2_CID_MPEG_VIDEO_AV1_PROFILE (V4L2_CID_CODEC_BASE + 655)
+/**
+ * enum v4l2_mpeg_video_av1_profile - AV1 profiles
+ *
+ * @V4L2_MPEG_VIDEO_AV1_PROFILE_MAIN: compliant decoders must be able to decode
+ * streams with seq_profile equal to 0.
+ * @V4L2_MPEG_VIDEO_AV1_PROFILE_HIGH: compliant decoders must be able to decode
+ * streams with seq_profile equal less than or equal to 1.
+ * @V4L2_MPEG_VIDEO_AV1_PROFILE_PROFESSIONAL: compliant decoders must be able to
+ * decode streams with seq_profile less than or equal to 2.
+ *
+ * Conveys the highest profile a decoder can work with.
+ */
+enum v4l2_mpeg_video_av1_profile {
+	V4L2_MPEG_VIDEO_AV1_PROFILE_MAIN = 0,
+	V4L2_MPEG_VIDEO_AV1_PROFILE_HIGH = 1,
+	V4L2_MPEG_VIDEO_AV1_PROFILE_PROFESSIONAL = 2,
+};
+
+#define V4L2_CID_MPEG_VIDEO_AV1_LEVEL (V4L2_CID_CODEC_BASE + 656)
+/**
+ * enum v4l2_mpeg_video_av1_level - AV1 levels
+ *
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_2_0: Level 2.0.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_2_1: Level 2.1.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_2_2: Level 2.2.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_2_3: Level 2.3.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_3_0: Level 3.0.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_3_1: Level 3.1.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_3_2: Level 3.2.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_3_3: Level 3.3.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_4_0: Level 4.0.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_4_1: Level 4.1.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_4_2: Level 4.2.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_4_3: Level 4.3.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_5_0: Level 5.0.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_5_1: Level 5.1.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_5_2: Level 5.2.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_5_3: Level 5.3.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_6_0: Level 6.0.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_6_1: Level 6.1.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_6_2: Level 6.2.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_6_3: Level 6.3.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_7_0: Level 7.0.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_7_1: Level 7.1.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_7_2: Level 7.2.
+ * @V4L2_MPEG_VIDEO_AV1_LEVEL_7_3: Level 7.3.
+ *
+ * Conveys the highest level a decoder can work with.
+ */
+enum v4l2_mpeg_video_av1_level {
+	V4L2_MPEG_VIDEO_AV1_LEVEL_2_0 = 0,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_2_1 = 1,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_2_2 = 2,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_2_3 = 3,
+
+	V4L2_MPEG_VIDEO_AV1_LEVEL_3_0 = 4,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_3_1 = 5,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_3_2 = 6,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_3_3 = 7,
+
+	V4L2_MPEG_VIDEO_AV1_LEVEL_4_0 = 8,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_4_1 = 9,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_4_2 = 10,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_4_3 = 11,
+
+	V4L2_MPEG_VIDEO_AV1_LEVEL_5_0 = 12,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_5_1 = 13,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_5_2 = 14,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_5_3 = 15,
+
+	V4L2_MPEG_VIDEO_AV1_LEVEL_6_0 = 16,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_6_1 = 17,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_6_2 = 18,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_6_3 = 19,
+
+	V4L2_MPEG_VIDEO_AV1_LEVEL_7_0 = 20,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_7_1 = 21,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_7_2 = 22,
+	V4L2_MPEG_VIDEO_AV1_LEVEL_7_3 = 23
+};
+
 /*  MPEG-class control IDs specific to the CX2341x driver as defined by V4L2 */
 #define V4L2_CID_CODEC_CX2341X_BASE				(V4L2_CTRL_CLASS_CODEC | 0x1000)
 #define V4L2_CID_MPEG_CX2341X_VIDEO_SPATIAL_FILTER_MODE		(V4L2_CID_CODEC_CX2341X_BASE+0)
@@ -1015,6 +1085,8 @@
 
 #define V4L2_CID_CAMERA_SENSOR_ROTATION		(V4L2_CID_CAMERA_CLASS_BASE+35)
 
+#define V4L2_CID_HDR_SENSOR_MODE		(V4L2_CID_CAMERA_CLASS_BASE+36)
+
 /* FM Modulator class control IDs */
 
 #define V4L2_CID_FM_TX_CLASS_BASE		(V4L2_CTRL_CLASS_FM_TX | 0x900)
@@ -1732,7 +1804,7 @@
  * @sharpness_level: matches sharpness_level syntax element.
  * @level: matches loop_filter_level syntax element.
  * @padding: padding field. Should be zeroed by applications.
- * @flags: see V4L2_VP8_LF_FLAG_{}.
+ * @flags: see V4L2_VP8_LF_{}.
  *
  * This structure contains loop filter related parameters.
  * See the 'mb_lf_adjustments()' part of the frame header syntax,
@@ -1999,6 +2071,469 @@
 	__u8	chroma_non_intra_quantiser_matrix[64];
 };
 
+#define V4L2_CID_STATELESS_HEVC_SPS		(V4L2_CID_CODEC_STATELESS_BASE + 400)
+#define V4L2_CID_STATELESS_HEVC_PPS		(V4L2_CID_CODEC_STATELESS_BASE + 401)
+#define V4L2_CID_STATELESS_HEVC_SLICE_PARAMS	(V4L2_CID_CODEC_STATELESS_BASE + 402)
+#define V4L2_CID_STATELESS_HEVC_SCALING_MATRIX	(V4L2_CID_CODEC_STATELESS_BASE + 403)
+#define V4L2_CID_STATELESS_HEVC_DECODE_PARAMS	(V4L2_CID_CODEC_STATELESS_BASE + 404)
+#define V4L2_CID_STATELESS_HEVC_DECODE_MODE	(V4L2_CID_CODEC_STATELESS_BASE + 405)
+#define V4L2_CID_STATELESS_HEVC_START_CODE	(V4L2_CID_CODEC_STATELESS_BASE + 406)
+#define V4L2_CID_STATELESS_HEVC_ENTRY_POINT_OFFSETS (V4L2_CID_CODEC_STATELESS_BASE + 407)
+
+enum v4l2_stateless_hevc_decode_mode {
+	V4L2_STATELESS_HEVC_DECODE_MODE_SLICE_BASED,
+	V4L2_STATELESS_HEVC_DECODE_MODE_FRAME_BASED,
+};
+
+enum v4l2_stateless_hevc_start_code {
+	V4L2_STATELESS_HEVC_START_CODE_NONE,
+	V4L2_STATELESS_HEVC_START_CODE_ANNEX_B,
+};
+
+#define V4L2_HEVC_SLICE_TYPE_B	0
+#define V4L2_HEVC_SLICE_TYPE_P	1
+#define V4L2_HEVC_SLICE_TYPE_I	2
+
+#define V4L2_HEVC_SPS_FLAG_SEPARATE_COLOUR_PLANE		(1ULL << 0)
+#define V4L2_HEVC_SPS_FLAG_SCALING_LIST_ENABLED			(1ULL << 1)
+#define V4L2_HEVC_SPS_FLAG_AMP_ENABLED				(1ULL << 2)
+#define V4L2_HEVC_SPS_FLAG_SAMPLE_ADAPTIVE_OFFSET		(1ULL << 3)
+#define V4L2_HEVC_SPS_FLAG_PCM_ENABLED				(1ULL << 4)
+#define V4L2_HEVC_SPS_FLAG_PCM_LOOP_FILTER_DISABLED		(1ULL << 5)
+#define V4L2_HEVC_SPS_FLAG_LONG_TERM_REF_PICS_PRESENT		(1ULL << 6)
+#define V4L2_HEVC_SPS_FLAG_SPS_TEMPORAL_MVP_ENABLED		(1ULL << 7)
+#define V4L2_HEVC_SPS_FLAG_STRONG_INTRA_SMOOTHING_ENABLED	(1ULL << 8)
+
+/**
+ * struct v4l2_ctrl_hevc_sps - ITU-T Rec. H.265: Sequence parameter set
+ *
+ * @video_parameter_set_id: specifies the value of the
+ *			vps_video_parameter_set_id of the active VPS
+ * @seq_parameter_set_id: provides an identifier for the SPS for
+ *			  reference by other syntax elements
+ * @pic_width_in_luma_samples:	specifies the width of each decoded picture
+ *				in units of luma samples
+ * @pic_height_in_luma_samples: specifies the height of each decoded picture
+ *				in units of luma samples
+ * @bit_depth_luma_minus8: this value plus 8specifies the bit depth of the
+ *                         samples of the luma array
+ * @bit_depth_chroma_minus8: this value plus 8 specifies the bit depth of the
+ *                           samples of the chroma arrays
+ * @log2_max_pic_order_cnt_lsb_minus4: this value plus 4 specifies the value of
+ *                                     the variable MaxPicOrderCntLsb
+ * @sps_max_dec_pic_buffering_minus1: this value plus 1 specifies the maximum
+ *                                    required size of the decoded picture
+ *                                    buffer for the codec video sequence
+ * @sps_max_num_reorder_pics: indicates the maximum allowed number of pictures
+ * @sps_max_latency_increase_plus1: not equal to 0 is used to compute the
+ *				    value of SpsMaxLatencyPictures array
+ * @log2_min_luma_coding_block_size_minus3: plus 3 specifies the minimum
+ *					    luma coding block size
+ * @log2_diff_max_min_luma_coding_block_size: specifies the difference between
+ *					      the maximum and minimum luma
+ *					      coding block size
+ * @log2_min_luma_transform_block_size_minus2: plus 2 specifies the minimum luma
+ *					       transform block size
+ * @log2_diff_max_min_luma_transform_block_size: specifies the difference between
+ *						 the maximum and minimum luma
+ *						 transform block size
+ * @max_transform_hierarchy_depth_inter: specifies the maximum hierarchy
+ *					 depth for transform units of
+ *					 coding units coded in inter
+ *					 prediction mode
+ * @max_transform_hierarchy_depth_intra: specifies the maximum hierarchy
+ *					 depth for transform units of
+ *					 coding units coded in intra
+ *					 prediction mode
+ * @pcm_sample_bit_depth_luma_minus1: this value plus 1 specifies the number of
+ *                                    bits used to represent each of PCM sample
+ *                                    values of the luma component
+ * @pcm_sample_bit_depth_chroma_minus1: this value plus 1 specifies the number
+ *                                      of bits used to represent each of PCM
+ *                                      sample values of the chroma components
+ * @log2_min_pcm_luma_coding_block_size_minus3: this value plus 3 specifies the
+ *                                              minimum size of coding blocks
+ * @log2_diff_max_min_pcm_luma_coding_block_size: specifies the difference between
+ *						  the maximum and minimum size of
+ *						  coding blocks
+ * @num_short_term_ref_pic_sets: specifies the number of st_ref_pic_set()
+ *				 syntax structures included in the SPS
+ * @num_long_term_ref_pics_sps: specifies the number of candidate long-term
+ *				reference pictures that are specified in the SPS
+ * @chroma_format_idc: specifies the chroma sampling
+ * @sps_max_sub_layers_minus1: this value plus 1 specifies the maximum number
+ *                             of temporal sub-layers
+ * @reserved: padding field. Should be zeroed by applications.
+ * @flags: see V4L2_HEVC_SPS_FLAG_{}
+ */
+struct v4l2_ctrl_hevc_sps {
+	__u8	video_parameter_set_id;
+	__u8	seq_parameter_set_id;
+	__u16	pic_width_in_luma_samples;
+	__u16	pic_height_in_luma_samples;
+	__u8	bit_depth_luma_minus8;
+	__u8	bit_depth_chroma_minus8;
+	__u8	log2_max_pic_order_cnt_lsb_minus4;
+	__u8	sps_max_dec_pic_buffering_minus1;
+	__u8	sps_max_num_reorder_pics;
+	__u8	sps_max_latency_increase_plus1;
+	__u8	log2_min_luma_coding_block_size_minus3;
+	__u8	log2_diff_max_min_luma_coding_block_size;
+	__u8	log2_min_luma_transform_block_size_minus2;
+	__u8	log2_diff_max_min_luma_transform_block_size;
+	__u8	max_transform_hierarchy_depth_inter;
+	__u8	max_transform_hierarchy_depth_intra;
+	__u8	pcm_sample_bit_depth_luma_minus1;
+	__u8	pcm_sample_bit_depth_chroma_minus1;
+	__u8	log2_min_pcm_luma_coding_block_size_minus3;
+	__u8	log2_diff_max_min_pcm_luma_coding_block_size;
+	__u8	num_short_term_ref_pic_sets;
+	__u8	num_long_term_ref_pics_sps;
+	__u8	chroma_format_idc;
+	__u8	sps_max_sub_layers_minus1;
+
+	__u8	reserved[6];
+	__u64	flags;
+};
+
+#define V4L2_HEVC_PPS_FLAG_DEPENDENT_SLICE_SEGMENT_ENABLED	(1ULL << 0)
+#define V4L2_HEVC_PPS_FLAG_OUTPUT_FLAG_PRESENT			(1ULL << 1)
+#define V4L2_HEVC_PPS_FLAG_SIGN_DATA_HIDING_ENABLED		(1ULL << 2)
+#define V4L2_HEVC_PPS_FLAG_CABAC_INIT_PRESENT			(1ULL << 3)
+#define V4L2_HEVC_PPS_FLAG_CONSTRAINED_INTRA_PRED		(1ULL << 4)
+#define V4L2_HEVC_PPS_FLAG_TRANSFORM_SKIP_ENABLED		(1ULL << 5)
+#define V4L2_HEVC_PPS_FLAG_CU_QP_DELTA_ENABLED			(1ULL << 6)
+#define V4L2_HEVC_PPS_FLAG_PPS_SLICE_CHROMA_QP_OFFSETS_PRESENT	(1ULL << 7)
+#define V4L2_HEVC_PPS_FLAG_WEIGHTED_PRED			(1ULL << 8)
+#define V4L2_HEVC_PPS_FLAG_WEIGHTED_BIPRED			(1ULL << 9)
+#define V4L2_HEVC_PPS_FLAG_TRANSQUANT_BYPASS_ENABLED		(1ULL << 10)
+#define V4L2_HEVC_PPS_FLAG_TILES_ENABLED			(1ULL << 11)
+#define V4L2_HEVC_PPS_FLAG_ENTROPY_CODING_SYNC_ENABLED		(1ULL << 12)
+#define V4L2_HEVC_PPS_FLAG_LOOP_FILTER_ACROSS_TILES_ENABLED	(1ULL << 13)
+#define V4L2_HEVC_PPS_FLAG_PPS_LOOP_FILTER_ACROSS_SLICES_ENABLED (1ULL << 14)
+#define V4L2_HEVC_PPS_FLAG_DEBLOCKING_FILTER_OVERRIDE_ENABLED	(1ULL << 15)
+#define V4L2_HEVC_PPS_FLAG_PPS_DISABLE_DEBLOCKING_FILTER	(1ULL << 16)
+#define V4L2_HEVC_PPS_FLAG_LISTS_MODIFICATION_PRESENT		(1ULL << 17)
+#define V4L2_HEVC_PPS_FLAG_SLICE_SEGMENT_HEADER_EXTENSION_PRESENT (1ULL << 18)
+#define V4L2_HEVC_PPS_FLAG_DEBLOCKING_FILTER_CONTROL_PRESENT	(1ULL << 19)
+#define V4L2_HEVC_PPS_FLAG_UNIFORM_SPACING			(1ULL << 20)
+
+/**
+ * struct v4l2_ctrl_hevc_pps - ITU-T Rec. H.265: Picture parameter set
+ *
+ * @pic_parameter_set_id: identifies the PPS for reference by other
+ *			  syntax elements
+ * @num_extra_slice_header_bits: specifies the number of extra slice header
+ *				 bits that are present in the slice header RBSP
+ *				 for coded pictures referring to the PPS.
+ * @num_ref_idx_l0_default_active_minus1: this value plus 1 specifies the
+ *                                        inferred value of num_ref_idx_l0_active_minus1
+ * @num_ref_idx_l1_default_active_minus1: this value plus 1 specifies the
+ *                                        inferred value of num_ref_idx_l1_active_minus1
+ * @init_qp_minus26: this value plus 26 specifies the initial value of SliceQp Y for
+ *		     each slice referring to the PPS
+ * @diff_cu_qp_delta_depth: specifies the difference between the luma coding
+ *			    tree block size and the minimum luma coding block
+ *			    size of coding units that convey cu_qp_delta_abs
+ *			    and cu_qp_delta_sign_flag
+ * @pps_cb_qp_offset: specify the offsets to the luma quantization parameter Cb
+ * @pps_cr_qp_offset: specify the offsets to the luma quantization parameter Cr
+ * @num_tile_columns_minus1: this value plus 1 specifies the number of tile columns
+ *			     partitioning the picture
+ * @num_tile_rows_minus1: this value plus 1 specifies the number of tile rows partitioning
+ *			  the picture
+ * @column_width_minus1: this value plus 1 specifies the width of the each tile column in
+ *			 units of coding tree blocks
+ * @row_height_minus1: this value plus 1 specifies the height of the each tile row in
+ *		       units of coding tree blocks
+ * @pps_beta_offset_div2: specify the default deblocking parameter offsets for
+ *			  beta divided by 2
+ * @pps_tc_offset_div2: specify the default deblocking parameter offsets for tC
+ *			divided by 2
+ * @log2_parallel_merge_level_minus2: this value plus 2 specifies the value of
+ *                                    the variable Log2ParMrgLevel
+ * @reserved: padding field. Should be zeroed by applications.
+ * @flags: see V4L2_HEVC_PPS_FLAG_{}
+ */
+struct v4l2_ctrl_hevc_pps {
+	__u8	pic_parameter_set_id;
+	__u8	num_extra_slice_header_bits;
+	__u8	num_ref_idx_l0_default_active_minus1;
+	__u8	num_ref_idx_l1_default_active_minus1;
+	__s8	init_qp_minus26;
+	__u8	diff_cu_qp_delta_depth;
+	__s8	pps_cb_qp_offset;
+	__s8	pps_cr_qp_offset;
+	__u8	num_tile_columns_minus1;
+	__u8	num_tile_rows_minus1;
+	__u8	column_width_minus1[20];
+	__u8	row_height_minus1[22];
+	__s8	pps_beta_offset_div2;
+	__s8	pps_tc_offset_div2;
+	__u8	log2_parallel_merge_level_minus2;
+	__u8	reserved;
+	__u64	flags;
+};
+
+#define V4L2_HEVC_DPB_ENTRY_LONG_TERM_REFERENCE	0x01
+
+#define V4L2_HEVC_SEI_PIC_STRUCT_FRAME				0
+#define V4L2_HEVC_SEI_PIC_STRUCT_TOP_FIELD			1
+#define V4L2_HEVC_SEI_PIC_STRUCT_BOTTOM_FIELD			2
+#define V4L2_HEVC_SEI_PIC_STRUCT_TOP_BOTTOM			3
+#define V4L2_HEVC_SEI_PIC_STRUCT_BOTTOM_TOP			4
+#define V4L2_HEVC_SEI_PIC_STRUCT_TOP_BOTTOM_TOP			5
+#define V4L2_HEVC_SEI_PIC_STRUCT_BOTTOM_TOP_BOTTOM		6
+#define V4L2_HEVC_SEI_PIC_STRUCT_FRAME_DOUBLING			7
+#define V4L2_HEVC_SEI_PIC_STRUCT_FRAME_TRIPLING			8
+#define V4L2_HEVC_SEI_PIC_STRUCT_TOP_PAIRED_PREVIOUS_BOTTOM	9
+#define V4L2_HEVC_SEI_PIC_STRUCT_BOTTOM_PAIRED_PREVIOUS_TOP	10
+#define V4L2_HEVC_SEI_PIC_STRUCT_TOP_PAIRED_NEXT_BOTTOM		11
+#define V4L2_HEVC_SEI_PIC_STRUCT_BOTTOM_PAIRED_NEXT_TOP		12
+
+#define V4L2_HEVC_DPB_ENTRIES_NUM_MAX		16
+
+/**
+ * struct v4l2_hevc_dpb_entry - HEVC decoded picture buffer entry
+ *
+ * @timestamp: timestamp of the V4L2 capture buffer to use as reference.
+ * @flags: long term flag for the reference frame
+ * @field_pic: whether the reference is a field picture or a frame.
+ * @reserved: padding field. Should be zeroed by applications.
+ * @pic_order_cnt_val: the picture order count of the current picture.
+ */
+struct v4l2_hevc_dpb_entry {
+	__u64	timestamp;
+	__u8	flags;
+	__u8	field_pic;
+	__u16	reserved;
+	__s32	pic_order_cnt_val;
+};
+
+/**
+ * struct v4l2_hevc_pred_weight_table - HEVC weighted prediction parameters
+ *
+ * @delta_luma_weight_l0: the difference of the weighting factor applied
+ *			  to the luma prediction value for list 0
+ * @luma_offset_l0: the additive offset applied to the luma prediction value
+ *		    for list 0
+ * @delta_chroma_weight_l0: the difference of the weighting factor applied
+ *			    to the chroma prediction values for list 0
+ * @chroma_offset_l0: the difference of the additive offset applied to
+ *		      the chroma prediction values for list 0
+ * @delta_luma_weight_l1: the difference of the weighting factor applied
+ *			  to the luma prediction value for list 1
+ * @luma_offset_l1: the additive offset applied to the luma prediction value
+ *		    for list 1
+ * @delta_chroma_weight_l1: the difference of the weighting factor applied
+ *			    to the chroma prediction values for list 1
+ * @chroma_offset_l1: the difference of the additive offset applied to
+ *		      the chroma prediction values for list 1
+ * @luma_log2_weight_denom: the base 2 logarithm of the denominator for
+ *			    all luma weighting factors
+ * @delta_chroma_log2_weight_denom: the difference of the base 2 logarithm
+ *				    of the denominator for all chroma
+ *				    weighting factors
+ */
+struct v4l2_hevc_pred_weight_table {
+	__s8	delta_luma_weight_l0[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__s8	luma_offset_l0[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__s8	delta_chroma_weight_l0[V4L2_HEVC_DPB_ENTRIES_NUM_MAX][2];
+	__s8	chroma_offset_l0[V4L2_HEVC_DPB_ENTRIES_NUM_MAX][2];
+
+	__s8	delta_luma_weight_l1[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__s8	luma_offset_l1[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__s8	delta_chroma_weight_l1[V4L2_HEVC_DPB_ENTRIES_NUM_MAX][2];
+	__s8	chroma_offset_l1[V4L2_HEVC_DPB_ENTRIES_NUM_MAX][2];
+
+	__u8	luma_log2_weight_denom;
+	__s8	delta_chroma_log2_weight_denom;
+};
+
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_SLICE_SAO_LUMA		(1ULL << 0)
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_SLICE_SAO_CHROMA		(1ULL << 1)
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_SLICE_TEMPORAL_MVP_ENABLED	(1ULL << 2)
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_MVD_L1_ZERO			(1ULL << 3)
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_CABAC_INIT			(1ULL << 4)
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_COLLOCATED_FROM_L0		(1ULL << 5)
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_USE_INTEGER_MV		(1ULL << 6)
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_SLICE_DEBLOCKING_FILTER_DISABLED (1ULL << 7)
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_SLICE_LOOP_FILTER_ACROSS_SLICES_ENABLED (1ULL << 8)
+#define V4L2_HEVC_SLICE_PARAMS_FLAG_DEPENDENT_SLICE_SEGMENT	(1ULL << 9)
+
+/**
+ * struct v4l2_ctrl_hevc_slice_params - HEVC slice parameters
+ *
+ * This control is a dynamically sized 1-dimensional array,
+ * V4L2_CTRL_FLAG_DYNAMIC_ARRAY flag must be set when using it.
+ *
+ * @bit_size: size (in bits) of the current slice data
+ * @data_byte_offset: offset (in bytes) to the video data in the current slice data
+ * @num_entry_point_offsets: specifies the number of entry point offset syntax
+ *			     elements in the slice header.
+ * @nal_unit_type: specifies the coding type of the slice (B, P or I)
+ * @nuh_temporal_id_plus1: minus 1 specifies a temporal identifier for the NAL unit
+ * @slice_type: see V4L2_HEVC_SLICE_TYPE_{}
+ * @colour_plane_id: specifies the colour plane associated with the current slice
+ * @slice_pic_order_cnt: specifies the picture order count
+ * @num_ref_idx_l0_active_minus1: this value plus 1 specifies the maximum
+ *                                reference index for reference picture list 0
+ *                                that may be used to decode the slice
+ * @num_ref_idx_l1_active_minus1: this value plus 1 specifies the maximum
+ *                                reference index for reference picture list 1
+ *                                that may be used to decode the slice
+ * @collocated_ref_idx: specifies the reference index of the collocated picture used
+ *			for temporal motion vector prediction
+ * @five_minus_max_num_merge_cand: specifies the maximum number of merging
+ *				   motion vector prediction candidates supported in
+ *				   the slice subtracted from 5
+ * @slice_qp_delta: specifies the initial value of QpY to be used for the coding
+ *		    blocks in the slice
+ * @slice_cb_qp_offset: specifies a difference to be added to the value of pps_cb_qp_offset
+ * @slice_cr_qp_offset: specifies a difference to be added to the value of pps_cr_qp_offset
+ * @slice_act_y_qp_offset: screen content extension parameters
+ * @slice_act_cb_qp_offset: screen content extension parameters
+ * @slice_act_cr_qp_offset: screen content extension parameters
+ * @slice_beta_offset_div2: specify the deblocking parameter offsets for beta divided by 2
+ * @slice_tc_offset_div2: specify the deblocking parameter offsets for tC divided by 2
+ * @pic_struct: indicates whether a picture should be displayed as a frame or as one or
+ *		more fields
+ * @reserved0: padding field. Should be zeroed by applications.
+ * @slice_segment_addr: specifies the address of the first coding tree block in
+ *			the slice segment
+ * @ref_idx_l0: the list of L0 reference elements as indices in the DPB
+ * @ref_idx_l1: the list of L1 reference elements as indices in the DPB
+ * @short_term_ref_pic_set_size: specifies the size of short-term reference
+ *				 pictures set included in the SPS
+ * @long_term_ref_pic_set_size: specifies the size of long-term reference
+ *				pictures set include in the SPS
+ * @pred_weight_table: the prediction weight coefficients for inter-picture
+ *		       prediction
+ * @reserved1: padding field. Should be zeroed by applications.
+ * @flags: see V4L2_HEVC_SLICE_PARAMS_FLAG_{}
+ */
+struct v4l2_ctrl_hevc_slice_params {
+	__u32	bit_size;
+	__u32	data_byte_offset;
+	__u32	num_entry_point_offsets;
+
+	/* ISO/IEC 23008-2, ITU-T Rec. H.265: NAL unit header */
+	__u8	nal_unit_type;
+	__u8	nuh_temporal_id_plus1;
+
+	/* ISO/IEC 23008-2, ITU-T Rec. H.265: General slice segment header */
+	__u8	slice_type;
+	__u8	colour_plane_id;
+	__s32	slice_pic_order_cnt;
+	__u8	num_ref_idx_l0_active_minus1;
+	__u8	num_ref_idx_l1_active_minus1;
+	__u8	collocated_ref_idx;
+	__u8	five_minus_max_num_merge_cand;
+	__s8	slice_qp_delta;
+	__s8	slice_cb_qp_offset;
+	__s8	slice_cr_qp_offset;
+	__s8	slice_act_y_qp_offset;
+	__s8	slice_act_cb_qp_offset;
+	__s8	slice_act_cr_qp_offset;
+	__s8	slice_beta_offset_div2;
+	__s8	slice_tc_offset_div2;
+
+	/* ISO/IEC 23008-2, ITU-T Rec. H.265: Picture timing SEI message */
+	__u8	pic_struct;
+
+	__u8	reserved0[3];
+	/* ISO/IEC 23008-2, ITU-T Rec. H.265: General slice segment header */
+	__u32	slice_segment_addr;
+	__u8	ref_idx_l0[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__u8	ref_idx_l1[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__u16	short_term_ref_pic_set_size;
+	__u16	long_term_ref_pic_set_size;
+
+	/* ISO/IEC 23008-2, ITU-T Rec. H.265: Weighted prediction parameter */
+	struct v4l2_hevc_pred_weight_table pred_weight_table;
+
+	__u8	reserved1[2];
+	__u64	flags;
+};
+
+#define V4L2_HEVC_DECODE_PARAM_FLAG_IRAP_PIC		0x1
+#define V4L2_HEVC_DECODE_PARAM_FLAG_IDR_PIC		0x2
+#define V4L2_HEVC_DECODE_PARAM_FLAG_NO_OUTPUT_OF_PRIOR  0x4
+
+/**
+ * struct v4l2_ctrl_hevc_decode_params - HEVC decode parameters
+ *
+ * @pic_order_cnt_val: picture order count
+ * @short_term_ref_pic_set_size: specifies the size of short-term reference
+ *				 pictures set included in the SPS of the first slice
+ * @long_term_ref_pic_set_size: specifies the size of long-term reference
+ *				pictures set include in the SPS of the first slice
+ * @num_active_dpb_entries: the number of entries in dpb
+ * @num_poc_st_curr_before: the number of reference pictures in the short-term
+ *			    set that come before the current frame
+ * @num_poc_st_curr_after: the number of reference pictures in the short-term
+ *			   set that come after the current frame
+ * @num_poc_lt_curr: the number of reference pictures in the long-term set
+ * @poc_st_curr_before: provides the index of the short term before references
+ *			in DPB array
+ * @poc_st_curr_after: provides the index of the short term after references
+ *		       in DPB array
+ * @poc_lt_curr: provides the index of the long term references in DPB array
+ * @num_delta_pocs_of_ref_rps_idx: same as the derived value NumDeltaPocs[RefRpsIdx],
+ *				   can be used to parse the RPS data in slice headers
+ *				   instead of skipping it with @short_term_ref_pic_set_size.
+ * @reserved: padding field. Should be zeroed by applications.
+ * @dpb: the decoded picture buffer, for meta-data about reference frames
+ * @flags: see V4L2_HEVC_DECODE_PARAM_FLAG_{}
+ */
+struct v4l2_ctrl_hevc_decode_params {
+	__s32	pic_order_cnt_val;
+	__u16	short_term_ref_pic_set_size;
+	__u16	long_term_ref_pic_set_size;
+	__u8	num_active_dpb_entries;
+	__u8	num_poc_st_curr_before;
+	__u8	num_poc_st_curr_after;
+	__u8	num_poc_lt_curr;
+	__u8	poc_st_curr_before[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__u8	poc_st_curr_after[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__u8	poc_lt_curr[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__u8	num_delta_pocs_of_ref_rps_idx;
+	__u8	reserved[3];
+	struct	v4l2_hevc_dpb_entry dpb[V4L2_HEVC_DPB_ENTRIES_NUM_MAX];
+	__u64	flags;
+};
+
+/**
+ * struct v4l2_ctrl_hevc_scaling_matrix - HEVC scaling lists parameters
+ *
+ * @scaling_list_4x4: scaling list is used for the scaling process for
+ *		      transform coefficients. The values on each scaling
+ *		      list are expected in raster scan order
+ * @scaling_list_8x8: scaling list is used for the scaling process for
+ *		      transform coefficients. The values on each scaling
+ *		      list are expected in raster scan order
+ * @scaling_list_16x16:	scaling list is used for the scaling process for
+ *			transform coefficients. The values on each scaling
+ *			list are expected in raster scan order
+ * @scaling_list_32x32:	scaling list is used for the scaling process for
+ *			transform coefficients. The values on each scaling
+ *			list are expected in raster scan order
+ * @scaling_list_dc_coef_16x16:	scaling list is used for the scaling process
+ *				for transform coefficients. The values on each
+ *				scaling list are expected in raster scan order.
+ * @scaling_list_dc_coef_32x32:	scaling list is used for the scaling process
+ *				for transform coefficients. The values on each
+ *				scaling list are expected in raster scan order.
+ */
+struct v4l2_ctrl_hevc_scaling_matrix {
+	__u8	scaling_list_4x4[6][16];
+	__u8	scaling_list_8x8[6][64];
+	__u8	scaling_list_16x16[6][64];
+	__u8	scaling_list_32x32[2][64];
+	__u8	scaling_list_dc_coef_16x16[6];
+	__u8	scaling_list_dc_coef_32x32[2];
+};
+
 #define V4L2_CID_COLORIMETRY_CLASS_BASE	(V4L2_CTRL_CLASS_COLORIMETRY | 0x900)
 #define V4L2_CID_COLORIMETRY_CLASS	(V4L2_CTRL_CLASS_COLORIMETRY | 1)
 
@@ -2317,6 +2852,645 @@
 	struct v4l2_vp9_mv_probs mv;
 };
 
+/* Stateless AV1 controls */
+
+#define V4L2_AV1_TOTAL_REFS_PER_FRAME	8
+#define V4L2_AV1_CDEF_MAX		8
+#define V4L2_AV1_NUM_PLANES_MAX		3 /* 1 if monochrome, 3 otherwise */
+#define V4L2_AV1_MAX_SEGMENTS		8
+#define V4L2_AV1_MAX_OPERATING_POINTS	(1 << 5) /* 5 bits to encode */
+#define V4L2_AV1_REFS_PER_FRAME		7
+#define V4L2_AV1_MAX_NUM_Y_POINTS	(1 << 4) /* 4 bits to encode */
+#define V4L2_AV1_MAX_NUM_CB_POINTS	(1 << 4) /* 4 bits to encode */
+#define V4L2_AV1_MAX_NUM_CR_POINTS	(1 << 4) /* 4 bits to encode */
+#define V4L2_AV1_AR_COEFFS_SIZE		25 /* (2 * 3 * (3 + 1)) + 1 */
+#define V4L2_AV1_MAX_NUM_PLANES		3
+#define V4L2_AV1_MAX_TILE_COLS		64
+#define V4L2_AV1_MAX_TILE_ROWS		64
+#define V4L2_AV1_MAX_TILE_COUNT		512
+
+#define V4L2_AV1_SEQUENCE_FLAG_STILL_PICTURE		  0x00000001
+#define V4L2_AV1_SEQUENCE_FLAG_USE_128X128_SUPERBLOCK	  0x00000002
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_FILTER_INTRA	  0x00000004
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_INTRA_EDGE_FILTER   0x00000008
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_INTERINTRA_COMPOUND 0x00000010
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_MASKED_COMPOUND	  0x00000020
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_WARPED_MOTION	  0x00000040
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_DUAL_FILTER	  0x00000080
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_ORDER_HINT	  0x00000100
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_JNT_COMP		  0x00000200
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_REF_FRAME_MVS	  0x00000400
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_SUPERRES		  0x00000800
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_CDEF		  0x00001000
+#define V4L2_AV1_SEQUENCE_FLAG_ENABLE_RESTORATION	  0x00002000
+#define V4L2_AV1_SEQUENCE_FLAG_MONO_CHROME		  0x00004000
+#define V4L2_AV1_SEQUENCE_FLAG_COLOR_RANGE		  0x00008000
+#define V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_X		  0x00010000
+#define V4L2_AV1_SEQUENCE_FLAG_SUBSAMPLING_Y		  0x00020000
+#define V4L2_AV1_SEQUENCE_FLAG_FILM_GRAIN_PARAMS_PRESENT  0x00040000
+#define V4L2_AV1_SEQUENCE_FLAG_SEPARATE_UV_DELTA_Q	  0x00080000
+
+#define V4L2_CID_STATELESS_AV1_SEQUENCE (V4L2_CID_CODEC_STATELESS_BASE + 500)
+/**
+ * struct v4l2_ctrl_av1_sequence - AV1 Sequence
+ *
+ * Represents an AV1 Sequence OBU. See section 5.5 "Sequence header OBU syntax"
+ * for more details.
+ *
+ * @flags: See V4L2_AV1_SEQUENCE_FLAG_{}.
+ * @seq_profile: specifies the features that can be used in the coded video
+ * sequence.
+ * @order_hint_bits: specifies the number of bits used for the order_hint field
+ * at each frame.
+ * @bit_depth: the bitdepth to use for the sequence as described in section
+ * 5.5.2 "Color config syntax".
+ * @reserved: padding field. Should be zeroed by applications.
+ * @max_frame_width_minus_1: specifies the maximum frame width minus 1 for the
+ * frames represented by this sequence header.
+ * @max_frame_height_minus_1: specifies the maximum frame height minus 1 for the
+ * frames represented by this sequence header.
+ */
+struct v4l2_ctrl_av1_sequence {
+	__u32 flags;
+	__u8 seq_profile;
+	__u8 order_hint_bits;
+	__u8 bit_depth;
+	__u8 reserved;
+	__u16 max_frame_width_minus_1;
+	__u16 max_frame_height_minus_1;
+};
+
+#define V4L2_CID_STATELESS_AV1_TILE_GROUP_ENTRY (V4L2_CID_CODEC_STATELESS_BASE + 501)
+/**
+ * struct v4l2_ctrl_av1_tile_group_entry - AV1 Tile Group entry
+ *
+ * Represents a single AV1 tile inside an AV1 Tile Group. Note that MiRowStart,
+ * MiRowEnd, MiColStart and MiColEnd can be retrieved from struct
+ * v4l2_av1_tile_info in struct v4l2_ctrl_av1_frame using tile_row and
+ * tile_col. See section 6.10.1 "General tile group OBU semantics" for more
+ * details.
+ *
+ * @tile_offset: offset from the OBU data, i.e. where the coded tile data
+ * actually starts.
+ * @tile_size: specifies the size in bytes of the coded tile. Equivalent to
+ * "TileSize" in the AV1 Specification.
+ * @tile_row: specifies the row of the current tile. Equivalent to "TileRow" in
+ * the AV1 Specification.
+ * @tile_col: specifies the col of the current tile. Equivalent to "TileCol" in
+ * the AV1 Specification.
+ */
+struct v4l2_ctrl_av1_tile_group_entry {
+	__u32 tile_offset;
+	__u32 tile_size;
+	__u32 tile_row;
+	__u32 tile_col;
+};
+
+/**
+ * enum v4l2_av1_warp_model - AV1 Warp Model as described in section 3
+ * "Symbols and abbreviated terms" of the AV1 Specification.
+ *
+ * @V4L2_AV1_WARP_MODEL_IDENTITY: Warp model is just an identity transform.
+ * @V4L2_AV1_WARP_MODEL_TRANSLATION: Warp model is a pure translation.
+ * @V4L2_AV1_WARP_MODEL_ROTZOOM: Warp model is a rotation + symmetric zoom +
+ * translation.
+ * @V4L2_AV1_WARP_MODEL_AFFINE: Warp model is a general affine transform.
+ */
+enum v4l2_av1_warp_model {
+	V4L2_AV1_WARP_MODEL_IDENTITY = 0,
+	V4L2_AV1_WARP_MODEL_TRANSLATION = 1,
+	V4L2_AV1_WARP_MODEL_ROTZOOM = 2,
+	V4L2_AV1_WARP_MODEL_AFFINE = 3,
+};
+
+/**
+ * enum v4l2_av1_reference_frame - AV1 reference frames
+ *
+ * @V4L2_AV1_REF_INTRA_FRAME: Intra Frame Reference
+ * @V4L2_AV1_REF_LAST_FRAME: Last Reference Frame
+ * @V4L2_AV1_REF_LAST2_FRAME: Last2 Reference Frame
+ * @V4L2_AV1_REF_LAST3_FRAME: Last3 Reference Frame
+ * @V4L2_AV1_REF_GOLDEN_FRAME: Golden Reference Frame
+ * @V4L2_AV1_REF_BWDREF_FRAME: BWD Reference Frame
+ * @V4L2_AV1_REF_ALTREF2_FRAME: Alternative2 Reference Frame
+ * @V4L2_AV1_REF_ALTREF_FRAME: Alternative Reference Frame
+ */
+enum v4l2_av1_reference_frame {
+	V4L2_AV1_REF_INTRA_FRAME = 0,
+	V4L2_AV1_REF_LAST_FRAME = 1,
+	V4L2_AV1_REF_LAST2_FRAME = 2,
+	V4L2_AV1_REF_LAST3_FRAME = 3,
+	V4L2_AV1_REF_GOLDEN_FRAME = 4,
+	V4L2_AV1_REF_BWDREF_FRAME = 5,
+	V4L2_AV1_REF_ALTREF2_FRAME = 6,
+	V4L2_AV1_REF_ALTREF_FRAME = 7,
+};
+
+#define V4L2_AV1_GLOBAL_MOTION_IS_INVALID(ref) (1 << (ref))
+
+#define V4L2_AV1_GLOBAL_MOTION_FLAG_IS_GLOBAL	   0x1
+#define V4L2_AV1_GLOBAL_MOTION_FLAG_IS_ROT_ZOOM	   0x2
+#define V4L2_AV1_GLOBAL_MOTION_FLAG_IS_TRANSLATION 0x4
+/**
+ * struct v4l2_av1_global_motion - AV1 Global Motion parameters as described in
+ * section 6.8.17 "Global motion params semantics" of the AV1 specification.
+ *
+ * @flags: A bitfield containing the flags per reference frame. See
+ * V4L2_AV1_GLOBAL_MOTION_FLAG_{}
+ * @type: The type of global motion transform used.
+ * @params: this field has the same meaning as "gm_params" in the AV1
+ * specification.
+ * @invalid: bitfield indicating whether the global motion params are invalid
+ * for a given reference frame. See section 7.11.3.6 Setup shear process and
+ * the variable "warpValid". Use V4L2_AV1_GLOBAL_MOTION_IS_INVALID(ref) to
+ * create a suitable mask.
+ * @reserved: padding field. Should be zeroed by applications.
+ */
+
+struct v4l2_av1_global_motion {
+	__u8 flags[V4L2_AV1_TOTAL_REFS_PER_FRAME];
+	enum v4l2_av1_warp_model type[V4L2_AV1_TOTAL_REFS_PER_FRAME];
+	__s32 params[V4L2_AV1_TOTAL_REFS_PER_FRAME][6];
+	__u8 invalid;
+	__u8 reserved[3];
+};
+
+/**
+ * enum v4l2_av1_frame_restoration_type - AV1 Frame Restoration Type
+ * @V4L2_AV1_FRAME_RESTORE_NONE: no filtering is applied.
+ * @V4L2_AV1_FRAME_RESTORE_WIENER: Wiener filter process is invoked.
+ * @V4L2_AV1_FRAME_RESTORE_SGRPROJ: self guided filter process is invoked.
+ * @V4L2_AV1_FRAME_RESTORE_SWITCHABLE: restoration filter is swichtable.
+ */
+enum v4l2_av1_frame_restoration_type {
+	V4L2_AV1_FRAME_RESTORE_NONE = 0,
+	V4L2_AV1_FRAME_RESTORE_WIENER = 1,
+	V4L2_AV1_FRAME_RESTORE_SGRPROJ = 2,
+	V4L2_AV1_FRAME_RESTORE_SWITCHABLE = 3,
+};
+
+#define V4L2_AV1_LOOP_RESTORATION_FLAG_USES_LR		0x1
+#define V4L2_AV1_LOOP_RESTORATION_FLAG_USES_CHROMA_LR	0x2
+
+/**
+ * struct v4l2_av1_loop_restoration - AV1 Loop Restauration as described in
+ * section 6.10.15 "Loop restoration params semantics" of the AV1 specification.
+ *
+ * @flags: See V4L2_AV1_LOOP_RESTORATION_FLAG_{}.
+ * @lr_unit_shift: specifies if the luma restoration size should be halved.
+ * @lr_uv_shift: specifies if the chroma size should be half the luma size.
+ * @reserved: padding field. Should be zeroed by applications.
+ * @frame_restoration_type: specifies the type of restoration used for each
+ * plane. See enum v4l2_av1_frame_restoration_type.
+ * @loop_restoration_size: specifies the size of loop restoration units in units
+ * of samples in the current plane.
+ */
+struct v4l2_av1_loop_restoration {
+	__u8 flags;
+	__u8 lr_unit_shift;
+	__u8 lr_uv_shift;
+	__u8 reserved;
+	enum v4l2_av1_frame_restoration_type frame_restoration_type[V4L2_AV1_NUM_PLANES_MAX];
+	__u32 loop_restoration_size[V4L2_AV1_MAX_NUM_PLANES];
+};
+
+/**
+ * struct v4l2_av1_cdef - AV1 CDEF params semantics as described in section
+ * 6.10.14 "CDEF params semantics" of the AV1 specification
+ *
+ * @damping_minus_3: controls the amount of damping in the deringing filter.
+ * @bits: specifies the number of bits needed to specify which CDEF filter to
+ * apply.
+ * @y_pri_strength: specifies the strength of the primary filter.
+ * @y_sec_strength: specifies the strength of the secondary filter.
+ * @uv_pri_strength: specifies the strength of the primary filter.
+ * @uv_sec_strength: specifies the strength of the secondary filter.
+ */
+struct v4l2_av1_cdef {
+	__u8 damping_minus_3;
+	__u8 bits;
+	__u8 y_pri_strength[V4L2_AV1_CDEF_MAX];
+	__u8 y_sec_strength[V4L2_AV1_CDEF_MAX];
+	__u8 uv_pri_strength[V4L2_AV1_CDEF_MAX];
+	__u8 uv_sec_strength[V4L2_AV1_CDEF_MAX];
+};
+
+#define V4L2_AV1_SEGMENTATION_FLAG_ENABLED	   0x1
+#define V4L2_AV1_SEGMENTATION_FLAG_UPDATE_MAP	   0x2
+#define V4L2_AV1_SEGMENTATION_FLAG_TEMPORAL_UPDATE 0x4
+#define V4L2_AV1_SEGMENTATION_FLAG_UPDATE_DATA	   0x8
+#define V4L2_AV1_SEGMENTATION_FLAG_SEG_ID_PRE_SKIP 0x10
+
+/**
+ * enum v4l2_av1_segment_feature - AV1 segment features as described in section
+ * 3 "Symbols and abbreviated terms" of the AV1 specification.
+ *
+ * @V4L2_AV1_SEG_LVL_ALT_Q: Index for quantizer segment feature.
+ * @V4L2_AV1_SEG_LVL_ALT_LF_Y_V: Index for vertical luma loop filter segment
+ * feature.
+ * @V4L2_AV1_SEG_LVL_REF_FRAME: Index for reference frame segment feature.
+ * @V4L2_AV1_SEG_LVL_REF_SKIP: Index for skip segment feature.
+ * @V4L2_AV1_SEG_LVL_REF_GLOBALMV: Index for global mv feature.
+ * @V4L2_AV1_SEG_LVL_MAX: Number of segment features.
+ */
+enum v4l2_av1_segment_feature {
+	V4L2_AV1_SEG_LVL_ALT_Q = 0,
+	V4L2_AV1_SEG_LVL_ALT_LF_Y_V = 1,
+	V4L2_AV1_SEG_LVL_REF_FRAME = 5,
+	V4L2_AV1_SEG_LVL_REF_SKIP = 6,
+	V4L2_AV1_SEG_LVL_REF_GLOBALMV = 7,
+	V4L2_AV1_SEG_LVL_MAX = 8
+};
+
+#define V4L2_AV1_SEGMENT_FEATURE_ENABLED(id)	(1 << (id))
+
+/**
+ * struct v4l2_av1_segmentation - AV1 Segmentation params as defined in section
+ * 6.8.13 "Segmentation params semantics" of the AV1 specification.
+ *
+ * @flags: see V4L2_AV1_SEGMENTATION_FLAG_{}.
+ * @last_active_seg_id: indicates the highest numbered segment id that has some
+ * enabled feature. This is used when decoding the segment id to only decode
+ * choices corresponding to used segments.
+ * @feature_enabled: bitmask defining which features are enabled in each
+ * segment. Use V4L2_AV1_SEGMENT_FEATURE_ENABLED to build a suitable mask.
+ * @feature_data: data attached to each feature. Data entry is only valid if the
+ * feature is enabled
+ */
+struct v4l2_av1_segmentation {
+	__u8 flags;
+	__u8 last_active_seg_id;
+	__u8 feature_enabled[V4L2_AV1_MAX_SEGMENTS];
+	__s16 feature_data[V4L2_AV1_MAX_SEGMENTS][V4L2_AV1_SEG_LVL_MAX];
+};
+
+#define V4L2_AV1_LOOP_FILTER_FLAG_DELTA_ENABLED    0x1
+#define V4L2_AV1_LOOP_FILTER_FLAG_DELTA_UPDATE     0x2
+#define V4L2_AV1_LOOP_FILTER_FLAG_DELTA_LF_PRESENT 0x4
+#define V4L2_AV1_LOOP_FILTER_FLAG_DELTA_LF_MULTI   0x8
+
+/**
+ * struct v4l2_av1_loop_filter - AV1 Loop filter params as defined in section
+ * 6.8.10 "Loop filter semantics" and 6.8.16 "Loop filter delta parameters
+ * semantics" of the AV1 specification.
+ *
+ * @flags: see V4L2_AV1_LOOP_FILTER_FLAG_{}
+ * @level: an array containing loop filter strength values. Different loop
+ * filter strength values from the array are used depending on the image plane
+ * being filtered, and the edge direction (vertical or horizontal) being
+ * filtered.
+ * @sharpness: indicates the sharpness level. The loop_filter_level and
+ * loop_filter_sharpness together determine when a block edge is filtered, and
+ * by how much the filtering can change the sample values. The loop filter
+ * process is described in section 7.14 of the AV1 specification.
+ * @ref_deltas: contains the adjustment needed for the filter level based on the
+ * chosen reference frame. If this syntax element is not present, it maintains
+ * its previous value.
+ * @mode_deltas: contains the adjustment needed for the filter level based on
+ * the chosen mode. If this syntax element is not present, it maintains its
+ * previous value.
+ * @delta_lf_res: specifies the left shift which should be applied to decoded
+ * loop filter delta values.
+ */
+struct v4l2_av1_loop_filter {
+	__u8 flags;
+	__u8 level[4];
+	__u8 sharpness;
+	__s8 ref_deltas[V4L2_AV1_TOTAL_REFS_PER_FRAME];
+	__s8 mode_deltas[2];
+	__u8 delta_lf_res;
+};
+
+#define V4L2_AV1_QUANTIZATION_FLAG_DIFF_UV_DELTA   0x1
+#define V4L2_AV1_QUANTIZATION_FLAG_USING_QMATRIX   0x2
+#define V4L2_AV1_QUANTIZATION_FLAG_DELTA_Q_PRESENT 0x4
+
+/**
+ * struct v4l2_av1_quantization - AV1 Quantization params as defined in section
+ * 6.8.11 "Quantization params semantics" of the AV1 specification.
+ *
+ * @flags: see V4L2_AV1_QUANTIZATION_FLAG_{}
+ * @base_q_idx: indicates the base frame qindex. This is used for Y AC
+ * coefficients and as the base value for the other quantizers.
+ * @delta_q_y_dc: indicates the Y DC quantizer relative to base_q_idx.
+ * @delta_q_u_dc: indicates the U DC quantizer relative to base_q_idx.
+ * @delta_q_u_ac: indicates the U AC quantizer relative to base_q_idx.
+ * @delta_q_v_dc: indicates the V DC quantizer relative to base_q_idx.
+ * @delta_q_v_ac: indicates the V AC quantizer relative to base_q_idx.
+ * @qm_y: specifies the level in the quantizer matrix that should be used for
+ * luma plane decoding.
+ * @qm_u: specifies the level in the quantizer matrix that should be used for
+ * chroma U plane decoding.
+ * @qm_v: specifies the level in the quantizer matrix that should be used for
+ * chroma V plane decoding.
+ * @delta_q_res: specifies the left shift which should be applied to decoded
+ * quantizer index delta values.
+ */
+struct v4l2_av1_quantization {
+	__u8 flags;
+	__u8 base_q_idx;
+	__s8 delta_q_y_dc;
+	__s8 delta_q_u_dc;
+	__s8 delta_q_u_ac;
+	__s8 delta_q_v_dc;
+	__s8 delta_q_v_ac;
+	__u8 qm_y;
+	__u8 qm_u;
+	__u8 qm_v;
+	__u8 delta_q_res;
+};
+
+#define V4L2_AV1_TILE_INFO_FLAG_UNIFORM_TILE_SPACING	0x1
+
+/**
+ * struct v4l2_av1_tile_info - AV1 Tile info as defined in section 6.8.14 "Tile
+ * info semantics" of the AV1 specification.
+ *
+ * @flags: see V4L2_AV1_TILE_INFO_FLAG_{}
+ * @context_update_tile_id: specifies which tile to use for the CDF update.
+ * @tile_rows: specifies the number of tiles down the frame.
+ * @tile_cols: specifies the number of tiles across the frame.
+ * @mi_col_starts: an array specifying the start column (in units of 4x4 luma
+ * samples) for each tile across the image.
+ * @mi_row_starts: an array specifying the start row (in units of 4x4 luma
+ * samples) for each tile down the image.
+ * @width_in_sbs_minus_1: specifies the width of a tile minus 1 in units of
+ * superblocks.
+ * @height_in_sbs_minus_1:  specifies the height of a tile minus 1 in units of
+ * superblocks.
+ * @tile_size_bytes: specifies the number of bytes needed to code each tile
+ * size.
+ * @reserved: padding field. Should be zeroed by applications.
+ */
+struct v4l2_av1_tile_info {
+	__u8 flags;
+	__u8 context_update_tile_id;
+	__u8 tile_cols;
+	__u8 tile_rows;
+	__u32 mi_col_starts[V4L2_AV1_MAX_TILE_COLS + 1];
+	__u32 mi_row_starts[V4L2_AV1_MAX_TILE_ROWS + 1];
+	__u32 width_in_sbs_minus_1[V4L2_AV1_MAX_TILE_COLS];
+	__u32 height_in_sbs_minus_1[V4L2_AV1_MAX_TILE_ROWS];
+	__u8 tile_size_bytes;
+	__u8 reserved[3];
+};
+
+/**
+ * enum v4l2_av1_frame_type - AV1 Frame Type
+ *
+ * @V4L2_AV1_KEY_FRAME: Key frame
+ * @V4L2_AV1_INTER_FRAME: Inter frame
+ * @V4L2_AV1_INTRA_ONLY_FRAME: Intra-only frame
+ * @V4L2_AV1_SWITCH_FRAME: Switch frame
+ */
+enum v4l2_av1_frame_type {
+	V4L2_AV1_KEY_FRAME = 0,
+	V4L2_AV1_INTER_FRAME = 1,
+	V4L2_AV1_INTRA_ONLY_FRAME = 2,
+	V4L2_AV1_SWITCH_FRAME = 3
+};
+
+/**
+ * enum v4l2_av1_interpolation_filter - AV1 interpolation filter types
+ *
+ * @V4L2_AV1_INTERPOLATION_FILTER_EIGHTTAP: eight tap filter
+ * @V4L2_AV1_INTERPOLATION_FILTER_EIGHTTAP_SMOOTH: eight tap smooth filter
+ * @V4L2_AV1_INTERPOLATION_FILTER_EIGHTTAP_SHARP: eight tap sharp filter
+ * @V4L2_AV1_INTERPOLATION_FILTER_BILINEAR: bilinear filter
+ * @V4L2_AV1_INTERPOLATION_FILTER_SWITCHABLE: filter selection is signaled at
+ * the block level
+ *
+ * See section 6.8.9 "Interpolation filter semantics" of the AV1 specification
+ * for more details.
+ */
+enum v4l2_av1_interpolation_filter {
+	V4L2_AV1_INTERPOLATION_FILTER_EIGHTTAP = 0,
+	V4L2_AV1_INTERPOLATION_FILTER_EIGHTTAP_SMOOTH = 1,
+	V4L2_AV1_INTERPOLATION_FILTER_EIGHTTAP_SHARP = 2,
+	V4L2_AV1_INTERPOLATION_FILTER_BILINEAR = 3,
+	V4L2_AV1_INTERPOLATION_FILTER_SWITCHABLE = 4,
+};
+
+/**
+ * enum v4l2_av1_tx_mode - AV1 Tx mode as described in section 6.8.21 "TX mode
+ * semantics" of the AV1 specification.
+ * @V4L2_AV1_TX_MODE_ONLY_4X4: the inverse transform will use only 4x4
+ * transforms
+ * @V4L2_AV1_TX_MODE_LARGEST: the inverse transform will use the largest
+ * transform size that fits inside the block
+ * @V4L2_AV1_TX_MODE_SELECT: the choice of transform size is specified
+ * explicitly for each block.
+ */
+enum v4l2_av1_tx_mode {
+	V4L2_AV1_TX_MODE_ONLY_4X4 = 0,
+	V4L2_AV1_TX_MODE_LARGEST = 1,
+	V4L2_AV1_TX_MODE_SELECT = 2
+};
+
+#define V4L2_AV1_FRAME_FLAG_SHOW_FRAME			 0x00000001
+#define V4L2_AV1_FRAME_FLAG_SHOWABLE_FRAME		 0x00000002
+#define V4L2_AV1_FRAME_FLAG_ERROR_RESILIENT_MODE	 0x00000004
+#define V4L2_AV1_FRAME_FLAG_DISABLE_CDF_UPDATE		 0x00000008
+#define V4L2_AV1_FRAME_FLAG_ALLOW_SCREEN_CONTENT_TOOLS	 0x00000010
+#define V4L2_AV1_FRAME_FLAG_FORCE_INTEGER_MV		 0x00000020
+#define V4L2_AV1_FRAME_FLAG_ALLOW_INTRABC		 0x00000040
+#define V4L2_AV1_FRAME_FLAG_USE_SUPERRES		 0x00000080
+#define V4L2_AV1_FRAME_FLAG_ALLOW_HIGH_PRECISION_MV	 0x00000100
+#define V4L2_AV1_FRAME_FLAG_IS_MOTION_MODE_SWITCHABLE	 0x00000200
+#define V4L2_AV1_FRAME_FLAG_USE_REF_FRAME_MVS		 0x00000400
+#define V4L2_AV1_FRAME_FLAG_DISABLE_FRAME_END_UPDATE_CDF 0x00000800
+#define V4L2_AV1_FRAME_FLAG_ALLOW_WARPED_MOTION		 0x00001000
+#define V4L2_AV1_FRAME_FLAG_REFERENCE_SELECT		 0x00002000
+#define V4L2_AV1_FRAME_FLAG_REDUCED_TX_SET		 0x00004000
+#define V4L2_AV1_FRAME_FLAG_SKIP_MODE_ALLOWED		 0x00008000
+#define V4L2_AV1_FRAME_FLAG_SKIP_MODE_PRESENT		 0x00010000
+#define V4L2_AV1_FRAME_FLAG_FRAME_SIZE_OVERRIDE		 0x00020000
+#define V4L2_AV1_FRAME_FLAG_BUFFER_REMOVAL_TIME_PRESENT	 0x00040000
+#define V4L2_AV1_FRAME_FLAG_FRAME_REFS_SHORT_SIGNALING	 0x00080000
+
+#define V4L2_CID_STATELESS_AV1_FRAME (V4L2_CID_CODEC_STATELESS_BASE + 502)
+/**
+ * struct v4l2_ctrl_av1_frame - Represents an AV1 Frame Header OBU.
+ *
+ * @tile_info: tile info
+ * @quantization: quantization params
+ * @segmentation: segmentation params
+ * @superres_denom: the denominator for the upscaling ratio.
+ * @loop_filter: loop filter params
+ * @cdef: cdef params
+ * @skip_mode_frame: specifies the frames to use for compound prediction when
+ * skip_mode is equal to 1.
+ * @primary_ref_frame: specifies which reference frame contains the CDF values
+ * and other state that should be loaded at the start of the frame.
+ * @loop_restoration: loop restoration params
+ * @global_motion: global motion params
+ * @flags: see V4L2_AV1_FRAME_FLAG_{}
+ * @frame_type: specifies the AV1 frame type
+ * @order_hint: specifies OrderHintBits least significant bits of the expected
+ * output order for this frame.
+ * @upscaled_width: the upscaled width.
+ * @interpolation_filter: specifies the filter selection used for performing
+ * inter prediction.
+ * @tx_mode: specifies how the transform size is determined.
+ * @frame_width_minus_1: add 1 to get the frame's width.
+ * @frame_height_minus_1: add 1 to get the frame's height
+ * @render_width_minus_1: add 1 to get the render width of the frame in luma
+ * samples.
+ * @render_height_minus_1: add 1 to get the render height of the frame in luma
+ * samples.
+ * @current_frame_id: specifies the frame id number for the current frame. Frame
+ * id numbers are additional information that do not affect the decoding
+ * process, but provide decoders with a way of detecting missing reference
+ * frames so that appropriate action can be taken.
+ * @buffer_removal_time: specifies the frame removal time in units of DecCT clock
+ * ticks counted from the removal time of the last random access point for
+ * operating point opNum.
+ * @reserved: padding field. Should be zeroed by applications.
+ * @order_hints: specifies the expected output order hint for each reference
+ * frame. This field corresponds to the OrderHints variable from the
+ * specification (section 5.9.2 "Uncompressed header syntax"). As such, this is
+ * only used for non-intra frames and ignored otherwise. order_hints[0] is
+ * always ignored.
+ * @reference_frame_ts: the V4L2 timestamp of the reference frame slots.
+ * @ref_frame_idx: used to index into @reference_frame_ts when decoding
+ * inter-frames. The meaning of this array is the same as in the specification.
+ * The timestamp refers to the timestamp field in struct v4l2_buffer. Use
+ * v4l2_timeval_to_ns() to convert the struct timeval to a __u64.
+ * @refresh_frame_flags: contains a bitmask that specifies which reference frame
+ * slots will be updated with the current frame after it is decoded.
+ */
+struct v4l2_ctrl_av1_frame {
+	struct v4l2_av1_tile_info tile_info;
+	struct v4l2_av1_quantization quantization;
+	__u8 superres_denom;
+	struct v4l2_av1_segmentation segmentation;
+	struct v4l2_av1_loop_filter loop_filter;
+	struct v4l2_av1_cdef cdef;
+	__u8 skip_mode_frame[2];
+	__u8 primary_ref_frame;
+	struct v4l2_av1_loop_restoration loop_restoration;
+	struct v4l2_av1_global_motion global_motion;
+	__u32 flags;
+	enum v4l2_av1_frame_type frame_type;
+	__u32 order_hint;
+	__u32 upscaled_width;
+	enum v4l2_av1_interpolation_filter interpolation_filter;
+	enum v4l2_av1_tx_mode tx_mode;
+	__u32 frame_width_minus_1;
+	__u32 frame_height_minus_1;
+	__u16 render_width_minus_1;
+	__u16 render_height_minus_1;
+
+	__u32 current_frame_id;
+	__u32 buffer_removal_time[V4L2_AV1_MAX_OPERATING_POINTS];
+	__u8 reserved[4];
+	__u32 order_hints[V4L2_AV1_TOTAL_REFS_PER_FRAME];
+	__u64 reference_frame_ts[V4L2_AV1_TOTAL_REFS_PER_FRAME];
+	__s8 ref_frame_idx[V4L2_AV1_REFS_PER_FRAME];
+	__u8 refresh_frame_flags;
+};
+
+#define V4L2_AV1_FILM_GRAIN_FLAG_APPLY_GRAIN 0x1
+#define V4L2_AV1_FILM_GRAIN_FLAG_UPDATE_GRAIN 0x2
+#define V4L2_AV1_FILM_GRAIN_FLAG_CHROMA_SCALING_FROM_LUMA 0x4
+#define V4L2_AV1_FILM_GRAIN_FLAG_OVERLAP 0x8
+#define V4L2_AV1_FILM_GRAIN_FLAG_CLIP_TO_RESTRICTED_RANGE 0x10
+
+#define V4L2_CID_STATELESS_AV1_FILM_GRAIN (V4L2_CID_CODEC_STATELESS_BASE + 505)
+/**
+ * struct v4l2_ctrl_av1_film_grain - AV1 Film Grain parameters.
+ *
+ * Film grain parameters as specified by section 6.8.20 of the AV1 Specification.
+ *
+ * @flags: see V4L2_AV1_FILM_GRAIN_{}.
+ * @cr_mult: represents a multiplier for the cr component used in derivation of
+ * the input index to the cr component scaling function.
+ * @grain_seed: specifies the starting value for the pseudo-random numbers used
+ * during film grain synthesis.
+ * @film_grain_params_ref_idx: indicates which reference frame contains the
+ * film grain parameters to be used for this frame.
+ * @num_y_points: specifies the number of points for the piece-wise linear
+ * scaling function of the luma component.
+ * @point_y_value: represents the x (luma value) coordinate for the i-th point
+ * of the piecewise linear scaling function for luma component. The values are
+ * signaled on the scale of 0..255. In case of 10 bit video, these values
+ * correspond to luma values divided by 4. In case of 12 bit video, these values
+ * correspond to luma values divided by 16.
+ * @point_y_scaling:  represents the scaling (output) value for the i-th point
+ * of the piecewise linear scaling function for luma component.
+ * @num_cb_points: specifies the number of points for the piece-wise linear
+ * scaling function of the cb component.
+ * @point_cb_value: represents the x coordinate for the i-th point of the
+ * piece-wise linear scaling function for cb component. The values are signaled
+ * on the scale of 0..255.
+ * @point_cb_scaling: represents the scaling (output) value for the i-th point
+ * of the piecewise linear scaling function for cb component.
+ * @num_cr_points: specifies represents the number of points for the piece-wise
+ * linear scaling function of the cr component.
+ * @point_cr_value:  represents the x coordinate for the i-th point of the
+ * piece-wise linear scaling function for cr component. The values are signaled
+ * on the scale of 0..255.
+ * @point_cr_scaling:  represents the scaling (output) value for the i-th point
+ * of the piecewise linear scaling function for cr component.
+ * @grain_scaling_minus_8: represents the shift – 8 applied to the values of the
+ * chroma component. The grain_scaling_minus_8 can take values of 0..3 and
+ * determines the range and quantization step of the standard deviation of film
+ * grain.
+ * @ar_coeff_lag: specifies the number of auto-regressive coefficients for luma
+ * and chroma.
+ * @ar_coeffs_y_plus_128: specifies auto-regressive coefficients used for the Y
+ * plane.
+ * @ar_coeffs_cb_plus_128: specifies auto-regressive coefficients used for the U
+ * plane.
+ * @ar_coeffs_cr_plus_128: specifies auto-regressive coefficients used for the V
+ * plane.
+ * @ar_coeff_shift_minus_6: specifies the range of the auto-regressive
+ * coefficients. Values of 0, 1, 2, and 3 correspond to the ranges for
+ * auto-regressive coefficients of [-2, 2), [-1, 1), [-0.5, 0.5) and [-0.25,
+ * 0.25) respectively.
+ * @grain_scale_shift: specifies how much the Gaussian random numbers should be
+ * scaled down during the grain synthesis process.
+ * @cb_mult: represents a multiplier for the cb component used in derivation of
+ * the input index to the cb component scaling function.
+ * @cb_luma_mult: represents a multiplier for the average luma component used in
+ * derivation of the input index to the cb component scaling function.
+ * @cr_luma_mult: represents a multiplier for the average luma component used in
+ * derivation of the input index to the cr component scaling function.
+ * @cb_offset: represents an offset used in derivation of the input index to the
+ * cb component scaling function.
+ * @cr_offset: represents an offset used in derivation of the input index to the
+ * cr component scaling function.
+ * @reserved: padding field. Should be zeroed by applications.
+ */
+struct v4l2_ctrl_av1_film_grain {
+	__u8 flags;
+	__u8 cr_mult;
+	__u16 grain_seed;
+	__u8 film_grain_params_ref_idx;
+	__u8 num_y_points;
+	__u8 point_y_value[V4L2_AV1_MAX_NUM_Y_POINTS];
+	__u8 point_y_scaling[V4L2_AV1_MAX_NUM_Y_POINTS];
+	__u8 num_cb_points;
+	__u8 point_cb_value[V4L2_AV1_MAX_NUM_CB_POINTS];
+	__u8 point_cb_scaling[V4L2_AV1_MAX_NUM_CB_POINTS];
+	__u8 num_cr_points;
+	__u8 point_cr_value[V4L2_AV1_MAX_NUM_CR_POINTS];
+	__u8 point_cr_scaling[V4L2_AV1_MAX_NUM_CR_POINTS];
+	__u8 grain_scaling_minus_8;
+	__u8 ar_coeff_lag;
+	__u8 ar_coeffs_y_plus_128[V4L2_AV1_AR_COEFFS_SIZE];
+	__u8 ar_coeffs_cb_plus_128[V4L2_AV1_AR_COEFFS_SIZE];
+	__u8 ar_coeffs_cr_plus_128[V4L2_AV1_AR_COEFFS_SIZE];
+	__u8 ar_coeff_shift_minus_6;
+	__u8 grain_scale_shift;
+	__u8 cb_mult;
+	__u8 cb_luma_mult;
+	__u8 cr_luma_mult;
+	__u16 cb_offset;
+	__u16 cr_offset;
+	__u8 reserved[4];
+};
+
 /* MPEG-compression definitions kept for backwards compatibility */
 #define V4L2_CTRL_CLASS_MPEG            V4L2_CTRL_CLASS_CODEC
 #define V4L2_CID_MPEG_CLASS             V4L2_CID_CODEC_CLASS
diff --git a/include/linux/v4l2-mediabus.h b/include/linux/v4l2-mediabus.h
index 846dadf..097ef73 100644
--- a/include/linux/v4l2-mediabus.h
+++ b/include/linux/v4l2-mediabus.h
@@ -3,10 +3,6 @@
  * Media Bus API header
  *
  * Copyright (C) 2009, Guennadi Liakhovetski <g.liakhovetski@gmx.de>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2 as
- * published by the Free Software Foundation.
  */
 
 #ifndef __LINUX_V4L2_MEDIABUS_H
@@ -23,12 +19,18 @@
  * @width:	image width
  * @height:	image height
  * @code:	data format code (from enum v4l2_mbus_pixelcode)
- * @field:	used interlacing type (from enum v4l2_field)
- * @colorspace:	colorspace of the data (from enum v4l2_colorspace)
- * @ycbcr_enc:	YCbCr encoding of the data (from enum v4l2_ycbcr_encoding)
- * @hsv_enc:	HSV encoding of the data (from enum v4l2_hsv_encoding)
- * @quantization: quantization of the data (from enum v4l2_quantization)
- * @xfer_func:  transfer function of the data (from enum v4l2_xfer_func)
+ * @field:	used interlacing type (from enum v4l2_field), zero for metadata
+ *		mbus codes
+ * @colorspace:	colorspace of the data (from enum v4l2_colorspace), zero on
+ *		metadata mbus codes
+ * @ycbcr_enc:	YCbCr encoding of the data (from enum v4l2_ycbcr_encoding), zero
+ *		for metadata mbus codes
+ * @hsv_enc:	HSV encoding of the data (from enum v4l2_hsv_encoding), zero for
+ *		metadata mbus codes
+ * @quantization: quantization of the data (from enum v4l2_quantization), zero
+ *		for metadata mbus codes
+ * @xfer_func:  transfer function of the data (from enum v4l2_xfer_func), zero
+ *		for metadata mbus codes
  * @flags:	flags (V4L2_MBUS_FRAMEFMT_*)
  * @reserved:  reserved bytes that can be later used
  */
diff --git a/include/linux/v4l2-subdev.h b/include/linux/v4l2-subdev.h
index 480891d..2347e26 100644
--- a/include/linux/v4l2-subdev.h
+++ b/include/linux/v4l2-subdev.h
@@ -6,24 +6,12 @@
  *
  * Contacts: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
  *	     Sakari Ailus <sakari.ailus@iki.fi>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 2 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
  */
 
 #ifndef __LINUX_V4L2_SUBDEV_H
 #define __LINUX_V4L2_SUBDEV_H
 
+#include <linux/const.h>
 #include <linux/ioctl.h>
 #include <linux/types.h>
 #include <linux/v4l2-common.h>
@@ -62,6 +50,10 @@
  * @rect: pad crop rectangle boundaries
  * @stream: stream number, defined in subdev routing
  * @reserved: drivers and applications must zero this array
+ *
+ * The subdev crop API is an obsolete interface and may be removed in the
+ * future. It is superseded by the selection API. No new extensions to this
+ * structure will be accepted.
  */
 struct v4l2_subdev_crop {
 	__u32 which;
@@ -128,13 +120,15 @@
  * @pad: pad number, as reported by the media API
  * @interval: frame interval in seconds
  * @stream: stream number, defined in subdev routing
+ * @which: interval type (from enum v4l2_subdev_format_whence)
  * @reserved: drivers and applications must zero this array
  */
 struct v4l2_subdev_frame_interval {
 	__u32 pad;
 	struct v4l2_fract interval;
 	__u32 stream;
-	__u32 reserved[8];
+	__u32 which;
+	__u32 reserved[7];
 };
 
 /**
@@ -145,7 +139,7 @@
  * @width: frame width in pixels
  * @height: frame height in pixels
  * @interval: frame interval in seconds
- * @which: format type (from enum v4l2_subdev_format_whence)
+ * @which: interval type (from enum v4l2_subdev_format_whence)
  * @stream: stream number, defined in subdev routing
  * @reserved: drivers and applications must zero this array
  */
@@ -202,29 +196,14 @@
 /* The v4l2 sub-device video device node is registered in read-only mode. */
 #define V4L2_SUBDEV_CAP_RO_SUBDEV		0x00000001
 
-/* The v4l2 sub-device supports multiplexed streams. */
-#define V4L2_SUBDEV_CAP_MPLEXED			0x00000002
+/* The v4l2 sub-device supports routing and multiplexed streams. */
+#define V4L2_SUBDEV_CAP_STREAMS			0x00000002
 
 /*
  * Is the route active? An active route will start when streaming is enabled
  * on a video node.
  */
-#define V4L2_SUBDEV_ROUTE_FL_ACTIVE		_BITUL(0)
-
-/*
- * Is the route immutable, i.e. can it be activated and inactivated?
- * Set by the driver.
- */
-#define V4L2_SUBDEV_ROUTE_FL_IMMUTABLE		_BITUL(1)
-
-/*
- * Is the route a source endpoint? A source endpoint route refers to a stream
- * generated internally by the subdevice (usually a sensor), and thus there
- * is no sink-side endpoint for the route. The sink_pad and sink_stream
- * fields are unused.
- * Set by the driver.
- */
-#define V4L2_SUBDEV_ROUTE_FL_SOURCE		_BITUL(2)
+#define V4L2_SUBDEV_ROUTE_FL_ACTIVE		(1U << 0)
 
 /**
  * struct v4l2_subdev_route - A route inside a subdev
@@ -249,15 +228,44 @@
  * struct v4l2_subdev_routing - Subdev routing information
  *
  * @which: configuration type (from enum v4l2_subdev_format_whence)
- * @num_routes: the total number of routes in the routes array
+ * @len_routes: the length of the routes array, in routes; set by the user, not
+ *		modified by the kernel
  * @routes: pointer to the routes array
+ * @num_routes: the total number of routes, possibly more than fits in the
+ *		routes array
  * @reserved: drivers and applications must zero this array
  */
 struct v4l2_subdev_routing {
 	__u32 which;
-	__u32 num_routes;
+	__u32 len_routes;
 	__u64 routes;
-	__u32 reserved[6];
+	__u32 num_routes;
+	__u32 reserved[11];
+};
+
+/*
+ * The client is aware of streams. Setting this flag enables the use of 'stream'
+ * fields (referring to the stream number) with various ioctls. If this is not
+ * set (which is the default), the 'stream' fields will be forced to 0 by the
+ * kernel.
+ */
+#define V4L2_SUBDEV_CLIENT_CAP_STREAMS			(1ULL << 0)
+
+/*
+ * The client is aware of the struct v4l2_subdev_frame_interval which field. If
+ * this is not set (which is the default), the which field is forced to
+ * V4L2_SUBDEV_FORMAT_ACTIVE by the kernel.
+ */
+#define V4L2_SUBDEV_CLIENT_CAP_INTERVAL_USES_WHICH	(1ULL << 1)
+
+/**
+ * struct v4l2_subdev_client_capability - Capabilities of the client accessing
+ *					  the subdev
+ *
+ * @capabilities: A bitmask of V4L2_SUBDEV_CLIENT_CAP_* flags.
+ */
+struct v4l2_subdev_client_capability {
+	__u64 capabilities;
 };
 
 /* Backwards compatibility define --- to be removed */
@@ -277,6 +285,9 @@
 #define VIDIOC_SUBDEV_S_SELECTION		_IOWR('V', 62, struct v4l2_subdev_selection)
 #define VIDIOC_SUBDEV_G_ROUTING			_IOWR('V', 38, struct v4l2_subdev_routing)
 #define VIDIOC_SUBDEV_S_ROUTING			_IOWR('V', 39, struct v4l2_subdev_routing)
+#define VIDIOC_SUBDEV_G_CLIENT_CAP		_IOR('V',  101, struct v4l2_subdev_client_capability)
+#define VIDIOC_SUBDEV_S_CLIENT_CAP		_IOWR('V',  102, struct v4l2_subdev_client_capability)
+
 /* The following ioctls are identical to the ioctls in videodev2.h */
 #define VIDIOC_SUBDEV_G_STD			_IOR('V', 23, v4l2_std_id)
 #define VIDIOC_SUBDEV_S_STD			_IOW('V', 24, v4l2_std_id)
diff --git a/include/linux/videodev2.h b/include/linux/videodev2.h
index bfb315d..7fe522e 100644
--- a/include/linux/videodev2.h
+++ b/include/linux/videodev2.h
@@ -243,6 +243,7 @@
 
 	/* DCI-P3 colorspace, used by cinema projectors */
 	V4L2_COLORSPACE_DCI_P3        = 12,
+
 };
 
 /*
@@ -474,7 +475,6 @@
 #define V4L2_CAP_META_CAPTURE		0x00800000  /* Is a metadata capture device */
 
 #define V4L2_CAP_READWRITE              0x01000000  /* read/write systemcalls */
-#define V4L2_CAP_ASYNCIO                0x02000000  /* async I/O */
 #define V4L2_CAP_STREAMING              0x04000000  /* streaming I/O ioctls */
 #define V4L2_CAP_META_OUTPUT		0x08000000  /* Is a metadata output device */
 
@@ -549,6 +549,15 @@
 #define V4L2_PIX_FMT_RGBX32  v4l2_fourcc('X', 'B', '2', '4') /* 32  RGBX-8-8-8-8  */
 #define V4L2_PIX_FMT_ARGB32  v4l2_fourcc('B', 'A', '2', '4') /* 32  ARGB-8-8-8-8  */
 #define V4L2_PIX_FMT_XRGB32  v4l2_fourcc('B', 'X', '2', '4') /* 32  XRGB-8-8-8-8  */
+#define V4L2_PIX_FMT_RGBX1010102 v4l2_fourcc('R', 'X', '3', '0') /* 32  RGBX-10-10-10-2 */
+#define V4L2_PIX_FMT_RGBA1010102 v4l2_fourcc('R', 'A', '3', '0') /* 32  RGBA-10-10-10-2 */
+#define V4L2_PIX_FMT_ARGB2101010 v4l2_fourcc('A', 'R', '3', '0') /* 32  ARGB-2-10-10-10 */
+
+/* RGB formats (6 or 8 bytes per pixel) */
+#define V4L2_PIX_FMT_BGR48_12    v4l2_fourcc('B', '3', '1', '2') /* 48  BGR 12-bit per component */
+#define V4L2_PIX_FMT_BGR48       v4l2_fourcc('B', 'G', 'R', '6') /* 48  BGR 16-bit per component */
+#define V4L2_PIX_FMT_RGB48       v4l2_fourcc('R', 'G', 'B', '6') /* 48  RGB 16-bit per component */
+#define V4L2_PIX_FMT_ABGR64_12   v4l2_fourcc('B', '4', '1', '2') /* 64  BGRA 12-bit per component */
 
 /* Grey formats */
 #define V4L2_PIX_FMT_GREY    v4l2_fourcc('G', 'R', 'E', 'Y') /*  8  Greyscale     */
@@ -556,6 +565,7 @@
 #define V4L2_PIX_FMT_Y6      v4l2_fourcc('Y', '0', '6', ' ') /*  6  Greyscale     */
 #define V4L2_PIX_FMT_Y10     v4l2_fourcc('Y', '1', '0', ' ') /* 10  Greyscale     */
 #define V4L2_PIX_FMT_Y12     v4l2_fourcc('Y', '1', '2', ' ') /* 12  Greyscale     */
+#define V4L2_PIX_FMT_Y012    v4l2_fourcc('Y', '0', '1', '2') /* 12  Greyscale     */
 #define V4L2_PIX_FMT_Y14     v4l2_fourcc('Y', '1', '4', ' ') /* 14  Greyscale     */
 #define V4L2_PIX_FMT_Y16     v4l2_fourcc('Y', '1', '6', ' ') /* 16  Greyscale     */
 #define V4L2_PIX_FMT_Y16_BE  v4l2_fourcc_be('Y', '1', '6', ' ') /* 16  Greyscale BE  */
@@ -564,6 +574,8 @@
 #define V4L2_PIX_FMT_Y10BPACK    v4l2_fourcc('Y', '1', '0', 'B') /* 10  Greyscale bit-packed */
 #define V4L2_PIX_FMT_Y10P    v4l2_fourcc('Y', '1', '0', 'P') /* 10  Greyscale, MIPI RAW10 packed */
 #define V4L2_PIX_FMT_IPU3_Y10		v4l2_fourcc('i', 'p', '3', 'y') /* IPU3 packed 10-bit greyscale */
+#define V4L2_PIX_FMT_Y12P    v4l2_fourcc('Y', '1', '2', 'P') /* 12  Greyscale, MIPI RAW12 packed */
+#define V4L2_PIX_FMT_Y14P    v4l2_fourcc('Y', '1', '4', 'P') /* 14  Greyscale, MIPI RAW14 packed */
 
 /* Palette formats */
 #define V4L2_PIX_FMT_PAL8    v4l2_fourcc('P', 'A', 'L', '8') /*  8  8-bit palette */
@@ -590,6 +602,15 @@
 #define V4L2_PIX_FMT_YUVA32  v4l2_fourcc('Y', 'U', 'V', 'A') /* 32  YUVA-8-8-8-8  */
 #define V4L2_PIX_FMT_YUVX32  v4l2_fourcc('Y', 'U', 'V', 'X') /* 32  YUVX-8-8-8-8  */
 #define V4L2_PIX_FMT_M420    v4l2_fourcc('M', '4', '2', '0') /* 12  YUV 4:2:0 2 lines y, 1 line uv interleaved */
+#define V4L2_PIX_FMT_YUV48_12    v4l2_fourcc('Y', '3', '1', '2') /* 48  YUV 4:4:4 12-bit per component */
+
+/*
+ * YCbCr packed format. For each Y2xx format, xx bits of valid data occupy the MSBs
+ * of the 16 bit components, and 16-xx bits of zero padding occupy the LSBs.
+ */
+#define V4L2_PIX_FMT_Y210    v4l2_fourcc('Y', '2', '1', '0') /* 32  YUYV 4:2:2 */
+#define V4L2_PIX_FMT_Y212    v4l2_fourcc('Y', '2', '1', '2') /* 32  YUYV 4:2:2 */
+#define V4L2_PIX_FMT_Y216    v4l2_fourcc('Y', '2', '1', '6') /* 32  YUYV 4:2:2 */
 
 /* two planes -- one Y, one Cr + Cb interleaved  */
 #define V4L2_PIX_FMT_NV12    v4l2_fourcc('N', 'V', '1', '2') /* 12  Y/CbCr 4:2:0  */
@@ -598,12 +619,15 @@
 #define V4L2_PIX_FMT_NV61    v4l2_fourcc('N', 'V', '6', '1') /* 16  Y/CrCb 4:2:2  */
 #define V4L2_PIX_FMT_NV24    v4l2_fourcc('N', 'V', '2', '4') /* 24  Y/CbCr 4:4:4  */
 #define V4L2_PIX_FMT_NV42    v4l2_fourcc('N', 'V', '4', '2') /* 24  Y/CrCb 4:4:4  */
+#define V4L2_PIX_FMT_P010    v4l2_fourcc('P', '0', '1', '0') /* 24  Y/CbCr 4:2:0 10-bit per component */
+#define V4L2_PIX_FMT_P012    v4l2_fourcc('P', '0', '1', '2') /* 24  Y/CbCr 4:2:0 12-bit per component */
 
 /* two non contiguous planes - one Y, one Cr + Cb interleaved  */
 #define V4L2_PIX_FMT_NV12M   v4l2_fourcc('N', 'M', '1', '2') /* 12  Y/CbCr 4:2:0  */
 #define V4L2_PIX_FMT_NV21M   v4l2_fourcc('N', 'M', '2', '1') /* 21  Y/CrCb 4:2:0  */
 #define V4L2_PIX_FMT_NV16M   v4l2_fourcc('N', 'M', '1', '6') /* 16  Y/CbCr 4:2:2  */
 #define V4L2_PIX_FMT_NV61M   v4l2_fourcc('N', 'M', '6', '1') /* 16  Y/CrCb 4:2:2  */
+#define V4L2_PIX_FMT_P012M   v4l2_fourcc('P', 'M', '1', '2') /* 24  Y/CbCr 4:2:0 12-bit per component */
 
 /* three planes - Y Cb, Cr */
 #define V4L2_PIX_FMT_YUV410  v4l2_fourcc('Y', 'U', 'V', '9') /*  9  YUV 4:1:0     */
@@ -625,6 +649,10 @@
 #define V4L2_PIX_FMT_NV12_4L4 v4l2_fourcc('V', 'T', '1', '2')   /* 12  Y/CbCr 4:2:0  4x4 tiles */
 #define V4L2_PIX_FMT_NV12_16L16 v4l2_fourcc('H', 'M', '1', '2') /* 12  Y/CbCr 4:2:0 16x16 tiles */
 #define V4L2_PIX_FMT_NV12_32L32 v4l2_fourcc('S', 'T', '1', '2') /* 12  Y/CbCr 4:2:0 32x32 tiles */
+#define V4L2_PIX_FMT_NV15_4L4 v4l2_fourcc('V', 'T', '1', '5') /* 15 Y/CbCr 4:2:0 10-bit 4x4 tiles */
+#define V4L2_PIX_FMT_P010_4L4 v4l2_fourcc('T', '0', '1', '0') /* 12  Y/CbCr 4:2:0 10-bit 4x4 macroblocks */
+#define V4L2_PIX_FMT_NV12_8L128       v4l2_fourcc('A', 'T', '1', '2') /* Y/CbCr 4:2:0 8x128 tiles */
+#define V4L2_PIX_FMT_NV12_10BE_8L128  v4l2_fourcc_be('A', 'X', '1', '2') /* Y/CbCr 4:2:0 10-bit 8x128 tiles */
 
 /* Tiled YUV formats, non contiguous planes */
 #define V4L2_PIX_FMT_NV12MT  v4l2_fourcc('T', 'M', '1', '2') /* 12  Y/CbCr 4:2:0 64x32 tiles */
@@ -707,6 +735,11 @@
 #define V4L2_PIX_FMT_FWHT     v4l2_fourcc('F', 'W', 'H', 'T') /* Fast Walsh Hadamard Transform (vicodec) */
 #define V4L2_PIX_FMT_FWHT_STATELESS     v4l2_fourcc('S', 'F', 'W', 'H') /* Stateless FWHT (vicodec) */
 #define V4L2_PIX_FMT_H264_SLICE v4l2_fourcc('S', '2', '6', '4') /* H264 parsed slices */
+#define V4L2_PIX_FMT_HEVC_SLICE v4l2_fourcc('S', '2', '6', '5') /* HEVC parsed slices */
+#define V4L2_PIX_FMT_AV1_FRAME v4l2_fourcc('A', 'V', '1', 'F') /* AV1 parsed frame */
+#define V4L2_PIX_FMT_SPK      v4l2_fourcc('S', 'P', 'K', '0') /* Sorenson Spark */
+#define V4L2_PIX_FMT_RV30     v4l2_fourcc('R', 'V', '3', '0') /* RealVideo 8 */
+#define V4L2_PIX_FMT_RV40     v4l2_fourcc('R', 'V', '4', '0') /* RealVideo 9 & 10 */
 
 /*  Vendor-specific formats   */
 #define V4L2_PIX_FMT_CPIA1    v4l2_fourcc('C', 'P', 'I', 'A') /* cpia1 YUV */
@@ -740,11 +773,15 @@
 #define V4L2_PIX_FMT_Z16      v4l2_fourcc('Z', '1', '6', ' ') /* Depth data 16-bit */
 #define V4L2_PIX_FMT_MT21C    v4l2_fourcc('M', 'T', '2', '1') /* Mediatek compressed block mode  */
 #define V4L2_PIX_FMT_MM21     v4l2_fourcc('M', 'M', '2', '1') /* Mediatek 8-bit block mode, two non-contiguous planes */
+#define V4L2_PIX_FMT_MT2110T  v4l2_fourcc('M', 'T', '2', 'T') /* Mediatek 10-bit block tile mode */
+#define V4L2_PIX_FMT_MT2110R  v4l2_fourcc('M', 'T', '2', 'R') /* Mediatek 10-bit block raster mode */
 #define V4L2_PIX_FMT_INZI     v4l2_fourcc('I', 'N', 'Z', 'I') /* Intel Planar Greyscale 10-bit and Depth 16-bit */
 #define V4L2_PIX_FMT_CNF4     v4l2_fourcc('C', 'N', 'F', '4') /* Intel 4-bit packed depth confidence information */
 #define V4L2_PIX_FMT_HI240    v4l2_fourcc('H', 'I', '2', '4') /* BTTV 8-bit dithered RGB */
 #define V4L2_PIX_FMT_QC08C    v4l2_fourcc('Q', '0', '8', 'C') /* Qualcomm 8-bit compressed */
 #define V4L2_PIX_FMT_QC10C    v4l2_fourcc('Q', '1', '0', 'C') /* Qualcomm 10-bit compressed */
+#define V4L2_PIX_FMT_AJPG     v4l2_fourcc('A', 'J', 'P', 'G') /* Aspeed JPEG */
+#define V4L2_PIX_FMT_HEXTILE  v4l2_fourcc('H', 'X', 'T', 'L') /* Hextile compressed */
 
 /* 10bit raw packed, 32 bytes for every 25 pixels, last LSB 6 bits unused */
 #define V4L2_PIX_FMT_IPU3_SBGGR10	v4l2_fourcc('i', 'p', '3', 'b') /* IPU3 packed 10-bit BGGR bayer */
@@ -752,6 +789,18 @@
 #define V4L2_PIX_FMT_IPU3_SGRBG10	v4l2_fourcc('i', 'p', '3', 'G') /* IPU3 packed 10-bit GRBG bayer */
 #define V4L2_PIX_FMT_IPU3_SRGGB10	v4l2_fourcc('i', 'p', '3', 'r') /* IPU3 packed 10-bit RGGB bayer */
 
+/* Raspberry Pi PiSP compressed formats. */
+#define V4L2_PIX_FMT_PISP_COMP1_RGGB	v4l2_fourcc('P', 'C', '1', 'R') /* PiSP 8-bit mode 1 compressed RGGB bayer */
+#define V4L2_PIX_FMT_PISP_COMP1_GRBG	v4l2_fourcc('P', 'C', '1', 'G') /* PiSP 8-bit mode 1 compressed GRBG bayer */
+#define V4L2_PIX_FMT_PISP_COMP1_GBRG	v4l2_fourcc('P', 'C', '1', 'g') /* PiSP 8-bit mode 1 compressed GBRG bayer */
+#define V4L2_PIX_FMT_PISP_COMP1_BGGR	v4l2_fourcc('P', 'C', '1', 'B') /* PiSP 8-bit mode 1 compressed BGGR bayer */
+#define V4L2_PIX_FMT_PISP_COMP1_MONO	v4l2_fourcc('P', 'C', '1', 'M') /* PiSP 8-bit mode 1 compressed monochrome */
+#define V4L2_PIX_FMT_PISP_COMP2_RGGB	v4l2_fourcc('P', 'C', '2', 'R') /* PiSP 8-bit mode 2 compressed RGGB bayer */
+#define V4L2_PIX_FMT_PISP_COMP2_GRBG	v4l2_fourcc('P', 'C', '2', 'G') /* PiSP 8-bit mode 2 compressed GRBG bayer */
+#define V4L2_PIX_FMT_PISP_COMP2_GBRG	v4l2_fourcc('P', 'C', '2', 'g') /* PiSP 8-bit mode 2 compressed GBRG bayer */
+#define V4L2_PIX_FMT_PISP_COMP2_BGGR	v4l2_fourcc('P', 'C', '2', 'B') /* PiSP 8-bit mode 2 compressed BGGR bayer */
+#define V4L2_PIX_FMT_PISP_COMP2_MONO	v4l2_fourcc('P', 'C', '2', 'M') /* PiSP 8-bit mode 2 compressed monochrome */
+
 /* SDR formats - used only for Software Defined Radio devices */
 #define V4L2_SDR_FMT_CU8          v4l2_fourcc('C', 'U', '0', '8') /* IQ u8 */
 #define V4L2_SDR_FMT_CU16LE       v4l2_fourcc('C', 'U', '1', '6') /* IQ u16le */
@@ -781,6 +830,15 @@
 #define V4L2_META_FMT_RK_ISP1_PARAMS	v4l2_fourcc('R', 'K', '1', 'P') /* Rockchip ISP1 3A Parameters */
 #define V4L2_META_FMT_RK_ISP1_STAT_3A	v4l2_fourcc('R', 'K', '1', 'S') /* Rockchip ISP1 3A Statistics */
 
+/* The metadata format identifier for BE configuration buffers. */
+#define V4L2_META_FMT_RPI_BE_CFG v4l2_fourcc('R', 'P', 'B', 'C')
+
+/* The metadata format identifier for FE configuration buffers. */
+#define V4L2_META_FMT_RPI_FE_CFG v4l2_fourcc('R', 'P', 'F', 'C')
+
+/* The metadata format identifier for FE stats buffers. */
+#define V4L2_META_FMT_RPI_FE_STATS v4l2_fourcc('R', 'P', 'F', 'S')
+
 /* priv field value to indicates that subsequent fields are valid. */
 #define V4L2_PIX_FMT_PRIV_MAGIC		0xfeedcafe
 
@@ -811,6 +869,7 @@
 #define V4L2_FMT_FLAG_CSC_YCBCR_ENC		0x0080
 #define V4L2_FMT_FLAG_CSC_HSV_ENC		V4L2_FMT_FLAG_CSC_YCBCR_ENC
 #define V4L2_FMT_FLAG_CSC_QUANTIZATION		0x0100
+#define V4L2_FMT_FLAG_META_LINE_BASED		0x0200
 
 	/* Frame Size and frame rate enumeration */
 /*
@@ -960,18 +1019,20 @@
 #define V4L2_BUF_CAP_SUPPORTS_ORPHANED_BUFS		(1 << 4)
 #define V4L2_BUF_CAP_SUPPORTS_M2M_HOLD_CAPTURE_BUF	(1 << 5)
 #define V4L2_BUF_CAP_SUPPORTS_MMAP_CACHE_HINTS		(1 << 6)
+#define V4L2_BUF_CAP_SUPPORTS_MAX_NUM_BUFFERS		(1 << 7)
+#define V4L2_BUF_CAP_SUPPORTS_REMOVE_BUFS		(1 << 8)
 
 /**
  * struct v4l2_plane - plane info for multi-planar buffers
  * @bytesused:		number of bytes occupied by data in the plane (payload)
  * @length:		size of this plane (NOT the payload) in bytes
- * @mem_offset:		when memory in the associated struct v4l2_buffer is
+ * @m.mem_offset:	when memory in the associated struct v4l2_buffer is
  *			V4L2_MEMORY_MMAP, equals the offset from the start of
  *			the device memory for this plane (or is a "cookie" that
  *			should be passed to mmap() called on the video node)
- * @userptr:		when memory is V4L2_MEMORY_USERPTR, a userspace pointer
+ * @m.userptr:		when memory is V4L2_MEMORY_USERPTR, a userspace pointer
  *			pointing to this plane
- * @fd:			when memory is V4L2_MEMORY_DMABUF, a userspace file
+ * @m.fd:		when memory is V4L2_MEMORY_DMABUF, a userspace file
  *			descriptor associated with this plane
  * @m:			union of @mem_offset, @userptr and @fd
  * @data_offset:	offset in the plane to the start of data; usually 0,
@@ -1009,14 +1070,14 @@
  * @sequence:	sequence count of this frame
  * @memory:	enum v4l2_memory; the method, in which the actual video data is
  *		passed
- * @offset:	for non-multiplanar buffers with memory == V4L2_MEMORY_MMAP;
+ * @m.offset:	for non-multiplanar buffers with memory == V4L2_MEMORY_MMAP;
  *		offset from the start of the device memory for this plane,
  *		(or a "cookie" that should be passed to mmap() as offset)
- * @userptr:	for non-multiplanar buffers with memory == V4L2_MEMORY_USERPTR;
+ * @m.userptr:	for non-multiplanar buffers with memory == V4L2_MEMORY_USERPTR;
  *		a userspace pointer pointing to this buffer
- * @fd:		for non-multiplanar buffers with memory == V4L2_MEMORY_DMABUF;
+ * @m.fd:		for non-multiplanar buffers with memory == V4L2_MEMORY_DMABUF;
  *		a userspace file descriptor associated with this buffer
- * @planes:	for multiplanar buffers; userspace pointer to the array of plane
+ * @m.planes:	for multiplanar buffers; userspace pointer to the array of plane
  *		info structs for this buffer
  * @m:		union of @offset, @userptr, @planes and @fd
  * @length:	size in bytes of the buffer (NOT its payload) for single-plane
@@ -1550,7 +1611,8 @@
 	((bt)->width + V4L2_DV_BT_BLANKING_WIDTH(bt))
 #define V4L2_DV_BT_BLANKING_HEIGHT(bt) \
 	((bt)->vfrontporch + (bt)->vsync + (bt)->vbackporch + \
-	 (bt)->il_vfrontporch + (bt)->il_vsync + (bt)->il_vbackporch)
+	 ((bt)->interlaced ? \
+	  ((bt)->il_vfrontporch + (bt)->il_vsync + (bt)->il_vbackporch) : 0))
 #define V4L2_DV_BT_FRAME_HEIGHT(bt) \
 	((bt)->height + V4L2_DV_BT_BLANKING_HEIGHT(bt))
 
@@ -1641,7 +1703,7 @@
 	__u8	     name[32];		/*  Label */
 	__u32	     type;		/*  Type of input */
 	__u32	     audioset;		/*  Associated audios (bitfield) */
-	__u32        tuner;             /*  enum v4l2_tuner_type */
+	__u32        tuner;             /*  Tuner index */
 	v4l2_std_id  std;
 	__u32	     status;
 	__u32	     capabilities;
@@ -1728,6 +1790,8 @@
 		__u8 *p_u8;
 		__u16 *p_u16;
 		__u32 *p_u32;
+		__s32 *p_s32;
+		__s64 *p_s64;
 		struct v4l2_area *p_area;
 		struct v4l2_ctrl_h264_sps *p_h264_sps;
 		struct v4l2_ctrl_h264_pps *p_h264_pps;
@@ -1742,8 +1806,19 @@
 		struct v4l2_ctrl_mpeg2_quantisation *p_mpeg2_quantisation;
 		struct v4l2_ctrl_vp9_compressed_hdr *p_vp9_compressed_hdr_probs;
 		struct v4l2_ctrl_vp9_frame *p_vp9_frame;
+		struct v4l2_ctrl_hevc_sps *p_hevc_sps;
+		struct v4l2_ctrl_hevc_pps *p_hevc_pps;
+		struct v4l2_ctrl_hevc_slice_params *p_hevc_slice_params;
+		struct v4l2_ctrl_hevc_scaling_matrix *p_hevc_scaling_matrix;
+		struct v4l2_ctrl_hevc_decode_params *p_hevc_decode_params;
+		struct v4l2_ctrl_av1_sequence *p_av1_sequence;
+		struct v4l2_ctrl_av1_tile_group_entry *p_av1_tile_group_entry;
+		struct v4l2_ctrl_av1_frame *p_av1_frame;
+		struct v4l2_ctrl_av1_film_grain *p_av1_film_grain;
+		struct v4l2_ctrl_hdr10_cll_info *p_hdr10_cll_info;
+		struct v4l2_ctrl_hdr10_mastering_display *p_hdr10_mastering_display;
 		void *ptr;
-	};
+	} __attribute__ ((packed));
 } __attribute__ ((packed));
 
 struct v4l2_ext_controls {
@@ -1805,6 +1880,17 @@
 
 	V4L2_CTRL_TYPE_VP9_COMPRESSED_HDR	= 0x0260,
 	V4L2_CTRL_TYPE_VP9_FRAME		= 0x0261,
+
+	V4L2_CTRL_TYPE_HEVC_SPS			= 0x0270,
+	V4L2_CTRL_TYPE_HEVC_PPS			= 0x0271,
+	V4L2_CTRL_TYPE_HEVC_SLICE_PARAMS	= 0x0272,
+	V4L2_CTRL_TYPE_HEVC_SCALING_MATRIX	= 0x0273,
+	V4L2_CTRL_TYPE_HEVC_DECODE_PARAMS	= 0x0274,
+
+	V4L2_CTRL_TYPE_AV1_SEQUENCE	    = 0x280,
+	V4L2_CTRL_TYPE_AV1_TILE_GROUP_ENTRY = 0x281,
+	V4L2_CTRL_TYPE_AV1_FRAME	    = 0x282,
+	V4L2_CTRL_TYPE_AV1_FILM_GRAIN	    = 0x283,
 };
 
 /*  Used in the VIDIOC_QUERYCTRL ioctl for querying controls */
@@ -1860,6 +1946,7 @@
 #define V4L2_CTRL_FLAG_HAS_PAYLOAD	0x0100
 #define V4L2_CTRL_FLAG_EXECUTE_ON_WRITE	0x0200
 #define V4L2_CTRL_FLAG_MODIFY_LAYOUT	0x0400
+#define V4L2_CTRL_FLAG_DYNAMIC_ARRAY	0x0800
 
 /*  Query flags, to be ORed with the control ID */
 #define V4L2_CTRL_FLAG_NEXT_CTRL	0x80000000
@@ -2301,23 +2388,32 @@
  * struct v4l2_meta_format - metadata format definition
  * @dataformat:		little endian four character code (fourcc)
  * @buffersize:		maximum size in bytes required for data
+ * @width:		number of data units of data per line (valid for line
+ *			based formats only, see format documentation)
+ * @height:		number of lines of data per buffer (valid for line based
+ *			formats only)
+ * @bytesperline:	offset between the beginnings of two adjacent lines in
+ *			bytes (valid for line based formats only)
  */
 struct v4l2_meta_format {
 	__u32				dataformat;
 	__u32				buffersize;
+	__u32				width;
+	__u32				height;
+	__u32				bytesperline;
 } __attribute__ ((packed));
 
 /**
  * struct v4l2_format - stream data format
- * @type:	enum v4l2_buf_type; type of the data stream
- * @pix:	definition of an image format
- * @pix_mp:	definition of a multiplanar image format
- * @win:	definition of an overlaid image
- * @vbi:	raw VBI capture or output parameters
- * @sliced:	sliced VBI capture or output parameters
- * @raw_data:	placeholder for future extensions and custom formats
- * @fmt:	union of @pix, @pix_mp, @win, @vbi, @sliced, @sdr, @meta
- *		and @raw_data
+ * @type:		enum v4l2_buf_type; type of the data stream
+ * @fmt.pix:		definition of an image format
+ * @fmt.pix_mp:		definition of a multiplanar image format
+ * @fmt.win:		definition of an overlaid image
+ * @fmt.vbi:		raw VBI capture or output parameters
+ * @fmt.sliced:		sliced VBI capture or output parameters
+ * @fmt.raw_data:	placeholder for future extensions and custom formats
+ * @fmt:		union of @pix, @pix_mp, @win, @vbi, @sliced, @sdr,
+ *			@meta and @raw_data
  */
 struct v4l2_format {
 	__u32	 type;
@@ -2367,6 +2463,7 @@
 #define V4L2_EVENT_CTRL_CH_VALUE		(1 << 0)
 #define V4L2_EVENT_CTRL_CH_FLAGS		(1 << 1)
 #define V4L2_EVENT_CTRL_CH_RANGE		(1 << 2)
+#define V4L2_EVENT_CTRL_CH_DIMENSIONS		(1 << 3)
 
 struct v4l2_event_ctrl {
 	__u32 changes;
@@ -2489,6 +2586,9 @@
  * @flags:	additional buffer management attributes (ignored unless the
  *		queue has V4L2_BUF_CAP_SUPPORTS_MMAP_CACHE_HINTS capability
  *		and configured for MMAP streaming I/O).
+ * @max_num_buffers: if V4L2_BUF_CAP_SUPPORTS_MAX_NUM_BUFFERS capability flag is set
+ *		this field indicate the maximum possible number of buffers
+ *		for this queue.
  * @reserved:	future extensions
  */
 struct v4l2_create_buffers {
@@ -2498,7 +2598,22 @@
 	struct v4l2_format	format;
 	__u32			capabilities;
 	__u32			flags;
-	__u32			reserved[6];
+	__u32			max_num_buffers;
+	__u32			reserved[5];
+};
+
+/**
+ * struct v4l2_remove_buffers - VIDIOC_REMOVE_BUFS argument
+ * @index:	the first buffer to be removed
+ * @count:	number of buffers to removed
+ * @type:	enum v4l2_buf_type
+ * @reserved:	future extensions
+ */
+struct v4l2_remove_buffers {
+	__u32			index;
+	__u32			count;
+	__u32			type;
+	__u32			reserved[13];
 };
 
 /*
@@ -2600,6 +2715,8 @@
 #define VIDIOC_DBG_G_CHIP_INFO  _IOWR('V', 102, struct v4l2_dbg_chip_info)
 
 #define VIDIOC_QUERY_EXT_CTRL	_IOWR('V', 103, struct v4l2_query_ext_ctrl)
+#define VIDIOC_REMOVE_BUFS	_IOWR('V', 104, struct v4l2_remove_buffers)
+
 
 /* Reminder: when adding new ioctls please add support for them to
    drivers/media/v4l2-core/v4l2-compat-ioctl32.c as well! */
@@ -2609,5 +2726,10 @@
 /* Deprecated definitions kept for backwards compatibility */
 #define V4L2_PIX_FMT_HM12 V4L2_PIX_FMT_NV12_16L16
 #define V4L2_PIX_FMT_SUNXI_TILED_NV12 V4L2_PIX_FMT_NV12_32L32
+/*
+ * This capability was never implemented, anyone using this cap should drop it
+ * from their code.
+ */
+#define V4L2_CAP_ASYNCIO 0x02000000
 
 #endif /* __LINUX_VIDEODEV2_H */
diff --git a/meson.build b/meson.build
index e9a1c7e..2acd8c3 100644
--- a/meson.build
+++ b/meson.build
@@ -1,8 +1,8 @@
 # SPDX-License-Identifier: CC0-1.0
 
 project('libcamera', 'c', 'cpp',
-    meson_version : '>= 0.57',
-    version : '0.1.0',
+    meson_version : '>= 0.60',
+    version : '0.3.0',
     default_options : [
         'werror=true',
         'warning_level=2',
@@ -74,6 +74,10 @@
 cxx = meson.get_compiler('cpp')
 config_h = configuration_data()
 
+if cc.has_header_symbol('fcntl.h', 'F_ADD_SEALS', prefix : '#define _GNU_SOURCE')
+    config_h.set('HAVE_FILE_SEALS', 1)
+endif
+
 if cc.has_header_symbol('unistd.h', 'issetugid')
     config_h.set('HAVE_ISSETUGID', 1)
 endif
@@ -82,11 +86,16 @@
     config_h.set('HAVE_LOCALE_T', 1)
 endif
 
+if cc.has_header_symbol('sys/mman.h', 'memfd_create', prefix : '#define _GNU_SOURCE')
+    config_h.set('HAVE_MEMFD_CREATE', 1)
+endif
+
 if cc.has_header_symbol('stdlib.h', 'secure_getenv', prefix : '#define _GNU_SOURCE')
     config_h.set('HAVE_SECURE_GETENV', 1)
 endif
 
 common_arguments = [
+    '-Wmissing-declarations',
     '-Wshadow',
     '-include', meson.current_build_dir() / 'config.h',
 ]
@@ -198,6 +207,7 @@
 pipelines_support = {
     'imx8-isi':     arch_arm,
     'ipu3':         arch_x86,
+    'mali-c55':     arch_arm,
     'rkisp1':       arch_arm,
     'rpi/vc4':      arch_arm,
     'simple':       arch_arm,
@@ -267,6 +277,8 @@
 summary({
             'Enabled pipelines': pipelines,
             'Enabled IPA modules': enabled_ipa_names,
+            'Controls files': controls_files,
+            'Properties files': properties_files,
             'Hotplug support': libudev.found(),
             'Tracing support': tracing_enabled,
             'Android support': android_enabled,
diff --git a/meson_options.txt b/meson_options.txt
index fad928a..7aa4124 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -20,6 +20,11 @@
         type : 'feature',
         description : 'Generate the project documentation')
 
+option('doc_werror',
+        type : 'boolean',
+        value : false,
+        description : 'Treat documentation warnings as errors')
+
 option('gstreamer',
         type : 'feature',
         value : 'auto',
@@ -27,7 +32,7 @@
 
 option('ipas',
         type : 'array',
-        choices : ['ipu3', 'rkisp1', 'rpi/vc4', 'vimc'],
+        choices : ['ipu3', 'rkisp1', 'rpi/vc4', 'simple', 'vimc'],
         description : 'Select which IPA modules to build')
 
 option('lc-compliance',
@@ -43,6 +48,7 @@
             'auto',
             'imx8-isi',
             'ipu3',
+            'mali-c55',
             'rkisp1',
             'rpi/vc4',
             'simple',
@@ -51,6 +57,11 @@
         ],
         description : 'Select which pipeline handlers to build. If this is set to "auto", all the pipelines applicable to the target architecture will be built. If this is set to "all", all the pipelines will be built. If both are selected then "all" will take precedence.')
 
+option('pycamera',
+        type : 'feature',
+        value : 'auto',
+        description : 'Enable libcamera Python bindings (experimental)')
+
 option('qcam',
         type : 'feature',
         value : 'auto',
@@ -75,8 +86,3 @@
         type : 'boolean',
         value : false,
         description : 'Compile the V4L2 compatibility layer')
-
-option('pycamera',
-        type : 'feature',
-        value : 'disabled',
-        description : 'Enable libcamera Python bindings (experimental)')
diff --git a/src/android/camera3_hal.cpp b/src/android/camera3_hal.cpp
index da836ba..a5ad237 100644
--- a/src/android/camera3_hal.cpp
+++ b/src/android/camera3_hal.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera3_hal.cpp - Android Camera HALv3 module
+ * Android Camera HALv3 module
  */
 
 #include <hardware/camera_common.h>
diff --git a/src/android/camera_buffer.h b/src/android/camera_buffer.h
index b4531c8..9666996 100644
--- a/src/android/camera_buffer.h
+++ b/src/android/camera_buffer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_buffer.h - Frame buffer handling interface definition
+ * Frame buffer handling interface definition
  */
 
 #pragma once
diff --git a/src/android/camera_capabilities.cpp b/src/android/camera_capabilities.cpp
index 1bfeaea..71043e1 100644
--- a/src/android/camera_capabilities.cpp
+++ b/src/android/camera_capabilities.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_capabilities.cpp - Camera static properties manager
+ * Camera static properties manager
  */
 
 #include "camera_capabilities.h"
@@ -1081,7 +1081,7 @@
 	}
 
 	{
-		const Span<const Rectangle> &rects =
+		const Span<const Rectangle> rects =
 			properties.get(properties::PixelArrayActiveAreas).value_or(Span<const Rectangle>{});
 		std::vector<int32_t> data{
 			static_cast<int32_t>(rects[0].x),
diff --git a/src/android/camera_capabilities.h b/src/android/camera_capabilities.h
index 6f66f22..56ac1ef 100644
--- a/src/android/camera_capabilities.h
+++ b/src/android/camera_capabilities.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_capabilities.h - Camera static properties manager
+ * Camera static properties manager
  */
 
 #pragma once
diff --git a/src/android/camera_device.cpp b/src/android/camera_device.cpp
index 25cedd4..493f66e 100644
--- a/src/android/camera_device.cpp
+++ b/src/android/camera_device.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_device.cpp - libcamera Android Camera Device
+ * libcamera Android Camera Device
  */
 
 #include "camera_device.h"
@@ -433,8 +433,6 @@
 void CameraDevice::stop()
 {
 	MutexLocker stateLock(stateMutex_);
-	if (state_ == State::Stopped)
-		return;
 
 	camera_->stop();
 
@@ -952,8 +950,8 @@
 	 */
 	if (camera3Request->settings)
 		lastSettings_ = camera3Request->settings;
-	else
-		descriptor->settings_ = lastSettings_;
+
+	descriptor->settings_ = lastSettings_;
 
 	LOG(HAL, Debug) << "Queueing request " << descriptor->request_->cookie()
 			<< " with " << descriptor->buffers_.size() << " streams";
@@ -1108,6 +1106,8 @@
 	}
 
 	if (state_ == State::Stopped) {
+		lastSettings_ = {};
+
 		ret = camera_->start();
 		if (ret) {
 			LOG(HAL, Error) << "Failed to start camera";
diff --git a/src/android/camera_device.h b/src/android/camera_device.h
index 43ee015..194ca30 100644
--- a/src/android/camera_device.h
+++ b/src/android/camera_device.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_device.h - libcamera Android Camera Device
+ * libcamera Android Camera Device
  */
 
 #pragma once
diff --git a/src/android/camera_hal_config.cpp b/src/android/camera_hal_config.cpp
index 0e7cde6..7ef451e 100644
--- a/src/android/camera_hal_config.cpp
+++ b/src/android/camera_hal_config.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_hal_config.cpp - Camera HAL configuration file manager
+ * Camera HAL configuration file manager
  */
 #include "camera_hal_config.h"
 
diff --git a/src/android/camera_hal_config.h b/src/android/camera_hal_config.h
index 9df554f..a4bedb6 100644
--- a/src/android/camera_hal_config.h
+++ b/src/android/camera_hal_config.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_hal_config.h - Camera HAL configuration file manager
+ * Camera HAL configuration file manager
  */
 
 #pragma once
diff --git a/src/android/camera_hal_manager.cpp b/src/android/camera_hal_manager.cpp
index a86e23d..7500c74 100644
--- a/src/android/camera_hal_manager.cpp
+++ b/src/android/camera_hal_manager.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_hal_manager.cpp - libcamera Android Camera Manager
+ * libcamera Android Camera Manager
  */
 
 #include "camera_hal_manager.h"
diff --git a/src/android/camera_hal_manager.h b/src/android/camera_hal_manager.h
index a5f8b93..836a8da 100644
--- a/src/android/camera_hal_manager.h
+++ b/src/android/camera_hal_manager.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_hal_manager.h - libcamera Android Camera Manager
+ * libcamera Android Camera Manager
  */
 
 #pragma once
diff --git a/src/android/camera_metadata.cpp b/src/android/camera_metadata.cpp
index b3e515d..99f033f 100644
--- a/src/android/camera_metadata.cpp
+++ b/src/android/camera_metadata.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_metadata.cpp - libcamera Android Camera Metadata Helper
+ * libcamera Android Camera Metadata Helper
  */
 
 #include "camera_metadata.h"
diff --git a/src/android/camera_metadata.h b/src/android/camera_metadata.h
index 0c31ec6..474f280 100644
--- a/src/android/camera_metadata.h
+++ b/src/android/camera_metadata.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_metadata.h - libcamera Android Camera Metadata Helper
+ * libcamera Android Camera Metadata Helper
  */
 
 #pragma once
diff --git a/src/android/camera_ops.cpp b/src/android/camera_ops.cpp
index 8a3cfa1..ecaac5a 100644
--- a/src/android/camera_ops.cpp
+++ b/src/android/camera_ops.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_ops.h - Android Camera HAL Operations
+ * Android Camera HAL Operations
  */
 
 #include "camera_ops.h"
diff --git a/src/android/camera_ops.h b/src/android/camera_ops.h
index b501bb7..750dc94 100644
--- a/src/android/camera_ops.h
+++ b/src/android/camera_ops.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_ops.h - Android Camera HAL Operations
+ * Android Camera HAL Operations
  */
 
 #pragma once
diff --git a/src/android/camera_request.cpp b/src/android/camera_request.cpp
index 6c87adb..0d45960 100644
--- a/src/android/camera_request.cpp
+++ b/src/android/camera_request.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2021, Google Inc.
  *
- * camera_request.cpp - libcamera Android Camera Request Descriptor
+ * libcamera Android Camera Request Descriptor
  */
 
 #include "camera_request.h"
diff --git a/src/android/camera_request.h b/src/android/camera_request.h
index 20aba79..5b47918 100644
--- a/src/android/camera_request.h
+++ b/src/android/camera_request.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2021, Google Inc.
  *
- * camera_request.h - libcamera Android Camera Request Descriptor
+ * libcamera Android Camera Request Descriptor
  */
 
 #pragma once
diff --git a/src/android/camera_stream.cpp b/src/android/camera_stream.cpp
index 045e600..1d68540 100644
--- a/src/android/camera_stream.cpp
+++ b/src/android/camera_stream.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * camera_stream.cpp - Camera HAL stream
+ * Camera HAL stream
  */
 
 #include "camera_stream.h"
diff --git a/src/android/camera_stream.h b/src/android/camera_stream.h
index 4c5078b..395552d 100644
--- a/src/android/camera_stream.h
+++ b/src/android/camera_stream.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * camera_stream.h - Camera HAL stream
+ * Camera HAL stream
  */
 
 #pragma once
diff --git a/src/android/cros/camera3_hal.cpp b/src/android/cros/camera3_hal.cpp
index 71acb44..6010a5a 100644
--- a/src/android/cros/camera3_hal.cpp
+++ b/src/android/cros/camera3_hal.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera3_hal.cpp - cros-specific components of Android Camera HALv3 module
+ * cros-specific components of Android Camera HALv3 module
  */
 
 #include <cros-camera/cros_camera_hal.h>
diff --git a/src/android/cros_mojo_token.h b/src/android/cros_mojo_token.h
index 043c752..d0baa80 100644
--- a/src/android/cros_mojo_token.h
+++ b/src/android/cros_mojo_token.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * cros_mojo_token.h - cros-specific mojo token
+ * cros-specific mojo token
  */
 
 #pragma once
diff --git a/src/android/frame_buffer_allocator.h b/src/android/frame_buffer_allocator.h
index e5c9492..3e68641 100644
--- a/src/android/frame_buffer_allocator.h
+++ b/src/android/frame_buffer_allocator.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * frame_buffer_allocator.h - Interface definition to allocate Frame buffer in
+ * Interface definition to allocate Frame buffer in
  * platform dependent way.
  */
 #ifndef __ANDROID_FRAME_BUFFER_ALLOCATOR_H__
diff --git a/src/android/hal_framebuffer.cpp b/src/android/hal_framebuffer.cpp
index 3f3d1ed..d4899f4 100644
--- a/src/android/hal_framebuffer.cpp
+++ b/src/android/hal_framebuffer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * hal_framebuffer.cpp - HAL Frame Buffer Handling
+ * HAL Frame Buffer Handling
  */
 
 #include "hal_framebuffer.h"
diff --git a/src/android/hal_framebuffer.h b/src/android/hal_framebuffer.h
index dc96a7e..cea49e2 100644
--- a/src/android/hal_framebuffer.h
+++ b/src/android/hal_framebuffer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * hal_framebuffer.h - HAL Frame Buffer Handling
+ * HAL Frame Buffer Handling
  */
 
 #pragma once
diff --git a/src/android/jpeg/encoder.h b/src/android/jpeg/encoder.h
index 31f2689..ed033c1 100644
--- a/src/android/jpeg/encoder.h
+++ b/src/android/jpeg/encoder.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * encoder.h - Image encoding interface
+ * Image encoding interface
  */
 
 #pragma once
diff --git a/src/android/jpeg/encoder_jea.cpp b/src/android/jpeg/encoder_jea.cpp
index 7880a6b..25dc431 100644
--- a/src/android/jpeg/encoder_jea.cpp
+++ b/src/android/jpeg/encoder_jea.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * encoder_jea.cpp - JPEG encoding using CrOS JEA
+ * JPEG encoding using CrOS JEA
  */
 
 #include "encoder_jea.h"
diff --git a/src/android/jpeg/encoder_jea.h b/src/android/jpeg/encoder_jea.h
index ffe9df2..91115d2 100644
--- a/src/android/jpeg/encoder_jea.h
+++ b/src/android/jpeg/encoder_jea.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * encoder_jea.h - JPEG encoding using CrOS JEA
+ * JPEG encoding using CrOS JEA
  */
 
 #pragma once
diff --git a/src/android/jpeg/encoder_libjpeg.cpp b/src/android/jpeg/encoder_libjpeg.cpp
index f4e8dfa..7fc6287 100644
--- a/src/android/jpeg/encoder_libjpeg.cpp
+++ b/src/android/jpeg/encoder_libjpeg.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * encoder_libjpeg.cpp - JPEG encoding using libjpeg native API
+ * JPEG encoding using libjpeg native API
  */
 
 #include "encoder_libjpeg.h"
diff --git a/src/android/jpeg/encoder_libjpeg.h b/src/android/jpeg/encoder_libjpeg.h
index 146a6a7..4ac85c2 100644
--- a/src/android/jpeg/encoder_libjpeg.h
+++ b/src/android/jpeg/encoder_libjpeg.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * encoder_libjpeg.h - JPEG encoding using libjpeg
+ * JPEG encoding using libjpeg
  */
 
 #pragma once
diff --git a/src/android/jpeg/exif.cpp b/src/android/jpeg/exif.cpp
index 6b1d0f1..b8c871d 100644
--- a/src/android/jpeg/exif.cpp
+++ b/src/android/jpeg/exif.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * exif.cpp - EXIF tag creation using libexif
+ * EXIF tag creation using libexif
  */
 
 #include "exif.h"
diff --git a/src/android/jpeg/exif.h b/src/android/jpeg/exif.h
index e68716f..446d53f 100644
--- a/src/android/jpeg/exif.h
+++ b/src/android/jpeg/exif.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * exif.h - EXIF tag creator using libexif
+ * EXIF tag creator using libexif
  */
 
 #pragma once
diff --git a/src/android/jpeg/post_processor_jpeg.cpp b/src/android/jpeg/post_processor_jpeg.cpp
index 4026165..89b8a40 100644
--- a/src/android/jpeg/post_processor_jpeg.cpp
+++ b/src/android/jpeg/post_processor_jpeg.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * post_processor_jpeg.cpp - JPEG Post Processor
+ * JPEG Post Processor
  */
 
 #include "post_processor_jpeg.h"
diff --git a/src/android/jpeg/post_processor_jpeg.h b/src/android/jpeg/post_processor_jpeg.h
index 98309b0..6fe2145 100644
--- a/src/android/jpeg/post_processor_jpeg.h
+++ b/src/android/jpeg/post_processor_jpeg.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * post_processor_jpeg.h - JPEG Post Processor
+ * JPEG Post Processor
  */
 
 #pragma once
diff --git a/src/android/jpeg/thumbnailer.cpp b/src/android/jpeg/thumbnailer.cpp
index 41c71c7..adafc46 100644
--- a/src/android/jpeg/thumbnailer.cpp
+++ b/src/android/jpeg/thumbnailer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * thumbnailer.cpp - Simple image thumbnailer
+ * Simple image thumbnailer
  */
 
 #include "thumbnailer.h"
diff --git a/src/android/jpeg/thumbnailer.h b/src/android/jpeg/thumbnailer.h
index d933cf0..1b836e5 100644
--- a/src/android/jpeg/thumbnailer.h
+++ b/src/android/jpeg/thumbnailer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * thumbnailer.h - Simple image thumbnailer
+ * Simple image thumbnailer
  */
 
 #pragma once
diff --git a/src/android/mm/cros_camera_buffer.cpp b/src/android/mm/cros_camera_buffer.cpp
index 2ac3dc4..e2a44a2 100644
--- a/src/android/mm/cros_camera_buffer.cpp
+++ b/src/android/mm/cros_camera_buffer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * cros_camera_buffer.cpp - Chromium OS buffer backend using CameraBufferManager
+ * Chromium OS buffer backend using CameraBufferManager
  */
 
 #include "../camera_buffer.h"
diff --git a/src/android/mm/cros_frame_buffer_allocator.cpp b/src/android/mm/cros_frame_buffer_allocator.cpp
index 0a5c59f..264c0d4 100644
--- a/src/android/mm/cros_frame_buffer_allocator.cpp
+++ b/src/android/mm/cros_frame_buffer_allocator.cpp
@@ -2,8 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * cros_frame_buffer.cpp - Allocate FrameBuffer for Chromium OS using
- * CameraBufferManager
+ * Allocate FrameBuffer for Chromium OS using CameraBufferManager
  */
 
 #include <memory>
diff --git a/src/android/mm/generic_camera_buffer.cpp b/src/android/mm/generic_camera_buffer.cpp
index 1bd7090..0ffcb44 100644
--- a/src/android/mm/generic_camera_buffer.cpp
+++ b/src/android/mm/generic_camera_buffer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * generic_camera_buffer.cpp - Generic Android frame buffer backend
+ * Generic Android frame buffer backend
  */
 
 #include "../camera_buffer.h"
diff --git a/src/android/mm/generic_frame_buffer_allocator.cpp b/src/android/mm/generic_frame_buffer_allocator.cpp
index 7ecef2c..79625a9 100644
--- a/src/android/mm/generic_frame_buffer_allocator.cpp
+++ b/src/android/mm/generic_frame_buffer_allocator.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * generic_camera_buffer.cpp - Allocate FrameBuffer using gralloc API
+ * Allocate FrameBuffer using gralloc API
  */
 
 #include <dlfcn.h>
diff --git a/src/android/mm/libhardware_stub.c b/src/android/mm/libhardware_stub.c
index 00f15cd..28faa63 100644
--- a/src/android/mm/libhardware_stub.c
+++ b/src/android/mm/libhardware_stub.c
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2023, Ideas on Board
  *
- * libhardware_stub.c - Android libhardware stub for test compilation
+ * Android libhardware stub for test compilation
  */
 
 #include <errno.h>
diff --git a/src/android/post_processor.h b/src/android/post_processor.h
index 1a205b0..b504a37 100644
--- a/src/android/post_processor.h
+++ b/src/android/post_processor.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * post_processor.h - CameraStream Post Processing Interface
+ * CameraStream Post Processing Interface
  */
 
 #pragma once
diff --git a/src/android/yuv/post_processor_yuv.cpp b/src/android/yuv/post_processor_yuv.cpp
index ed44e6f..c998807 100644
--- a/src/android/yuv/post_processor_yuv.cpp
+++ b/src/android/yuv/post_processor_yuv.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * post_processor_yuv.cpp - Post Processor using libyuv
+ * Post Processor using libyuv
  */
 
 #include "post_processor_yuv.h"
diff --git a/src/android/yuv/post_processor_yuv.h b/src/android/yuv/post_processor_yuv.h
index a7ac17c..ed7bb1f 100644
--- a/src/android/yuv/post_processor_yuv.h
+++ b/src/android/yuv/post_processor_yuv.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * post_processor_yuv.h - Post Processor using libyuv
+ * Post Processor using libyuv
  */
 
 #pragma once
diff --git a/src/apps/cam/camera_session.cpp b/src/apps/cam/camera_session.cpp
index 066e397..097dc47 100644
--- a/src/apps/cam/camera_session.cpp
+++ b/src/apps/cam/camera_session.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_session.cpp - Camera capture session
+ * Camera capture session
  */
 
 #include <iomanip>
@@ -39,9 +39,14 @@
 {
 	char *endptr;
 	unsigned long index = strtoul(cameraId.c_str(), &endptr, 10);
-	if (*endptr == '\0' && index > 0 && index <= cm->cameras().size())
-		camera_ = cm->cameras()[index - 1];
-	else
+
+	if (*endptr == '\0' && index > 0) {
+		auto cameras = cm->cameras();
+		if (index <= cameras.size())
+			camera_ = cameras[index - 1];
+	}
+
+	if (!camera_)
 		camera_ = cm->get(cameraId);
 
 	if (!camera_) {
@@ -65,6 +70,24 @@
 		return;
 	}
 
+	if (options_.isSet(OptOrientation)) {
+		std::string orientOpt = options_[OptOrientation].toString();
+		static const std::map<std::string, libcamera::Orientation> orientations{
+			{ "rot0", libcamera::Orientation::Rotate0 },
+			{ "rot180", libcamera::Orientation::Rotate180 },
+			{ "mirror", libcamera::Orientation::Rotate0Mirror },
+			{ "flip", libcamera::Orientation::Rotate180Mirror },
+		};
+
+		auto orientation = orientations.find(orientOpt);
+		if (orientation == orientations.end()) {
+			std::cerr << "Invalid orientation " << orientOpt << std::endl;
+			return;
+		}
+
+		config->orientation = orientation->second;
+	}
+
 	/* Apply configuration if explicitly requested. */
 	if (StreamKeyValueParser::updateConfiguration(config.get(),
 						      options_[OptStream])) {
@@ -359,7 +382,7 @@
 	 * Defer processing of the completed request to the event loop, to avoid
 	 * blocking the camera manager thread.
 	 */
-	EventLoop::instance()->callLater([=]() { processRequest(request); });
+	EventLoop::instance()->callLater([this, request]() { processRequest(request); });
 }
 
 void CameraSession::processRequest(Request *request)
diff --git a/src/apps/cam/camera_session.h b/src/apps/cam/camera_session.h
index 0bab519..4442fd9 100644
--- a/src/apps/cam/camera_session.h
+++ b/src/apps/cam/camera_session.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_session.h - Camera capture session
+ * Camera capture session
  */
 
 #pragma once
diff --git a/src/apps/cam/capture_script.cpp b/src/apps/cam/capture_script.cpp
index 062a725..fc1dfa7 100644
--- a/src/apps/cam/capture_script.cpp
+++ b/src/apps/cam/capture_script.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * capture_script.cpp - Capture session configuration script
+ * Capture session configuration script
  */
 
 #include "capture_script.h"
@@ -351,7 +351,10 @@
 	}
 
 	ControlValue controlValue;
-	controlValue.set(Span<const Rectangle>(rectangles));
+	if (rectangles.size() == 1)
+		controlValue.set(rectangles.at(0));
+	else
+		controlValue.set(Span<const Rectangle>(rectangles));
 
 	return controlValue;
 }
diff --git a/src/apps/cam/capture_script.h b/src/apps/cam/capture_script.h
index 40042c0..294b920 100644
--- a/src/apps/cam/capture_script.h
+++ b/src/apps/cam/capture_script.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * capture_script.h - Capture session configuration script
+ * Capture session configuration script
  */
 
 #pragma once
diff --git a/src/apps/cam/drm.cpp b/src/apps/cam/drm.cpp
index 8779a71..47bbb6b 100644
--- a/src/apps/cam/drm.cpp
+++ b/src/apps/cam/drm.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * drm.cpp - DRM/KMS Helpers
+ * DRM/KMS Helpers
  */
 
 #include "drm.h"
diff --git a/src/apps/cam/drm.h b/src/apps/cam/drm.h
index ebaea04..1ba83b6 100644
--- a/src/apps/cam/drm.h
+++ b/src/apps/cam/drm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * drm.h - DRM/KMS Helpers
+ * DRM/KMS Helpers
  */
 
 #pragma once
diff --git a/src/apps/cam/file_sink.cpp b/src/apps/cam/file_sink.cpp
index dca350c..3e000d2 100644
--- a/src/apps/cam/file_sink.cpp
+++ b/src/apps/cam/file_sink.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * file_sink.cpp - File Sink
+ * File Sink
  */
 
 #include <assert.h>
@@ -17,6 +17,7 @@
 
 #include "../common/dng_writer.h"
 #include "../common/image.h"
+#include "../common/ppm_writer.h"
 
 #include "file_sink.h"
 
@@ -76,6 +77,7 @@
 #ifdef HAVE_TIFF
 	bool dng = filename.find(".dng", filename.size() - 4) != std::string::npos;
 #endif /* HAVE_TIFF */
+	bool ppm = filename.find(".ppm", filename.size() - 4) != std::string::npos;
 
 	if (filename.empty() || filename.back() == '/')
 		filename += "frame-#.bin";
@@ -102,6 +104,15 @@
 		return;
 	}
 #endif /* HAVE_TIFF */
+	if (ppm) {
+		ret = PPMWriter::write(filename.c_str(), stream->configuration(),
+				       image->data(0));
+		if (ret < 0)
+			std::cerr << "failed to write PPM file `" << filename
+				  << "'" << std::endl;
+
+		return;
+	}
 
 	fd = open(filename.c_str(), O_CREAT | O_WRONLY |
 		  (pos == std::string::npos ? O_APPEND : O_TRUNC),
diff --git a/src/apps/cam/file_sink.h b/src/apps/cam/file_sink.h
index 300edf8..9d56078 100644
--- a/src/apps/cam/file_sink.h
+++ b/src/apps/cam/file_sink.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * file_sink.h - File Sink
+ * File Sink
  */
 
 #pragma once
diff --git a/src/apps/cam/frame_sink.cpp b/src/apps/cam/frame_sink.cpp
index af21d57..68d6f2c 100644
--- a/src/apps/cam/frame_sink.cpp
+++ b/src/apps/cam/frame_sink.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * frame_sink.cpp - Base Frame Sink Class
+ * Base Frame Sink Class
  */
 
 #include "frame_sink.h"
diff --git a/src/apps/cam/frame_sink.h b/src/apps/cam/frame_sink.h
index ca4347c..11105c6 100644
--- a/src/apps/cam/frame_sink.h
+++ b/src/apps/cam/frame_sink.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * frame_sink.h - Base Frame Sink Class
+ * Base Frame Sink Class
  */
 
 #pragma once
diff --git a/src/apps/cam/kms_sink.cpp b/src/apps/cam/kms_sink.cpp
index 6991c3f..672c985 100644
--- a/src/apps/cam/kms_sink.cpp
+++ b/src/apps/cam/kms_sink.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * kms_sink.cpp - KMS Sink
+ * KMS Sink
  */
 
 #include "kms_sink.h"
diff --git a/src/apps/cam/kms_sink.h b/src/apps/cam/kms_sink.h
index e2c618a..4b7b4c2 100644
--- a/src/apps/cam/kms_sink.h
+++ b/src/apps/cam/kms_sink.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * kms_sink.h - KMS Sink
+ * KMS Sink
  */
 
 #pragma once
diff --git a/src/apps/cam/main.cpp b/src/apps/cam/main.cpp
index 5d8a57b..460dbc8 100644
--- a/src/apps/cam/main.cpp
+++ b/src/apps/cam/main.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * main.cpp - cam - The libcamera swiss army knife
+ * cam - The libcamera swiss army knife
  */
 
 #include <atomic>
@@ -133,6 +133,11 @@
 			 "Capture until interrupted by user or until <count> frames captured",
 			 "capture", ArgumentOptional, "count", false,
 			 OptCamera);
+
+	parser.addOption(OptOrientation, OptionString,
+			 "Desired image orientation (rot0, rot180, mirror, flip)",
+			 "orientation", ArgumentRequired, "orientation", false,
+			 OptCamera);
 #ifdef HAVE_KMS
 	parser.addOption(OptDisplay, OptionString,
 			 "Display viewfinder through DRM/KMS on specified connector",
@@ -149,6 +154,8 @@
 			 "If the file name ends with '.dng', then the frame will be written to\n"
 			 "the output file(s) in DNG format.\n"
 #endif
+			 "If the file name ends with '.ppm', then the frame will be written to\n"
+			 "the output file(s) in PPM format.\n"
 			 "The default file name is 'frame-#.bin'.",
 			 "file", ArgumentOptional, "filename", false,
 			 OptCamera);
@@ -337,12 +344,16 @@
 	return name;
 }
 
+namespace {
+
 void signalHandler([[maybe_unused]] int signal)
 {
 	std::cout << "Exiting" << std::endl;
 	CamApp::instance()->quit();
 }
 
+} /* namespace */
+
 int main(int argc, char **argv)
 {
 	CamApp app;
diff --git a/src/apps/cam/main.h b/src/apps/cam/main.h
index 526aece..64e6a20 100644
--- a/src/apps/cam/main.h
+++ b/src/apps/cam/main.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * main.h - Cam application
+ * Cam application
  */
 
 #pragma once
@@ -17,6 +17,7 @@
 	OptList = 'l',
 	OptListProperties = 'p',
 	OptMonitor = 'm',
+	OptOrientation = 'o',
 	OptSDL = 'S',
 	OptStream = 's',
 	OptListControls = 256,
diff --git a/src/apps/cam/meson.build b/src/apps/cam/meson.build
index 48c834a..c70ca3c 100644
--- a/src/apps/cam/meson.build
+++ b/src/apps/cam/meson.build
@@ -58,4 +58,5 @@
                       libyaml,
                   ],
                   cpp_args : cam_cpp_args,
-                  install : true)
+                  install : true,
+                  install_tag : 'bin')
diff --git a/src/apps/cam/sdl_sink.cpp b/src/apps/cam/sdl_sink.cpp
index a2f4abc..8355dd5 100644
--- a/src/apps/cam/sdl_sink.cpp
+++ b/src/apps/cam/sdl_sink.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * sdl_sink.h - SDL Sink
+ * SDL Sink
  */
 
 #include "sdl_sink.h"
diff --git a/src/apps/cam/sdl_sink.h b/src/apps/cam/sdl_sink.h
index 6c19c66..18ec7fb 100644
--- a/src/apps/cam/sdl_sink.h
+++ b/src/apps/cam/sdl_sink.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * sdl_sink.h - SDL Sink
+ * SDL Sink
  */
 
 #pragma once
diff --git a/src/apps/cam/sdl_texture.cpp b/src/apps/cam/sdl_texture.cpp
index e9040bc..e52c4a3 100644
--- a/src/apps/cam/sdl_texture.cpp
+++ b/src/apps/cam/sdl_texture.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * sdl_texture.cpp - SDL Texture
+ * SDL Texture
  */
 
 #include "sdl_texture.h"
diff --git a/src/apps/cam/sdl_texture.h b/src/apps/cam/sdl_texture.h
index 3993dd4..990f83b 100644
--- a/src/apps/cam/sdl_texture.h
+++ b/src/apps/cam/sdl_texture.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * sdl_texture.h - SDL Texture
+ * SDL Texture
  */
 
 #pragma once
diff --git a/src/apps/cam/sdl_texture_mjpg.cpp b/src/apps/cam/sdl_texture_mjpg.cpp
index da958e0..cace18f 100644
--- a/src/apps/cam/sdl_texture_mjpg.cpp
+++ b/src/apps/cam/sdl_texture_mjpg.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * sdl_texture_mjpg.cpp - SDL Texture MJPG
+ * SDL Texture MJPG
  */
 
 #include "sdl_texture_mjpg.h"
diff --git a/src/apps/cam/sdl_texture_mjpg.h b/src/apps/cam/sdl_texture_mjpg.h
index 814ca79..37bed5f 100644
--- a/src/apps/cam/sdl_texture_mjpg.h
+++ b/src/apps/cam/sdl_texture_mjpg.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * sdl_texture_mjpg.h - SDL Texture MJPG
+ * SDL Texture MJPG
  */
 
 #pragma once
diff --git a/src/apps/cam/sdl_texture_yuv.cpp b/src/apps/cam/sdl_texture_yuv.cpp
index b29c3b9..480d7a3 100644
--- a/src/apps/cam/sdl_texture_yuv.cpp
+++ b/src/apps/cam/sdl_texture_yuv.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * sdl_texture_yuv.cpp - SDL YUV Textures
+ * SDL YUV Textures
  */
 
 #include "sdl_texture_yuv.h"
diff --git a/src/apps/cam/sdl_texture_yuv.h b/src/apps/cam/sdl_texture_yuv.h
index 310e4e5..29c756e 100644
--- a/src/apps/cam/sdl_texture_yuv.h
+++ b/src/apps/cam/sdl_texture_yuv.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy
  *
- * sdl_texture_yuv.h - SDL YUV Textures
+ * SDL YUV Textures
  */
 
 #pragma once
diff --git a/src/apps/common/dng_writer.cpp b/src/apps/common/dng_writer.cpp
index c945edc..9241f23 100644
--- a/src/apps/common/dng_writer.cpp
+++ b/src/apps/common/dng_writer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * dng_writer.cpp - DNG writer
+ * DNG writer
  */
 
 #include "dng_writer.h"
@@ -126,7 +126,9 @@
 	float m[9];
 };
 
-void packScanlineSBGGR8(void *output, const void *input, unsigned int width)
+namespace {
+
+void packScanlineRaw8(void *output, const void *input, unsigned int width)
 {
 	const uint8_t *in = static_cast<const uint8_t *>(input);
 	uint8_t *out = static_cast<uint8_t *>(output);
@@ -134,7 +136,66 @@
 	std::copy(in, in + width, out);
 }
 
-void packScanlineSBGGR10P(void *output, const void *input, unsigned int width)
+void packScanlineRaw10(void *output, const void *input, unsigned int width)
+{
+	const uint16_t *in = static_cast<const uint16_t *>(input);
+	uint8_t *out = static_cast<uint8_t *>(output);
+
+	for (unsigned int i = 0; i < width; i += 4) {
+		*out++ = (in[0] & 0x3fc) >> 2;
+		*out++ = (in[0] & 0x003) << 6 | (in[1] & 0x3f0) >> 4;
+		*out++ = (in[1] & 0x00f) << 4 | (in[2] & 0x3c0) >> 6;
+		*out++ = (in[2] & 0x03f) << 2 | (in[3] & 0x300) >> 8;
+		*out++ = (in[3] & 0x0ff);
+		in += 4;
+	}
+}
+
+void packScanlineRaw12(void *output, const void *input, unsigned int width)
+{
+	const uint16_t *in = static_cast<const uint16_t *>(input);
+	uint8_t *out = static_cast<uint8_t *>(output);
+
+	for (unsigned int i = 0; i < width; i += 2) {
+		*out++ = (in[0] & 0xff0) >> 4;
+		*out++ = (in[0] & 0x00f) << 4 | (in[1] & 0xf00) >> 8;
+		*out++ = (in[1] & 0x0ff);
+		in += 2;
+	}
+}
+
+void packScanlineRaw16(void *output, const void *input, unsigned int width)
+{
+	const uint16_t *in = static_cast<const uint16_t *>(input);
+	uint16_t *out = static_cast<uint16_t *>(output);
+
+	std::copy(in, in + width, out);
+}
+
+/* Thumbnail function for raw data with each pixel aligned to 16bit. */
+void thumbScanlineRaw(const FormatInfo &info, void *output, const void *input,
+		      unsigned int width, unsigned int stride)
+{
+	const uint16_t *in = static_cast<const uint16_t *>(input);
+	const uint16_t *in2 = static_cast<const uint16_t *>(input) + stride / 2;
+	uint8_t *out = static_cast<uint8_t *>(output);
+
+	/* Shift down to 8. */
+	unsigned int shift = info.bitsPerSample - 8;
+
+	/* Simple averaging that produces greyscale RGB values. */
+	for (unsigned int x = 0; x < width; x++) {
+		uint16_t value = (in[0] + in[1] + in2[0] + in2[1]) >> 2;
+		value = value >> shift;
+		*out++ = value;
+		*out++ = value;
+		*out++ = value;
+		in += 16;
+		in2 += 16;
+	}
+}
+
+void packScanlineRaw10_CSI2P(void *output, const void *input, unsigned int width)
 {
 	const uint8_t *in = static_cast<const uint8_t *>(input);
 	uint8_t *out = static_cast<uint8_t *>(output);
@@ -150,7 +211,7 @@
 	}
 }
 
-void packScanlineSBGGR12P(void *output, const void *input, unsigned int width)
+void packScanlineRaw12_CSI2P(void *output, const void *input, unsigned int width)
 {
 	const uint8_t *in = static_cast<const uint8_t *>(input);
 	uint8_t *out = static_cast<uint8_t *>(output);
@@ -164,7 +225,7 @@
 	}
 }
 
-void thumbScanlineSBGGRxxP(const FormatInfo &info, void *output,
+void thumbScanlineRaw_CSI2P(const FormatInfo &info, void *output,
 			   const void *input, unsigned int width,
 			   unsigned int stride)
 {
@@ -248,6 +309,7 @@
 
 		uint16_t val1, val2, val3, val4;
 		switch (pixelInBlock % 4) {
+		default:
 		case 0:
 			val1 = (in[1] & 0x03) << 14 | (in[0] & 0xff) << 6;
 			val2 = (in[2] & 0x0f) << 12 | (in[1] & 0xfc) << 4;
@@ -281,78 +343,150 @@
 	}
 }
 
-static const std::map<PixelFormat, FormatInfo> formatInfo = {
+const std::map<PixelFormat, FormatInfo> formatInfo = {
 	{ formats::SBGGR8, {
 		.bitsPerSample = 8,
 		.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
-		.packScanline = packScanlineSBGGR8,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw8,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SGBRG8, {
 		.bitsPerSample = 8,
 		.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
-		.packScanline = packScanlineSBGGR8,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw8,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SGRBG8, {
 		.bitsPerSample = 8,
 		.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
-		.packScanline = packScanlineSBGGR8,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw8,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SRGGB8, {
 		.bitsPerSample = 8,
 		.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
-		.packScanline = packScanlineSBGGR8,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw8,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
+	} },
+	{ formats::SBGGR10, {
+		.bitsPerSample = 10,
+		.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+		.packScanline = packScanlineRaw10,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SGBRG10, {
+		.bitsPerSample = 10,
+		.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+		.packScanline = packScanlineRaw10,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SGRBG10, {
+		.bitsPerSample = 10,
+		.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+		.packScanline = packScanlineRaw10,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SRGGB10, {
+		.bitsPerSample = 10,
+		.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+		.packScanline = packScanlineRaw10,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SBGGR12, {
+		.bitsPerSample = 12,
+		.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+		.packScanline = packScanlineRaw12,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SGBRG12, {
+		.bitsPerSample = 12,
+		.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+		.packScanline = packScanlineRaw12,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SGRBG12, {
+		.bitsPerSample = 12,
+		.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+		.packScanline = packScanlineRaw12,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SRGGB12, {
+		.bitsPerSample = 12,
+		.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+		.packScanline = packScanlineRaw12,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SBGGR16, {
+		.bitsPerSample = 16,
+		.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
+		.packScanline = packScanlineRaw16,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SGBRG16, {
+		.bitsPerSample = 16,
+		.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
+		.packScanline = packScanlineRaw16,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SGRBG16, {
+		.bitsPerSample = 16,
+		.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
+		.packScanline = packScanlineRaw16,
+		.thumbScanline = thumbScanlineRaw,
+	} },
+	{ formats::SRGGB16, {
+		.bitsPerSample = 16,
+		.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
+		.packScanline = packScanlineRaw16,
+		.thumbScanline = thumbScanlineRaw,
 	} },
 	{ formats::SBGGR10_CSI2P, {
 		.bitsPerSample = 10,
 		.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
-		.packScanline = packScanlineSBGGR10P,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw10_CSI2P,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SGBRG10_CSI2P, {
 		.bitsPerSample = 10,
 		.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
-		.packScanline = packScanlineSBGGR10P,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw10_CSI2P,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SGRBG10_CSI2P, {
 		.bitsPerSample = 10,
 		.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
-		.packScanline = packScanlineSBGGR10P,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw10_CSI2P,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SRGGB10_CSI2P, {
 		.bitsPerSample = 10,
 		.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
-		.packScanline = packScanlineSBGGR10P,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw10_CSI2P,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SBGGR12_CSI2P, {
 		.bitsPerSample = 12,
 		.pattern = { CFAPatternBlue, CFAPatternGreen, CFAPatternGreen, CFAPatternRed },
-		.packScanline = packScanlineSBGGR12P,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw12_CSI2P,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SGBRG12_CSI2P, {
 		.bitsPerSample = 12,
 		.pattern = { CFAPatternGreen, CFAPatternBlue, CFAPatternRed, CFAPatternGreen },
-		.packScanline = packScanlineSBGGR12P,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw12_CSI2P,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SGRBG12_CSI2P, {
 		.bitsPerSample = 12,
 		.pattern = { CFAPatternGreen, CFAPatternRed, CFAPatternBlue, CFAPatternGreen },
-		.packScanline = packScanlineSBGGR12P,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw12_CSI2P,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SRGGB12_CSI2P, {
 		.bitsPerSample = 12,
 		.pattern = { CFAPatternRed, CFAPatternGreen, CFAPatternGreen, CFAPatternBlue },
-		.packScanline = packScanlineSBGGR12P,
-		.thumbScanline = thumbScanlineSBGGRxxP,
+		.packScanline = packScanlineRaw12_CSI2P,
+		.thumbScanline = thumbScanlineRaw_CSI2P,
 	} },
 	{ formats::SBGGR10_IPU3, {
 		.bitsPerSample = 16,
@@ -380,6 +514,8 @@
 	} },
 };
 
+} /* namespace */
+
 int DNGWriter::write(const char *filename, const Camera *camera,
 		     const StreamConfiguration &config,
 		     const ControlList &metadata,
@@ -521,6 +657,23 @@
 
 	TIFFWriteDirectory(tif);
 
+	/*
+	 * Workaround for a bug introduced in libtiff version 4.5.1 and no fix
+	 * released. In these versions the CFA* tags were missing in the field
+	 * info.
+	 * Introduced by: https://gitlab.com/libtiff/libtiff/-/commit/738e04099b13192bb1f654e74e9b5829313f3161
+	 * Fixed by: https://gitlab.com/libtiff/libtiff/-/commit/49856998c3d82e65444b47bb4fb11b7830a0c2be
+	 */
+	if (!TIFFFindField(tif, TIFFTAG_CFAREPEATPATTERNDIM, TIFF_ANY)) {
+		static const TIFFFieldInfo infos[] = {
+			{ TIFFTAG_CFAREPEATPATTERNDIM, 2, 2, TIFF_SHORT, FIELD_CUSTOM,
+			  1, 0, const_cast<char *>("CFARepeatPatternDim") },
+			{ TIFFTAG_CFAPATTERN, -1, -1, TIFF_BYTE, FIELD_CUSTOM,
+			  1, 1, const_cast<char *>("CFAPattern") },
+		};
+		TIFFMergeFieldInfo(tif, infos, 2);
+	}
+
 	/* Create a new IFD for the RAW image. */
 	const uint16_t cfaRepeatPatternDim[] = { 2, 2 };
 	const uint8_t cfaPlaneColor[] = {
diff --git a/src/apps/common/dng_writer.h b/src/apps/common/dng_writer.h
index 38f38f6..917713e 100644
--- a/src/apps/common/dng_writer.h
+++ b/src/apps/common/dng_writer.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * dng_writer.h - DNG writer
+ * DNG writer
  */
 
 #pragma once
diff --git a/src/apps/common/event_loop.cpp b/src/apps/common/event_loop.cpp
index cb83845..f7f9afa 100644
--- a/src/apps/common/event_loop.cpp
+++ b/src/apps/common/event_loop.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event_loop.cpp - cam - Event loop
+ * cam - Event loop
  */
 
 #include "event_loop.h"
diff --git a/src/apps/common/event_loop.h b/src/apps/common/event_loop.h
index ef79e8e..ef129b9 100644
--- a/src/apps/common/event_loop.h
+++ b/src/apps/common/event_loop.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event_loop.h - cam - Event loop
+ * cam - Event loop
  */
 
 #pragma once
diff --git a/src/apps/common/image.cpp b/src/apps/common/image.cpp
index fe2cc6d..a2a0f58 100644
--- a/src/apps/common/image.cpp
+++ b/src/apps/common/image.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * image.cpp - Multi-planar image with access to pixel data
+ * Multi-planar image with access to pixel data
  */
 
 #include "image.h"
diff --git a/src/apps/common/image.h b/src/apps/common/image.h
index 7953b17..e47e446 100644
--- a/src/apps/common/image.h
+++ b/src/apps/common/image.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * image.h - Multi-planar image with access to pixel data
+ * Multi-planar image with access to pixel data
  */
 
 #pragma once
diff --git a/src/apps/common/meson.build b/src/apps/common/meson.build
index 479326c..5b68339 100644
--- a/src/apps/common/meson.build
+++ b/src/apps/common/meson.build
@@ -3,6 +3,7 @@
 apps_sources = files([
     'image.cpp',
     'options.cpp',
+    'ppm_writer.cpp',
     'stream_options.cpp',
 ])
 
diff --git a/src/apps/common/options.cpp b/src/apps/common/options.cpp
index 4f7e869..ab19aa3 100644
--- a/src/apps/common/options.cpp
+++ b/src/apps/common/options.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * options.cpp - cam - Options parsing
+ * cam - Options parsing
  */
 
 #include <assert.h>
diff --git a/src/apps/common/options.h b/src/apps/common/options.h
index 4ddd498..9771aa7 100644
--- a/src/apps/common/options.h
+++ b/src/apps/common/options.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * options.h - cam - Options parsing
+ * cam - Options parsing
  */
 
 #pragma once
diff --git a/src/apps/common/ppm_writer.cpp b/src/apps/common/ppm_writer.cpp
new file mode 100644
index 0000000..d6c8641
--- /dev/null
+++ b/src/apps/common/ppm_writer.cpp
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * PPM writer
+ */
+
+#include "ppm_writer.h"
+
+#include <fstream>
+#include <iostream>
+
+#include <libcamera/formats.h>
+#include <libcamera/pixel_format.h>
+
+using namespace libcamera;
+
+int PPMWriter::write(const char *filename,
+		     const StreamConfiguration &config,
+		     const Span<uint8_t> &data)
+{
+	if (config.pixelFormat != formats::BGR888) {
+		std::cerr << "Only BGR888 output pixel format is supported ("
+			  << config.pixelFormat << " requested)" << std::endl;
+		return -EINVAL;
+	}
+
+	std::ofstream output(filename, std::ios::binary);
+	if (!output) {
+		std::cerr << "Failed to open ppm file: " << filename << std::endl;
+		return -EINVAL;
+	}
+
+	output << "P6" << std::endl
+	       << config.size.width << " " << config.size.height << std::endl
+	       << "255" << std::endl;
+	if (!output) {
+		std::cerr << "Failed to write the file header" << std::endl;
+		return -EINVAL;
+	}
+
+	const unsigned int rowLength = config.size.width * 3;
+	const char *row = reinterpret_cast<const char *>(data.data());
+	for (unsigned int y = 0; y < config.size.height; y++, row += config.stride) {
+		output.write(row, rowLength);
+		if (!output) {
+			std::cerr << "Failed to write image data at row " << y << std::endl;
+			return -EINVAL;
+		}
+	}
+
+	return 0;
+}
diff --git a/src/apps/common/ppm_writer.h b/src/apps/common/ppm_writer.h
new file mode 100644
index 0000000..8c8d2e1
--- /dev/null
+++ b/src/apps/common/ppm_writer.h
@@ -0,0 +1,20 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat, Inc.
+ *
+ * PPM writer
+ */
+
+#pragma once
+
+#include <libcamera/base/span.h>
+
+#include <libcamera/stream.h>
+
+class PPMWriter
+{
+public:
+	static int write(const char *filename,
+			 const libcamera::StreamConfiguration &config,
+			 const libcamera::Span<uint8_t> &data);
+};
diff --git a/src/apps/common/stream_options.cpp b/src/apps/common/stream_options.cpp
index 663b979..99239e0 100644
--- a/src/apps/common/stream_options.cpp
+++ b/src/apps/common/stream_options.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * stream_options.cpp - Helper to parse options for streams
+ * Helper to parse options for streams
  */
 #include "stream_options.h"
 
diff --git a/src/apps/common/stream_options.h b/src/apps/common/stream_options.h
index a5f3bde..a93f104 100644
--- a/src/apps/common/stream_options.h
+++ b/src/apps/common/stream_options.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * stream_options.h - Helper to parse options for streams
+ * Helper to parse options for streams
  */
 
 #pragma once
diff --git a/src/apps/ipa-verify/main.cpp b/src/apps/ipa-verify/main.cpp
index 76ba507..0903cd8 100644
--- a/src/apps/ipa-verify/main.cpp
+++ b/src/apps/ipa-verify/main.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2023, Ideas on Board Oy
  *
- * ipa_verify.cpp - Verify signature on an IPA module
+ * Verify signature on an IPA module
  */
 
 #include <iostream>
diff --git a/src/apps/lc-compliance/environment.cpp b/src/apps/lc-compliance/environment.cpp
index 5eb3775..987264f 100644
--- a/src/apps/lc-compliance/environment.cpp
+++ b/src/apps/lc-compliance/environment.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Collabora Ltd.
  *
- * environment.cpp - Common environment for tests
+ * Common environment for tests
  */
 
 #include "environment.h"
diff --git a/src/apps/lc-compliance/environment.h b/src/apps/lc-compliance/environment.h
index 0debbcc..543e537 100644
--- a/src/apps/lc-compliance/environment.h
+++ b/src/apps/lc-compliance/environment.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Collabora Ltd.
  *
- * environment.h - Common environment for tests
+ * Common environment for tests
  */
 
 #pragma once
diff --git a/src/apps/lc-compliance/simple_capture.cpp b/src/apps/lc-compliance/helpers/capture.cpp
similarity index 75%
rename from src/apps/lc-compliance/simple_capture.cpp
rename to src/apps/lc-compliance/helpers/capture.cpp
index cf4d7cf..90c1530 100644
--- a/src/apps/lc-compliance/simple_capture.cpp
+++ b/src/apps/lc-compliance/helpers/capture.cpp
@@ -2,27 +2,27 @@
 /*
  * Copyright (C) 2020-2021, Google Inc.
  *
- * simple_capture.cpp - Simple capture helper
+ * Simple capture helper
  */
 
+#include "capture.h"
+
 #include <gtest/gtest.h>
 
-#include "simple_capture.h"
-
 using namespace libcamera;
 
-SimpleCapture::SimpleCapture(std::shared_ptr<Camera> camera)
+Capture::Capture(std::shared_ptr<Camera> camera)
 	: loop_(nullptr), camera_(camera),
 	  allocator_(std::make_unique<FrameBufferAllocator>(camera))
 {
 }
 
-SimpleCapture::~SimpleCapture()
+Capture::~Capture()
 {
 	stop();
 }
 
-void SimpleCapture::configure(StreamRole role)
+void Capture::configure(StreamRole role)
 {
 	config_ = camera_->generateConfiguration({ role });
 
@@ -42,7 +42,7 @@
 	}
 }
 
-void SimpleCapture::start()
+void Capture::start()
 {
 	Stream *stream = config_->at(0).stream();
 	int count = allocator_->allocate(stream);
@@ -50,12 +50,12 @@
 	ASSERT_GE(count, 0) << "Failed to allocate buffers";
 	EXPECT_EQ(count, config_->at(0).bufferCount) << "Allocated less buffers than expected";
 
-	camera_->requestCompleted.connect(this, &SimpleCapture::requestComplete);
+	camera_->requestCompleted.connect(this, &Capture::requestComplete);
 
 	ASSERT_EQ(camera_->start(), 0) << "Failed to start camera";
 }
 
-void SimpleCapture::stop()
+void Capture::stop()
 {
 	if (!config_ || !allocator_->allocated())
 		return;
@@ -69,14 +69,14 @@
 	allocator_->free(stream);
 }
 
-/* SimpleCaptureBalanced */
+/* CaptureBalanced */
 
-SimpleCaptureBalanced::SimpleCaptureBalanced(std::shared_ptr<Camera> camera)
-	: SimpleCapture(camera)
+CaptureBalanced::CaptureBalanced(std::shared_ptr<Camera> camera)
+	: Capture(camera)
 {
 }
 
-void SimpleCaptureBalanced::capture(unsigned int numRequests)
+void CaptureBalanced::capture(unsigned int numRequests)
 {
 	start();
 
@@ -95,7 +95,7 @@
 	captureCount_ = 0;
 	captureLimit_ = numRequests;
 
-	/* Queue the recommended number of reqeuests. */
+	/* Queue the recommended number of requests. */
 	for (const std::unique_ptr<FrameBuffer> &buffer : buffers) {
 		std::unique_ptr<Request> request = camera_->createRequest();
 		ASSERT_TRUE(request) << "Can't create request";
@@ -116,7 +116,7 @@
 	ASSERT_EQ(captureCount_, captureLimit_);
 }
 
-int SimpleCaptureBalanced::queueRequest(Request *request)
+int CaptureBalanced::queueRequest(Request *request)
 {
 	queueCount_++;
 	if (queueCount_ > captureLimit_)
@@ -125,8 +125,11 @@
 	return camera_->queueRequest(request);
 }
 
-void SimpleCaptureBalanced::requestComplete(Request *request)
+void CaptureBalanced::requestComplete(Request *request)
 {
+	EXPECT_EQ(request->status(), Request::Status::RequestComplete)
+		<< "Request didn't complete successfully";
+
 	captureCount_++;
 	if (captureCount_ >= captureLimit_) {
 		loop_->exit(0);
@@ -138,14 +141,14 @@
 		loop_->exit(-EINVAL);
 }
 
-/* SimpleCaptureUnbalanced */
+/* CaptureUnbalanced */
 
-SimpleCaptureUnbalanced::SimpleCaptureUnbalanced(std::shared_ptr<Camera> camera)
-	: SimpleCapture(camera)
+CaptureUnbalanced::CaptureUnbalanced(std::shared_ptr<Camera> camera)
+	: Capture(camera)
 {
 }
 
-void SimpleCaptureUnbalanced::capture(unsigned int numRequests)
+void CaptureUnbalanced::capture(unsigned int numRequests)
 {
 	start();
 
@@ -155,7 +158,7 @@
 	captureCount_ = 0;
 	captureLimit_ = numRequests;
 
-	/* Queue the recommended number of reqeuests. */
+	/* Queue the recommended number of requests. */
 	for (const std::unique_ptr<FrameBuffer> &buffer : buffers) {
 		std::unique_ptr<Request> request = camera_->createRequest();
 		ASSERT_TRUE(request) << "Can't create request";
@@ -176,7 +179,7 @@
 	ASSERT_EQ(status, 0);
 }
 
-void SimpleCaptureUnbalanced::requestComplete(Request *request)
+void CaptureUnbalanced::requestComplete(Request *request)
 {
 	captureCount_++;
 	if (captureCount_ >= captureLimit_) {
@@ -184,6 +187,9 @@
 		return;
 	}
 
+	EXPECT_EQ(request->status(), Request::Status::RequestComplete)
+		<< "Request didn't complete successfully";
+
 	request->reuse(Request::ReuseBuffers);
 	if (camera_->queueRequest(request))
 		loop_->exit(-EINVAL);
diff --git a/src/apps/lc-compliance/simple_capture.h b/src/apps/lc-compliance/helpers/capture.h
similarity index 72%
rename from src/apps/lc-compliance/simple_capture.h
rename to src/apps/lc-compliance/helpers/capture.h
index 2911d60..19b6927 100644
--- a/src/apps/lc-compliance/simple_capture.h
+++ b/src/apps/lc-compliance/helpers/capture.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020-2021, Google Inc.
  *
- * simple_capture.h - Simple capture helper
+ * Simple capture helper
  */
 
 #pragma once
@@ -13,14 +13,14 @@
 
 #include "../common/event_loop.h"
 
-class SimpleCapture
+class Capture
 {
 public:
 	void configure(libcamera::StreamRole role);
 
 protected:
-	SimpleCapture(std::shared_ptr<libcamera::Camera> camera);
-	virtual ~SimpleCapture();
+	Capture(std::shared_ptr<libcamera::Camera> camera);
+	virtual ~Capture();
 
 	void start();
 	void stop();
@@ -35,10 +35,10 @@
 	std::vector<std::unique_ptr<libcamera::Request>> requests_;
 };
 
-class SimpleCaptureBalanced : public SimpleCapture
+class CaptureBalanced : public Capture
 {
 public:
-	SimpleCaptureBalanced(std::shared_ptr<libcamera::Camera> camera);
+	CaptureBalanced(std::shared_ptr<libcamera::Camera> camera);
 
 	void capture(unsigned int numRequests);
 
@@ -51,10 +51,10 @@
 	unsigned int captureLimit_;
 };
 
-class SimpleCaptureUnbalanced : public SimpleCapture
+class CaptureUnbalanced : public Capture
 {
 public:
-	SimpleCaptureUnbalanced(std::shared_ptr<libcamera::Camera> camera);
+	CaptureUnbalanced(std::shared_ptr<libcamera::Camera> camera);
 
 	void capture(unsigned int numRequests);
 
diff --git a/src/apps/lc-compliance/main.cpp b/src/apps/lc-compliance/main.cpp
index 74e0d4d..3f1d2a6 100644
--- a/src/apps/lc-compliance/main.cpp
+++ b/src/apps/lc-compliance/main.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Google Inc.
  * Copyright (C) 2021, Collabora Ltd.
  *
- * main.cpp - lc-compliance - The libcamera compliance tool
+ * lc-compliance - The libcamera compliance tool
  */
 
 #include <iomanip>
diff --git a/src/apps/lc-compliance/meson.build b/src/apps/lc-compliance/meson.build
index 51d9075..b1f605f 100644
--- a/src/apps/lc-compliance/meson.build
+++ b/src/apps/lc-compliance/meson.build
@@ -1,6 +1,7 @@
 # SPDX-License-Identifier: CC0-1.0
 
-libgtest = dependency('gtest', required : get_option('lc-compliance'),
+libgtest = dependency('gtest', version : '>=1.10.0',
+                      required : get_option('lc-compliance'),
                       fallback : ['gtest', 'gtest_dep'])
 
 if opt_lc_compliance.disabled() or not libevent.found() or not libgtest.found()
@@ -12,9 +13,14 @@
 
 lc_compliance_sources = files([
     'environment.cpp',
+    'helpers/capture.cpp',
     'main.cpp',
-    'simple_capture.cpp',
-    'capture_test.cpp',
+    'tests/capture_test.cpp',
+])
+
+lc_compliance_includes = ([
+    include_directories('.'),
+    include_directories('helpers/')
 ])
 
 lc_compliance  = executable('lc-compliance', lc_compliance_sources,
@@ -26,4 +32,6 @@
                                 libevent,
                                 libgtest,
                             ],
-                            install : true)
+                            include_directories : lc_compliance_includes,
+                            install : true,
+                            install_tag : 'bin-devel')
diff --git a/src/apps/lc-compliance/capture_test.cpp b/src/apps/lc-compliance/tests/capture_test.cpp
similarity index 93%
rename from src/apps/lc-compliance/capture_test.cpp
rename to src/apps/lc-compliance/tests/capture_test.cpp
index 1dcfcf9..ad3a1da 100644
--- a/src/apps/lc-compliance/capture_test.cpp
+++ b/src/apps/lc-compliance/tests/capture_test.cpp
@@ -3,15 +3,16 @@
  * Copyright (C) 2020, Google Inc.
  * Copyright (C) 2021, Collabora Ltd.
  *
- * capture_test.cpp - Test camera capture
+ * Test camera capture
  */
 
+#include "capture.h"
+
 #include <iostream>
 
 #include <gtest/gtest.h>
 
 #include "environment.h"
-#include "simple_capture.h"
 
 using namespace libcamera;
 
@@ -83,7 +84,7 @@
 {
 	auto [role, numRequests] = GetParam();
 
-	SimpleCaptureBalanced capture(camera_);
+	CaptureBalanced capture(camera_);
 
 	capture.configure(role);
 
@@ -102,7 +103,7 @@
 	auto [role, numRequests] = GetParam();
 	unsigned int numRepeats = 3;
 
-	SimpleCaptureBalanced capture(camera_);
+	CaptureBalanced capture(camera_);
 
 	capture.configure(role);
 
@@ -121,7 +122,7 @@
 {
 	auto [role, numRequests] = GetParam();
 
-	SimpleCaptureUnbalanced capture(camera_);
+	CaptureUnbalanced capture(camera_);
 
 	capture.configure(role);
 
diff --git a/src/apps/qcam/cam_select_dialog.cpp b/src/apps/qcam/cam_select_dialog.cpp
index 3c8b12a..c51f597 100644
--- a/src/apps/qcam/cam_select_dialog.cpp
+++ b/src/apps/qcam/cam_select_dialog.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Utkarsh Tiwari <utkarsh02t@gmail.com>
  *
- * cam_select_dialog.cpp - qcam - Camera Selection dialog
+ * qcam - Camera Selection dialog
  */
 
 #include "cam_select_dialog.h"
diff --git a/src/apps/qcam/cam_select_dialog.h b/src/apps/qcam/cam_select_dialog.h
index 0b7709e..4bec9ea 100644
--- a/src/apps/qcam/cam_select_dialog.h
+++ b/src/apps/qcam/cam_select_dialog.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Utkarsh Tiwari <utkarsh02t@gmail.com>
  *
- * cam_select_dialog.h - qcam - Camera Selection dialog
+ * qcam - Camera Selection dialog
  */
 
 #pragma once
diff --git a/src/apps/qcam/format_converter.cpp b/src/apps/qcam/format_converter.cpp
index de76b32..3212349 100644
--- a/src/apps/qcam/format_converter.cpp
+++ b/src/apps/qcam/format_converter.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * format_convert.cpp - qcam - Convert buffer to RGB
+ * Convert buffer to RGB
  */
 
 #include "format_converter.h"
diff --git a/src/apps/qcam/format_converter.h b/src/apps/qcam/format_converter.h
index 37dbfae..819a87a 100644
--- a/src/apps/qcam/format_converter.h
+++ b/src/apps/qcam/format_converter.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * format_convert.h - qcam - Convert buffer to RGB
+ * Convert buffer to RGB
  */
 
 #pragma once
diff --git a/src/apps/qcam/main.cpp b/src/apps/qcam/main.cpp
index 36cb93a..d0bde14 100644
--- a/src/apps/qcam/main.cpp
+++ b/src/apps/qcam/main.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * main.cpp - qcam - The libcamera GUI test application
+ * qcam - The libcamera GUI test application
  */
 
 #include <signal.h>
@@ -21,6 +21,8 @@
 
 using namespace libcamera;
 
+namespace {
+
 void signalHandler([[maybe_unused]] int signal)
 {
 	qInfo() << "Exiting";
@@ -52,6 +54,8 @@
 	return options;
 }
 
+} /* namespace */
+
 int main(int argc, char **argv)
 {
 	QApplication app(argc, argv);
diff --git a/src/apps/qcam/main_window.cpp b/src/apps/qcam/main_window.cpp
index 0f16c03..d515bee 100644
--- a/src/apps/qcam/main_window.cpp
+++ b/src/apps/qcam/main_window.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * main_window.cpp - qcam - Main application window
+ * qcam - Main application window
  */
 
 #include "main_window.h"
@@ -190,7 +190,7 @@
 	action = toolbar_->addAction(QIcon::fromTheme("application-exit",
 						      QIcon(":x-circle.svg")),
 				     "Quit");
-	action->setShortcut(Qt::CTRL | Qt::Key_Q);
+	action->setShortcut(QKeySequence::Quit);
 	connect(action, &QAction::triggered, this, &MainWindow::quit);
 
 	/* Camera selector. */
diff --git a/src/apps/qcam/main_window.h b/src/apps/qcam/main_window.h
index 2e3e1b5..4cead73 100644
--- a/src/apps/qcam/main_window.h
+++ b/src/apps/qcam/main_window.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * main_window.h - qcam - Main application window
+ * qcam - Main application window
  */
 
 #pragma once
diff --git a/src/apps/qcam/meson.build b/src/apps/qcam/meson.build
index 2e77146..6cf4c17 100644
--- a/src/apps/qcam/meson.build
+++ b/src/apps/qcam/meson.build
@@ -63,6 +63,7 @@
 
 qcam  = executable('qcam', qcam_sources, resources,
                    install : true,
+                   install_tag : 'bin',
                    link_with : apps_lib,
                    dependencies : [
                        libatomic,
diff --git a/src/apps/qcam/message_handler.cpp b/src/apps/qcam/message_handler.cpp
index 261623e..c89714a 100644
--- a/src/apps/qcam/message_handler.cpp
+++ b/src/apps/qcam/message_handler.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Laurent Pinchart <laurent.pinchart@ideasonboard.com>
  *
- * message_handler.cpp - qcam - Log message handling
+ * qcam - Log message handling
  */
 
 #include "message_handler.h"
diff --git a/src/apps/qcam/message_handler.h b/src/apps/qcam/message_handler.h
index 56294d3..92ef74d 100644
--- a/src/apps/qcam/message_handler.h
+++ b/src/apps/qcam/message_handler.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Laurent Pinchart <laurent.pinchart@ideasonboard.com>
  *
- * message_handler.cpp - qcam - Log message handling
+ * Log message handling
  */
 
 #pragma once
diff --git a/src/apps/qcam/viewfinder.h b/src/apps/qcam/viewfinder.h
index a57446e..914f88e 100644
--- a/src/apps/qcam/viewfinder.h
+++ b/src/apps/qcam/viewfinder.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * viewfinder.h - qcam - Viewfinder base class
+ * qcam - Viewfinder base class
  */
 
 #pragma once
diff --git a/src/apps/qcam/viewfinder_gl.cpp b/src/apps/qcam/viewfinder_gl.cpp
index f83b99a..9d2a696 100644
--- a/src/apps/qcam/viewfinder_gl.cpp
+++ b/src/apps/qcam/viewfinder_gl.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Linaro
  *
- * viewfinderGL.cpp - OpenGL Viewfinder for rendering by OpenGL shader
+ * OpenGL Viewfinder for rendering by OpenGL shader
  */
 
 #include "viewfinder_gl.h"
diff --git a/src/apps/qcam/viewfinder_gl.h b/src/apps/qcam/viewfinder_gl.h
index 68c2912..23744b4 100644
--- a/src/apps/qcam/viewfinder_gl.h
+++ b/src/apps/qcam/viewfinder_gl.h
@@ -2,8 +2,7 @@
 /*
  * Copyright (C) 2020, Linaro
  *
- * viewfinder_GL.h - OpenGL Viewfinder for rendering by OpenGL shader
- *
+ * OpenGL Viewfinder for rendering by OpenGL shader
  */
 
 #pragma once
diff --git a/src/apps/qcam/viewfinder_qt.cpp b/src/apps/qcam/viewfinder_qt.cpp
index 597ccff..4821c27 100644
--- a/src/apps/qcam/viewfinder_qt.cpp
+++ b/src/apps/qcam/viewfinder_qt.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * viewfinder_qt.cpp - qcam - QPainter-based ViewFinder
+ * qcam - QPainter-based ViewFinder
  */
 
 #include "viewfinder_qt.h"
@@ -36,6 +36,7 @@
 	{ libcamera::formats::RGB888, QImage::Format_BGR888 },
 #endif
 	{ libcamera::formats::BGR888, QImage::Format_RGB888 },
+	{ libcamera::formats::RGB565, QImage::Format_RGB16 },
 };
 
 ViewFinderQt::ViewFinderQt(QWidget *parent)
diff --git a/src/apps/qcam/viewfinder_qt.h b/src/apps/qcam/viewfinder_qt.h
index eb3a998..4f4b9f1 100644
--- a/src/apps/qcam/viewfinder_qt.h
+++ b/src/apps/qcam/viewfinder_qt.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * viewfinder_qt.h - qcam - QPainter-based ViewFinder
+ * qcam - QPainter-based ViewFinder
  */
 
 #pragma once
diff --git a/src/gstreamer/gstlibcamera-utils.cpp b/src/gstreamer/gstlibcamera-utils.cpp
index 750ec35..ec4da43 100644
--- a/src/gstreamer/gstlibcamera-utils.cpp
+++ b/src/gstreamer/gstlibcamera-utils.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcamera-utils.c - GStreamer libcamera Utility Function
+ * GStreamer libcamera Utility Function
  */
 
 #include "gstlibcamera-utils.h"
@@ -25,6 +25,26 @@
 	{ GST_VIDEO_FORMAT_ENCODED, formats::SGBRG8 },
 	{ GST_VIDEO_FORMAT_ENCODED, formats::SGRBG8 },
 	{ GST_VIDEO_FORMAT_ENCODED, formats::SRGGB8 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SBGGR10 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SGBRG10 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SGRBG10 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SRGGB10 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SBGGR12 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SGBRG12 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SGRBG12 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SRGGB12 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SBGGR14 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SGBRG14 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SGRBG14 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SRGGB14 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SBGGR16 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SGBRG16 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SGRBG16 },
+	{ GST_VIDEO_FORMAT_ENCODED, formats::SRGGB16 },
+
+	/* Monochrome */
+	{ GST_VIDEO_FORMAT_GRAY8, formats::R8 },
+	{ GST_VIDEO_FORMAT_GRAY16_LE, formats::R16 },
 
 	/* RGB16 */
 	{ GST_VIDEO_FORMAT_RGB16, formats::RGB565 },
@@ -246,6 +266,38 @@
 		return "grbg";
 	case formats::SRGGB8:
 		return "rggb";
+	case formats::SBGGR10:
+		return "bggr10le";
+	case formats::SGBRG10:
+		return "gbrg10le";
+	case formats::SGRBG10:
+		return "grbg10le";
+	case formats::SRGGB10:
+		return "rggb10le";
+	case formats::SBGGR12:
+		return "bggr12le";
+	case formats::SGBRG12:
+		return "gbrg12le";
+	case formats::SGRBG12:
+		return "grbg12le";
+	case formats::SRGGB12:
+		return "rggb12le";
+	case formats::SBGGR14:
+		return "bggr14le";
+	case formats::SGBRG14:
+		return "gbrg14le";
+	case formats::SGRBG14:
+		return "grbg14le";
+	case formats::SRGGB14:
+		return "rggb14le";
+	case formats::SBGGR16:
+		return "bggr16le";
+	case formats::SGBRG16:
+		return "gbrg16le";
+	case formats::SGRBG16:
+		return "grbg16le";
+	case formats::SRGGB16:
+		return "rggb16le";
 	}
 	return NULL;
 }
@@ -333,7 +385,7 @@
 
 	if (stream_cfg.colorSpace) {
 		GstVideoColorimetry colorimetry = colorimetry_from_colorspace(stream_cfg.colorSpace.value());
-		gchar *colorimetry_str = gst_video_colorimetry_to_string(&colorimetry);
+		g_autofree gchar *colorimetry_str = gst_video_colorimetry_to_string(&colorimetry);
 
 		if (colorimetry_str)
 			gst_structure_set(s, "colorimetry", G_TYPE_STRING, colorimetry_str, nullptr);
diff --git a/src/gstreamer/gstlibcamera-utils.h b/src/gstreamer/gstlibcamera-utils.h
index fd304a8..cab1c81 100644
--- a/src/gstreamer/gstlibcamera-utils.h
+++ b/src/gstreamer/gstlibcamera-utils.h
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcamera-utils.h - GStreamer libcamera Utility Functions
+ * GStreamer libcamera Utility Functions
  */
 
 #pragma once
@@ -25,6 +25,13 @@
 					       GstStructure *element_caps);
 void gst_libcamera_framerate_to_caps(GstCaps *caps, const GstStructure *element_caps);
 
+#if !GST_CHECK_VERSION(1, 16, 0)
+static inline void gst_clear_event(GstEvent **event_ptr)
+{
+	g_clear_pointer(event_ptr, gst_mini_object_unref);
+}
+#endif
+
 #if !GST_CHECK_VERSION(1, 17, 1)
 gboolean gst_task_resume(GstTask *task);
 #endif
diff --git a/src/gstreamer/gstlibcamera.cpp b/src/gstreamer/gstlibcamera.cpp
index 52388b5..bff9897 100644
--- a/src/gstreamer/gstlibcamera.cpp
+++ b/src/gstreamer/gstlibcamera.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2019, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcamera.c - GStreamer plugin
+ * GStreamer plugin
  */
 
 #include "gstlibcameraprovider.h"
diff --git a/src/gstreamer/gstlibcameraallocator.cpp b/src/gstreamer/gstlibcameraallocator.cpp
index c740b8f..741ed59 100644
--- a/src/gstreamer/gstlibcameraallocator.cpp
+++ b/src/gstreamer/gstlibcameraallocator.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcameraallocator.cpp - GStreamer Custom Allocator
+ * GStreamer Custom Allocator
  */
 
 #include "gstlibcameraallocator.h"
diff --git a/src/gstreamer/gstlibcameraallocator.h b/src/gstreamer/gstlibcameraallocator.h
index 0a08c3b..1a6ba34 100644
--- a/src/gstreamer/gstlibcameraallocator.h
+++ b/src/gstreamer/gstlibcameraallocator.h
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcameraallocator.h - GStreamer Custom Allocator
+ * GStreamer Custom Allocator
  */
 
 #pragma once
diff --git a/src/gstreamer/gstlibcamerapad.cpp b/src/gstreamer/gstlibcamerapad.cpp
index 9e710a4..7b22aeb 100644
--- a/src/gstreamer/gstlibcamerapad.cpp
+++ b/src/gstreamer/gstlibcamerapad.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2019, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcamerapad.cpp - GStreamer Capture Pad
+ * GStreamer Capture Pad
  */
 
 #include "gstlibcamerapad.h"
diff --git a/src/gstreamer/gstlibcamerapad.h b/src/gstreamer/gstlibcamerapad.h
index 103ee57..630c168 100644
--- a/src/gstreamer/gstlibcamerapad.h
+++ b/src/gstreamer/gstlibcamerapad.h
@@ -3,7 +3,7 @@
  * Copyright (C) 2019, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcamerapad.h - GStreamer Capture Element
+ * GStreamer Capture Element
  */
 
 #pragma once
diff --git a/src/gstreamer/gstlibcamerapool.cpp b/src/gstreamer/gstlibcamerapool.cpp
index 0c2be43..9661c67 100644
--- a/src/gstreamer/gstlibcamerapool.cpp
+++ b/src/gstreamer/gstlibcamerapool.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcamerapool.cpp - GStreamer Buffer Pool
+ * GStreamer Buffer Pool
  */
 
 #include "gstlibcamerapool.h"
diff --git a/src/gstreamer/gstlibcamerapool.h b/src/gstreamer/gstlibcamerapool.h
index ce3bf60..2a7a9c7 100644
--- a/src/gstreamer/gstlibcamerapool.h
+++ b/src/gstreamer/gstlibcamerapool.h
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcamerapool.h - GStreamer Buffer Pool
+ * GStreamer Buffer Pool
  *
  * This is a partial implementation of GstBufferPool intended for internal use
  * only. This pool cannot be configured or activated.
diff --git a/src/gstreamer/gstlibcameraprovider.cpp b/src/gstreamer/gstlibcameraprovider.cpp
index ce3e0a0..4fb1b00 100644
--- a/src/gstreamer/gstlibcameraprovider.cpp
+++ b/src/gstreamer/gstlibcameraprovider.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcameraprovider.c - GStreamer Device Provider
+ * GStreamer Device Provider
  */
 
 #include <array>
diff --git a/src/gstreamer/gstlibcameraprovider.h b/src/gstreamer/gstlibcameraprovider.h
index aaceabf..19708b9 100644
--- a/src/gstreamer/gstlibcameraprovider.h
+++ b/src/gstreamer/gstlibcameraprovider.h
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcameraprovider.h - GStreamer Device Provider
+ * GStreamer Device Provider
  */
 
 #pragma once
diff --git a/src/gstreamer/gstlibcamerasrc.cpp b/src/gstreamer/gstlibcamerasrc.cpp
index 63d9957..9680d80 100644
--- a/src/gstreamer/gstlibcamerasrc.cpp
+++ b/src/gstreamer/gstlibcamerasrc.cpp
@@ -3,16 +3,14 @@
  * Copyright (C) 2019, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcamerasrc.cpp - GStreamer Capture Element
+ * GStreamer Capture Element
  */
 
 /**
  * \todo The following is a list of items that needs implementation in the GStreamer plugin
  *  - Implement GstElement::send_event
- *    + Allowing application to send EOS
  *    + Allowing application to use FLUSH/FLUSH_STOP
  *    + Prevent the main thread from accessing streaming thread
- *  - Implement renegotiation (even if slow)
  *  - Implement GstElement::request-new-pad (multi stream)
  *    + Evaluate if a single streaming thread is fine
  *  - Add application driven request (snapshot)
@@ -29,6 +27,7 @@
 
 #include "gstlibcamerasrc.h"
 
+#include <atomic>
 #include <queue>
 #include <vector>
 
@@ -133,6 +132,7 @@
 	int queueRequest();
 	void requestCompleted(Request *request);
 	int processRequest();
+	void clearRequests();
 };
 
 struct _GstLibcameraSrc {
@@ -144,6 +144,8 @@
 	gchar *camera_name;
 	controls::AfModeEnum auto_focus_mode = controls::AfModeManual;
 
+	std::atomic<GstEvent *> pending_eos;
+
 	GstLibcameraSrcState *state;
 	GstLibcameraAllocator *allocator;
 	GstFlowCombiner *flow_combiner;
@@ -299,23 +301,57 @@
 							srcpad, ret);
 	}
 
-	if (ret != GST_FLOW_OK) {
-		if (ret == GST_FLOW_EOS) {
-			g_autoptr(GstEvent) eos = gst_event_new_eos();
-			guint32 seqnum = gst_util_seqnum_next();
-			gst_event_set_seqnum(eos, seqnum);
-			for (GstPad *srcpad : srcpads_)
-				gst_pad_push_event(srcpad, gst_event_ref(eos));
-		} else if (ret != GST_FLOW_FLUSHING) {
-			GST_ELEMENT_FLOW_ERROR(src_, ret);
+	switch (ret) {
+	case GST_FLOW_OK:
+		break;
+
+	case GST_FLOW_NOT_NEGOTIATED: {
+		bool reconfigure = false;
+		for (GstPad *srcpad : srcpads_) {
+			if (gst_pad_needs_reconfigure(srcpad)) {
+				reconfigure = true;
+				break;
+			}
 		}
 
-		return -EPIPE;
+		/* If no pads need a reconfiguration something went wrong. */
+		if (!reconfigure)
+			err = -EPIPE;
+
+		break;
+	}
+
+	case GST_FLOW_EOS: {
+		g_autoptr(GstEvent) eos = gst_event_new_eos();
+		guint32 seqnum = gst_util_seqnum_next();
+		gst_event_set_seqnum(eos, seqnum);
+		for (GstPad *srcpad : srcpads_)
+			gst_pad_push_event(srcpad, gst_event_ref(eos));
+
+		err = -EPIPE;
+		break;
+	}
+
+	case GST_FLOW_FLUSHING:
+		err = -EPIPE;
+		break;
+
+	default:
+		GST_ELEMENT_FLOW_ERROR(src_, ret);
+
+		err = -EPIPE;
+		break;
 	}
 
 	return err;
 }
 
+void GstLibcameraSrcState::clearRequests()
+{
+	GLibLocker locker(&lock_);
+	completedRequests_ = {};
+}
+
 static bool
 gst_libcamera_src_open(GstLibcameraSrc *self)
 {
@@ -341,21 +377,22 @@
 	}
 
 	if (camera_name) {
-		cam = cm->get(self->camera_name);
+		cam = cm->get(camera_name);
 		if (!cam) {
 			GST_ELEMENT_ERROR(self, RESOURCE, NOT_FOUND,
-					  ("Could not find a camera named '%s'.", self->camera_name),
+					  ("Could not find a camera named '%s'.", camera_name),
 					  ("libcamera::CameraMananger::get() returned nullptr"));
 			return false;
 		}
 	} else {
-		if (cm->cameras().empty()) {
+		auto cameras = cm->cameras();
+		if (cameras.empty()) {
 			GST_ELEMENT_ERROR(self, RESOURCE, NOT_FOUND,
 					  ("Could not find any supported camera on this system."),
 					  ("libcamera::CameraMananger::cameras() is empty"));
 			return false;
 		}
-		cam = cm->cameras()[0];
+		cam = cameras[0];
 	}
 
 	GST_INFO_OBJECT(self, "Using camera '%s'", cam->id().c_str());
@@ -377,6 +414,90 @@
 	return true;
 }
 
+/* Must be called with stream_lock held. */
+static bool
+gst_libcamera_src_negotiate(GstLibcameraSrc *self)
+{
+	GstLibcameraSrcState *state = self->state;
+
+	g_autoptr(GstStructure) element_caps = gst_structure_new_empty("caps");
+
+	for (gsize i = 0; i < state->srcpads_.size(); i++) {
+		GstPad *srcpad = state->srcpads_[i];
+		StreamConfiguration &stream_cfg = state->config_->at(i);
+
+		/* Retrieve the supported caps. */
+		g_autoptr(GstCaps) filter = gst_libcamera_stream_formats_to_caps(stream_cfg.formats());
+		g_autoptr(GstCaps) caps = gst_pad_peer_query_caps(srcpad, filter);
+		if (gst_caps_is_empty(caps))
+			return false;
+
+		/* Fixate caps and configure the stream. */
+		caps = gst_caps_make_writable(caps);
+		gst_libcamera_configure_stream_from_caps(stream_cfg, caps);
+		gst_libcamera_get_framerate_from_caps(caps, element_caps);
+	}
+
+	/* Validate the configuration. */
+	if (state->config_->validate() == CameraConfiguration::Invalid)
+		return false;
+
+	int ret = state->cam_->configure(state->config_.get());
+	if (ret) {
+		GST_ELEMENT_ERROR(self, RESOURCE, SETTINGS,
+				  ("Failed to configure camera: %s", g_strerror(-ret)),
+				  ("Camera::configure() failed with error code %i", ret));
+		return false;
+	}
+
+	/* Check frame duration bounds within controls::FrameDurationLimits */
+	gst_libcamera_clamp_and_set_frameduration(state->initControls_,
+						  state->cam_->controls(), element_caps);
+
+	/*
+	 * Regardless if it has been modified, create clean caps and push the
+	 * caps event. Downstream will decide if the caps are acceptable.
+	 */
+	for (gsize i = 0; i < state->srcpads_.size(); i++) {
+		GstPad *srcpad = state->srcpads_[i];
+		const StreamConfiguration &stream_cfg = state->config_->at(i);
+
+		g_autoptr(GstCaps) caps = gst_libcamera_stream_configuration_to_caps(stream_cfg);
+		gst_libcamera_framerate_to_caps(caps, element_caps);
+
+		if (!gst_pad_push_event(srcpad, gst_event_new_caps(caps)))
+			return false;
+	}
+
+	if (self->allocator)
+		g_clear_object(&self->allocator);
+
+	self->allocator = gst_libcamera_allocator_new(state->cam_, state->config_.get());
+	if (!self->allocator) {
+		GST_ELEMENT_ERROR(self, RESOURCE, NO_SPACE_LEFT,
+				  ("Failed to allocate memory"),
+				  ("gst_libcamera_allocator_new() failed."));
+		return false;
+	}
+
+	for (gsize i = 0; i < state->srcpads_.size(); i++) {
+		GstPad *srcpad = state->srcpads_[i];
+		const StreamConfiguration &stream_cfg = state->config_->at(i);
+
+		GstLibcameraPool *pool = gst_libcamera_pool_new(self->allocator,
+								stream_cfg.stream());
+		g_signal_connect_swapped(pool, "buffer-notify",
+					 G_CALLBACK(gst_task_resume), self->task);
+
+		gst_libcamera_pad_set_pool(srcpad, pool);
+
+		/* Clear all reconfigure flags. */
+		gst_pad_check_reconfigure(srcpad);
+	}
+
+	return true;
+}
+
 static void
 gst_libcamera_src_task_run(gpointer user_data)
 {
@@ -397,6 +518,39 @@
 
 	bool doResume = false;
 
+	g_autoptr(GstEvent) event = self->pending_eos.exchange(nullptr);
+	if (event) {
+		for (GstPad *srcpad : state->srcpads_)
+			gst_pad_push_event(srcpad, gst_event_ref(event));
+
+		return;
+	}
+
+	/* Check if a srcpad requested a renegotiation. */
+	bool reconfigure = false;
+	for (GstPad *srcpad : state->srcpads_) {
+		if (gst_pad_check_reconfigure(srcpad)) {
+			/* Check if the caps even need changing. */
+			g_autoptr(GstCaps) caps = gst_pad_get_current_caps(srcpad);
+			if (!gst_pad_peer_query_accept_caps(srcpad, caps)) {
+				reconfigure = true;
+				break;
+			}
+		}
+	}
+
+	if (reconfigure) {
+		state->cam_->stop();
+		state->clearRequests();
+
+		if (!gst_libcamera_src_negotiate(self)) {
+			GST_ELEMENT_FLOW_ERROR(self, GST_FLOW_NOT_NEGOTIATED);
+			gst_task_stop(self->task);
+		}
+
+		state->cam_->start(&state->initControls_);
+	}
+
 	/*
 	 * Create and queue one request. If no buffers are available the
 	 * function returns -ENOBUFS, which we ignore here as that's not a
@@ -458,11 +612,8 @@
 	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(user_data);
 	GLibRecLocker lock(&self->stream_lock);
 	GstLibcameraSrcState *state = self->state;
-	GstFlowReturn flow_ret = GST_FLOW_OK;
 	gint ret;
 
-	g_autoptr(GstStructure) element_caps = gst_structure_new_empty("caps");
-
 	GST_DEBUG_OBJECT(self, "Streaming thread has started");
 
 	gint stream_id_num = 0;
@@ -490,89 +641,21 @@
 	}
 	g_assert(state->config_->size() == state->srcpads_.size());
 
-	for (gsize i = 0; i < state->srcpads_.size(); i++) {
-		GstPad *srcpad = state->srcpads_[i];
-		StreamConfiguration &stream_cfg = state->config_->at(i);
-
-		/* Retrieve the supported caps. */
-		g_autoptr(GstCaps) filter = gst_libcamera_stream_formats_to_caps(stream_cfg.formats());
-		g_autoptr(GstCaps) caps = gst_pad_peer_query_caps(srcpad, filter);
-		if (gst_caps_is_empty(caps)) {
-			flow_ret = GST_FLOW_NOT_NEGOTIATED;
-			break;
-		}
-
-		/* Fixate caps and configure the stream. */
-		caps = gst_caps_make_writable(caps);
-		gst_libcamera_configure_stream_from_caps(stream_cfg, caps);
-		gst_libcamera_get_framerate_from_caps(caps, element_caps);
-	}
-
-	if (flow_ret != GST_FLOW_OK)
-		goto done;
-
-	/* Validate the configuration. */
-	if (state->config_->validate() == CameraConfiguration::Invalid) {
-		flow_ret = GST_FLOW_NOT_NEGOTIATED;
-		goto done;
-	}
-
-	ret = state->cam_->configure(state->config_.get());
-	if (ret) {
-		GST_ELEMENT_ERROR(self, RESOURCE, SETTINGS,
-				  ("Failed to configure camera: %s", g_strerror(-ret)),
-				  ("Camera::configure() failed with error code %i", ret));
-		gst_task_stop(task);
-		flow_ret = GST_FLOW_NOT_NEGOTIATED;
-		goto done;
-	}
-
-	/* Check frame duration bounds within controls::FrameDurationLimits */
-	gst_libcamera_clamp_and_set_frameduration(state->initControls_,
-						  state->cam_->controls(), element_caps);
-
-	/*
-	 * Regardless if it has been modified, create clean caps and push the
-	 * caps event. Downstream will decide if the caps are acceptable.
-	 */
-	for (gsize i = 0; i < state->srcpads_.size(); i++) {
-		GstPad *srcpad = state->srcpads_[i];
-		const StreamConfiguration &stream_cfg = state->config_->at(i);
-
-		g_autoptr(GstCaps) caps = gst_libcamera_stream_configuration_to_caps(stream_cfg);
-		gst_libcamera_framerate_to_caps(caps, element_caps);
-
-		if (!gst_pad_push_event(srcpad, gst_event_new_caps(caps))) {
-			flow_ret = GST_FLOW_NOT_NEGOTIATED;
-			break;
-		}
-
-		/* Send an open segment event with time format. */
-		GstSegment segment;
-		gst_segment_init(&segment, GST_FORMAT_TIME);
-		gst_pad_push_event(srcpad, gst_event_new_segment(&segment));
-	}
-
-	self->allocator = gst_libcamera_allocator_new(state->cam_, state->config_.get());
-	if (!self->allocator) {
-		GST_ELEMENT_ERROR(self, RESOURCE, NO_SPACE_LEFT,
-				  ("Failed to allocate memory"),
-				  ("gst_libcamera_allocator_new() failed."));
+	if (!gst_libcamera_src_negotiate(self)) {
+		state->initControls_.clear();
+		GST_ELEMENT_FLOW_ERROR(self, GST_FLOW_NOT_NEGOTIATED);
 		gst_task_stop(task);
 		return;
 	}
 
 	self->flow_combiner = gst_flow_combiner_new();
-	for (gsize i = 0; i < state->srcpads_.size(); i++) {
-		GstPad *srcpad = state->srcpads_[i];
-		const StreamConfiguration &stream_cfg = state->config_->at(i);
-		GstLibcameraPool *pool = gst_libcamera_pool_new(self->allocator,
-								stream_cfg.stream());
-		g_signal_connect_swapped(pool, "buffer-notify",
-					 G_CALLBACK(gst_task_resume), task);
-
-		gst_libcamera_pad_set_pool(srcpad, pool);
+	for (GstPad *srcpad : state->srcpads_) {
 		gst_flow_combiner_add_pad(self->flow_combiner, srcpad);
+
+		/* Send an open segment event with time format. */
+		GstSegment segment;
+		gst_segment_init(&segment, GST_FORMAT_TIME);
+		gst_pad_push_event(srcpad, gst_event_new_segment(&segment));
 	}
 
 	if (self->auto_focus_mode != controls::AfModeManual) {
@@ -595,17 +678,6 @@
 		gst_task_stop(task);
 		return;
 	}
-
-done:
-	state->initControls_.clear();
-	switch (flow_ret) {
-	case GST_FLOW_NOT_NEGOTIATED:
-		GST_ELEMENT_FLOW_ERROR(self, flow_ret);
-		gst_task_stop(task);
-		break;
-	default:
-		break;
-	}
 }
 
 static void
@@ -619,11 +691,7 @@
 	GST_DEBUG_OBJECT(self, "Streaming thread is about to stop");
 
 	state->cam_->stop();
-
-	{
-		GLibLocker locker(&state->lock_);
-		state->completedRequests_ = {};
-	}
+	state->clearRequests();
 
 	{
 		GLibRecLocker locker(&self->stream_lock);
@@ -747,6 +815,27 @@
 	return ret;
 }
 
+static gboolean
+gst_libcamera_src_send_event(GstElement *element, GstEvent *event)
+{
+	GstLibcameraSrc *self = GST_LIBCAMERA_SRC(element);
+	gboolean ret = FALSE;
+
+	switch (GST_EVENT_TYPE(event)) {
+	case GST_EVENT_EOS: {
+		GstEvent *oldEvent = self->pending_eos.exchange(event);
+		gst_clear_event(&oldEvent);
+		ret = TRUE;
+		break;
+	}
+	default:
+		gst_event_unref(event);
+		break;
+	}
+
+	return ret;
+}
+
 static void
 gst_libcamera_src_finalize(GObject *object)
 {
@@ -779,6 +868,8 @@
 	state->srcpads_.push_back(gst_pad_new_from_template(templ, "src"));
 	gst_element_add_pad(GST_ELEMENT(self), state->srcpads_.back());
 
+	GST_OBJECT_FLAG_SET(self, GST_ELEMENT_FLAG_SOURCE);
+
 	/* C-style friend. */
 	state->src_ = self;
 	self->state = state;
@@ -844,6 +935,7 @@
 	element_class->request_new_pad = gst_libcamera_src_request_new_pad;
 	element_class->release_pad = gst_libcamera_src_release_pad;
 	element_class->change_state = gst_libcamera_src_change_state;
+	element_class->send_event = gst_libcamera_src_send_event;
 
 	gst_element_class_set_metadata(element_class,
 				       "libcamera Source", "Source/Video",
diff --git a/src/gstreamer/gstlibcamerasrc.h b/src/gstreamer/gstlibcamerasrc.h
index 0a88ba0..fd1f819 100644
--- a/src/gstreamer/gstlibcamerasrc.h
+++ b/src/gstreamer/gstlibcamerasrc.h
@@ -3,7 +3,7 @@
  * Copyright (C) 2019, Collabora Ltd.
  *     Author: Nicolas Dufresne <nicolas.dufresne@collabora.com>
  *
- * gstlibcamerasrc.h - GStreamer Capture Element
+ * GStreamer Capture Element
  */
 
 #pragma once
diff --git a/src/gstreamer/meson.build b/src/gstreamer/meson.build
index 20784b7..c2a01e7 100644
--- a/src/gstreamer/meson.build
+++ b/src/gstreamer/meson.build
@@ -46,3 +46,15 @@
     install : true,
     install_dir : '@0@/gstreamer-1.0'.format(get_option('libdir')),
 )
+
+# Make the plugin visible to GStreamer inside meson devenv.
+fs = import('fs')
+gst_plugin_path = fs.parent(libcamera_gst.full_path())
+
+gst_env = environment()
+gst_env.prepend('GST_PLUGIN_PATH', gst_plugin_path)
+
+# Avoid polluting the system registry.
+gst_env.set('GST_REGISTRY', gst_plugin_path / 'registry.data')
+
+meson.add_devenv(gst_env)
diff --git a/src/ipa/ipa-sign-install.sh b/src/ipa/ipa-sign-install.sh
index bcedb8b..71696d5 100755
--- a/src/ipa/ipa-sign-install.sh
+++ b/src/ipa/ipa-sign-install.sh
@@ -4,7 +4,7 @@
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# ipa-sign-install.sh - Regenerate IPA module signatures when installing
+# Regenerate IPA module signatures when installing
 
 key=$1
 shift
diff --git a/src/ipa/ipa-sign.sh b/src/ipa/ipa-sign.sh
index 8673dad..6902421 100755
--- a/src/ipa/ipa-sign.sh
+++ b/src/ipa/ipa-sign.sh
@@ -4,7 +4,7 @@
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# ipa-sign.sh - Generate a signature for an IPA module
+# Generate a signature for an IPA module
 
 key="$1"
 input="$2"
diff --git a/src/ipa/ipu3/algorithms/af.cpp b/src/ipa/ipu3/algorithms/af.cpp
index 12927ee..29eb735 100644
--- a/src/ipa/ipu3/algorithms/af.cpp
+++ b/src/ipa/ipu3/algorithms/af.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Red Hat
  *
- * af.cpp - IPU3 auto focus algorithm
+ * IPU3 auto focus algorithm
  */
 
 #include "af.h"
diff --git a/src/ipa/ipu3/algorithms/af.h b/src/ipa/ipu3/algorithms/af.h
index c6168e3..68126d4 100644
--- a/src/ipa/ipu3/algorithms/af.h
+++ b/src/ipa/ipu3/algorithms/af.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Red Hat
  *
- * af.h - IPU3 Af algorithm
+ * IPU3 Af algorithm
  */
 
 #pragma once
diff --git a/src/ipa/ipu3/algorithms/agc.cpp b/src/ipa/ipu3/algorithms/agc.cpp
index 606a237..0e0114f 100644
--- a/src/ipa/ipu3/algorithms/agc.cpp
+++ b/src/ipa/ipu3/algorithms/agc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas On Board
  *
- * ipu3_agc.cpp - AGC/AEC mean-based control algorithm
+ * AGC/AEC mean-based control algorithm
  */
 
 #include "agc.h"
@@ -56,27 +56,35 @@
 /* Histogram constants */
 static constexpr uint32_t knumHistogramBins = 256;
 
-/* Target value to reach for the top 2% of the histogram */
-static constexpr double kEvGainTarget = 0.5;
-
-/* Number of frames to wait before calculating stats on minimum exposure */
-static constexpr uint32_t kNumStartupFrames = 10;
-
-/*
- * Relative luminance target.
- *
- * It's a number that's chosen so that, when the camera points at a grey
- * target, the resulting image brightness is considered right.
- */
-static constexpr double kRelativeLuminanceTarget = 0.16;
-
 Agc::Agc()
-	: frameCount_(0), minShutterSpeed_(0s),
-	  maxShutterSpeed_(0s), filteredExposure_(0s)
+	: minShutterSpeed_(0s), maxShutterSpeed_(0s)
 {
 }
 
 /**
+ * \brief Initialise the AGC algorithm from tuning files
+ * \param[in] context The shared IPA context
+ * \param[in] tuningData The YamlObject containing Agc tuning data
+ *
+ * This function calls the base class' tuningData parsers to discover which
+ * control values are supported.
+ *
+ * \return 0 on success or errors from the base class
+ */
+int Agc::init(IPAContext &context, const YamlObject &tuningData)
+{
+	int ret;
+
+	ret = parseTuningData(tuningData);
+	if (ret)
+		return ret;
+
+	context.ctrlMap.merge(controls());
+
+	return 0;
+}
+
+/**
  * \brief Configure the AGC given a configInfo
  * \param[in] context The shared IPA context
  * \param[in] configInfo The IPA configuration data
@@ -90,6 +98,7 @@
 	IPAActiveState &activeState = context.activeState;
 
 	stride_ = configuration.grid.stride;
+	bdsGrid_ = configuration.grid.bdsGrid;
 
 	minShutterSpeed_ = configuration.agc.minShutterSpeed;
 	maxShutterSpeed_ = std::min(configuration.agc.maxShutterSpeed,
@@ -102,168 +111,53 @@
 	activeState.agc.gain = minAnalogueGain_;
 	activeState.agc.exposure = 10ms / configuration.sensor.lineDuration;
 
-	frameCount_ = 0;
+	context.activeState.agc.constraintMode = constraintModes().begin()->first;
+	context.activeState.agc.exposureMode = exposureModeHelpers().begin()->first;
+
+	/* \todo Run this again when FrameDurationLimits is passed in */
+	setLimits(minShutterSpeed_, maxShutterSpeed_, minAnalogueGain_,
+		  maxAnalogueGain_);
+	resetFrameCount();
+
 	return 0;
 }
 
-/**
- * \brief Estimate the mean value of the top 2% of the histogram
- * \param[in] stats The statistics computed by the ImgU
- * \param[in] grid The grid used to store the statistics in the IPU3
- * \return The mean value of the top 2% of the histogram
- */
-double Agc::measureBrightness(const ipu3_uapi_stats_3a *stats,
-			      const ipu3_uapi_grid_config &grid) const
+Histogram Agc::parseStatistics(const ipu3_uapi_stats_3a *stats,
+			       const ipu3_uapi_grid_config &grid)
 {
-	/* Initialise the histogram array */
 	uint32_t hist[knumHistogramBins] = { 0 };
 
+	rgbTriples_.clear();
+
 	for (unsigned int cellY = 0; cellY < grid.height; cellY++) {
 		for (unsigned int cellX = 0; cellX < grid.width; cellX++) {
 			uint32_t cellPosition = cellY * stride_ + cellX;
 
 			const ipu3_uapi_awb_set_item *cell =
 				reinterpret_cast<const ipu3_uapi_awb_set_item *>(
-					&stats->awb_raw_buffer.meta_data[cellPosition]
-				);
+					&stats->awb_raw_buffer.meta_data[cellPosition]);
 
-			uint8_t gr = cell->Gr_avg;
-			uint8_t gb = cell->Gb_avg;
+			rgbTriples_.push_back({
+				cell->R_avg,
+				(cell->Gr_avg + cell->Gb_avg) / 2,
+				cell->B_avg
+			});
+
 			/*
 			 * Store the average green value to estimate the
 			 * brightness. Even the overexposed pixels are
 			 * taken into account.
 			 */
-			hist[(gr + gb) / 2]++;
+			hist[(cell->Gr_avg + cell->Gb_avg) / 2]++;
 		}
 	}
 
-	/* Estimate the quantile mean of the top 2% of the histogram. */
-	return Histogram(Span<uint32_t>(hist)).interQuantileMean(0.98, 1.0);
-}
-
-/**
- * \brief Apply a filter on the exposure value to limit the speed of changes
- * \param[in] exposureValue The target exposure from the AGC algorithm
- *
- * The speed of the filter is adaptive, and will produce the target quicker
- * during startup, or when the target exposure is within 20% of the most recent
- * filter output.
- *
- * \return The filtered exposure
- */
-utils::Duration Agc::filterExposure(utils::Duration exposureValue)
-{
-	double speed = 0.2;
-
-	/* Adapt instantly if we are in startup phase. */
-	if (frameCount_ < kNumStartupFrames)
-		speed = 1.0;
-
-	/*
-	 * If we are close to the desired result, go faster to avoid making
-	 * multiple micro-adjustments.
-	 * \todo Make this customisable?
-	 */
-	if (filteredExposure_ < 1.2 * exposureValue &&
-	    filteredExposure_ > 0.8 * exposureValue)
-		speed = sqrt(speed);
-
-	filteredExposure_ = speed * exposureValue +
-			    filteredExposure_ * (1.0 - speed);
-
-	LOG(IPU3Agc, Debug) << "After filtering, exposure " << filteredExposure_;
-
-	return filteredExposure_;
-}
-
-/**
- * \brief Estimate the new exposure and gain values
- * \param[inout] frameContext The shared IPA frame Context
- * \param[in] yGain The gain calculated based on the relative luminance target
- * \param[in] iqMeanGain The gain calculated based on the relative luminance target
- */
-void Agc::computeExposure(IPAContext &context, IPAFrameContext &frameContext,
-			  double yGain, double iqMeanGain)
-{
-	const IPASessionConfiguration &configuration = context.configuration;
-	/* Get the effective exposure and gain applied on the sensor. */
-	uint32_t exposure = frameContext.sensor.exposure;
-	double analogueGain = frameContext.sensor.gain;
-
-	/* Use the highest of the two gain estimates. */
-	double evGain = std::max(yGain, iqMeanGain);
-
-	/* Consider within 1% of the target as correctly exposed */
-	if (utils::abs_diff(evGain, 1.0) < 0.01)
-		LOG(IPU3Agc, Debug) << "We are well exposed (evGain = "
-				    << evGain << ")";
-
-	/* extracted from Rpi::Agc::computeTargetExposure */
-
-	/* Calculate the shutter time in seconds */
-	utils::Duration currentShutter = exposure * configuration.sensor.lineDuration;
-
-	/*
-	 * Update the exposure value for the next computation using the values
-	 * of exposure and gain really used by the sensor.
-	 */
-	utils::Duration effectiveExposureValue = currentShutter * analogueGain;
-
-	LOG(IPU3Agc, Debug) << "Actual total exposure " << currentShutter * analogueGain
-			    << " Shutter speed " << currentShutter
-			    << " Gain " << analogueGain
-			    << " Needed ev gain " << evGain;
-
-	/*
-	 * Calculate the current exposure value for the scene as the latest
-	 * exposure value applied multiplied by the new estimated gain.
-	 */
-	utils::Duration exposureValue = effectiveExposureValue * evGain;
-
-	/* Clamp the exposure value to the min and max authorized */
-	utils::Duration maxTotalExposure = maxShutterSpeed_ * maxAnalogueGain_;
-	exposureValue = std::min(exposureValue, maxTotalExposure);
-	LOG(IPU3Agc, Debug) << "Target total exposure " << exposureValue
-			    << ", maximum is " << maxTotalExposure;
-
-	/*
-	 * Filter the exposure.
-	 * \todo estimate if we need to desaturate
-	 */
-	exposureValue = filterExposure(exposureValue);
-
-	/*
-	 * Divide the exposure value as new exposure and gain values.
-	 *
-	 * Push the shutter time up to the maximum first, and only then
-	 * increase the gain.
-	 */
-	utils::Duration shutterTime =
-		std::clamp<utils::Duration>(exposureValue / minAnalogueGain_,
-					    minShutterSpeed_, maxShutterSpeed_);
-	double stepGain = std::clamp(exposureValue / shutterTime,
-				     minAnalogueGain_, maxAnalogueGain_);
-	LOG(IPU3Agc, Debug) << "Divided up shutter and gain are "
-			    << shutterTime << " and "
-			    << stepGain;
-
-	IPAActiveState &activeState = context.activeState;
-	/* Update the estimated exposure and gain. */
-	activeState.agc.exposure = shutterTime / configuration.sensor.lineDuration;
-	activeState.agc.gain = stepGain;
+	return Histogram(Span<uint32_t>(hist));
 }
 
 /**
  * \brief Estimate the relative luminance of the frame with a given gain
- * \param[in] frameContext The shared IPA frame context
- * \param[in] grid The grid used to store the statistics in the IPU3
- * \param[in] stats The IPU3 statistics and ISP results
- * \param[in] gain The gain to apply to the frame
- * \return The relative luminance
- *
- * This function estimates the average relative luminance of the frame that
- * would be output by the sensor if an additional \a gain was applied.
+ * \param[in] gain The gain to apply in estimating luminance
  *
  * The estimation is based on the AWB statistics for the current frame. Red,
  * green and blue averages for all cells are first multiplied by the gain, and
@@ -278,40 +172,24 @@
  *
  * More detailed information can be found in:
  * https://en.wikipedia.org/wiki/Relative_luminance
+ *
+ * \return The relative luminance of the frame
  */
-double Agc::estimateLuminance(IPAActiveState &activeState,
-			      const ipu3_uapi_grid_config &grid,
-			      const ipu3_uapi_stats_3a *stats,
-			      double gain)
+double Agc::estimateLuminance(double gain) const
 {
 	double redSum = 0, greenSum = 0, blueSum = 0;
 
-	/* Sum the per-channel averages, saturated to 255. */
-	for (unsigned int cellY = 0; cellY < grid.height; cellY++) {
-		for (unsigned int cellX = 0; cellX < grid.width; cellX++) {
-			uint32_t cellPosition = cellY * stride_ + cellX;
-
-			const ipu3_uapi_awb_set_item *cell =
-				reinterpret_cast<const ipu3_uapi_awb_set_item *>(
-					&stats->awb_raw_buffer.meta_data[cellPosition]
-				);
-			const uint8_t G_avg = (cell->Gr_avg + cell->Gb_avg) / 2;
-
-			redSum += std::min(cell->R_avg * gain, 255.0);
-			greenSum += std::min(G_avg * gain, 255.0);
-			blueSum += std::min(cell->B_avg * gain, 255.0);
-		}
+	for (unsigned int i = 0; i < rgbTriples_.size(); i++) {
+		redSum += std::min(std::get<0>(rgbTriples_[i]) * gain, 255.0);
+		greenSum += std::min(std::get<1>(rgbTriples_[i]) * gain, 255.0);
+		blueSum += std::min(std::get<2>(rgbTriples_[i]) * gain, 255.0);
 	}
 
-	/*
-	 * Apply the AWB gains to approximate colours correctly, use the Rec.
-	 * 601 formula to calculate the relative luminance, and normalize it.
-	 */
-	double ySum = redSum * activeState.awb.gains.red * 0.299
-		    + greenSum * activeState.awb.gains.green * 0.587
-		    + blueSum * activeState.awb.gains.blue * 0.114;
+	double ySum = redSum * rGain_ * 0.299
+		    + greenSum * gGain_ * 0.587
+		    + blueSum * bGain_ * 0.114;
 
-	return ySum / (grid.height * grid.width) / 255;
+	return ySum / (bdsGrid_.height * bdsGrid_.width) / 255;
 }
 
 /**
@@ -330,44 +208,36 @@
 		  const ipu3_uapi_stats_3a *stats,
 		  ControlList &metadata)
 {
-	/*
-	 * Estimate the gain needed to have the proportion of pixels in a given
-	 * desired range. iqMean is the mean value of the top 2% of the
-	 * cumulative histogram, and we want it to be as close as possible to a
-	 * configured target.
-	 */
-	double iqMean = measureBrightness(stats, context.configuration.grid.bdsGrid);
-	double iqMeanGain = kEvGainTarget * knumHistogramBins / iqMean;
+	Histogram hist = parseStatistics(stats, context.configuration.grid.bdsGrid);
+	rGain_ = context.activeState.awb.gains.red;
+	gGain_ = context.activeState.awb.gains.blue;
+	bGain_ = context.activeState.awb.gains.green;
 
 	/*
-	 * Estimate the gain needed to achieve a relative luminance target. To
-	 * account for non-linearity caused by saturation, the value needs to be
-	 * estimated in an iterative process, as multiplying by a gain will not
-	 * increase the relative luminance by the same factor if some image
-	 * regions are saturated.
+	 * The Agc algorithm needs to know the effective exposure value that was
+	 * applied to the sensor when the statistics were collected.
 	 */
-	double yGain = 1.0;
-	double yTarget = kRelativeLuminanceTarget;
-
-	for (unsigned int i = 0; i < 8; i++) {
-		double yValue = estimateLuminance(context.activeState,
-						  context.configuration.grid.bdsGrid,
-						  stats, yGain);
-		double extraGain = std::min(10.0, yTarget / (yValue + .001));
-
-		yGain *= extraGain;
-		LOG(IPU3Agc, Debug) << "Y value: " << yValue
-				    << ", Y target: " << yTarget
-				    << ", gives gain " << yGain;
-		if (extraGain < 1.01)
-			break;
-	}
-
-	computeExposure(context, frameContext, yGain, iqMeanGain);
-	frameCount_++;
-
 	utils::Duration exposureTime = context.configuration.sensor.lineDuration
 				     * frameContext.sensor.exposure;
+	double analogueGain = frameContext.sensor.gain;
+	utils::Duration effectiveExposureValue = exposureTime * analogueGain;
+
+	utils::Duration shutterTime;
+	double aGain, dGain;
+	std::tie(shutterTime, aGain, dGain) =
+		calculateNewEv(context.activeState.agc.constraintMode,
+			       context.activeState.agc.exposureMode, hist,
+			       effectiveExposureValue);
+
+	LOG(IPU3Agc, Debug)
+		<< "Divided up shutter, analogue gain and digital gain are "
+		<< shutterTime << ", " << aGain << " and " << dGain;
+
+	IPAActiveState &activeState = context.activeState;
+	/* Update the estimated exposure and gain. */
+	activeState.agc.exposure = shutterTime / context.configuration.sensor.lineDuration;
+	activeState.agc.gain = aGain;
+
 	metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
 	metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
 
diff --git a/src/ipa/ipu3/algorithms/agc.h b/src/ipa/ipu3/algorithms/agc.h
index 9d6e3ff..411f4da 100644
--- a/src/ipa/ipu3/algorithms/agc.h
+++ b/src/ipa/ipu3/algorithms/agc.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas On Board
  *
- * agc.h - IPU3 AGC/AEC mean-based control algorithm
+ * IPU3 AGC/AEC mean-based control algorithm
  */
 
 #pragma once
@@ -13,6 +13,9 @@
 
 #include <libcamera/geometry.h>
 
+#include "libipa/agc_mean_luminance.h"
+#include "libipa/histogram.h"
+
 #include "algorithm.h"
 
 namespace libcamera {
@@ -21,12 +24,13 @@
 
 namespace ipa::ipu3::algorithms {
 
-class Agc : public Algorithm
+class Agc : public Algorithm, public AgcMeanLuminance
 {
 public:
 	Agc();
 	~Agc() = default;
 
+	int init(IPAContext &context, const YamlObject &tuningData) override;
 	int configure(IPAContext &context, const IPAConfigInfo &configInfo) override;
 	void process(IPAContext &context, const uint32_t frame,
 		     IPAFrameContext &frameContext,
@@ -34,17 +38,9 @@
 		     ControlList &metadata) override;
 
 private:
-	double measureBrightness(const ipu3_uapi_stats_3a *stats,
-				 const ipu3_uapi_grid_config &grid) const;
-	utils::Duration filterExposure(utils::Duration currentExposure);
-	void computeExposure(IPAContext &context, IPAFrameContext &frameContext,
-			     double yGain, double iqMeanGain);
-	double estimateLuminance(IPAActiveState &activeState,
-				 const ipu3_uapi_grid_config &grid,
-				 const ipu3_uapi_stats_3a *stats,
-				 double gain);
-
-	uint64_t frameCount_;
+	double estimateLuminance(double gain) const override;
+	Histogram parseStatistics(const ipu3_uapi_stats_3a *stats,
+				  const ipu3_uapi_grid_config &grid);
 
 	utils::Duration minShutterSpeed_;
 	utils::Duration maxShutterSpeed_;
@@ -52,9 +48,12 @@
 	double minAnalogueGain_;
 	double maxAnalogueGain_;
 
-	utils::Duration filteredExposure_;
-
 	uint32_t stride_;
+	double rGain_;
+	double gGain_;
+	double bGain_;
+	ipu3_uapi_grid_config bdsGrid_;
+	std::vector<std::tuple<uint8_t, uint8_t, uint8_t>> rgbTriples_;
 };
 
 } /* namespace ipa::ipu3::algorithms */
diff --git a/src/ipa/ipu3/algorithms/algorithm.h b/src/ipa/ipu3/algorithms/algorithm.h
index ae134a9..c7801f9 100644
--- a/src/ipa/ipu3/algorithms/algorithm.h
+++ b/src/ipa/ipu3/algorithms/algorithm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas On Board
  *
- * algorithm.h - IPU3 control algorithm interface
+ * IPU3 control algorithm interface
  */
 
 #pragma once
diff --git a/src/ipa/ipu3/algorithms/awb.cpp b/src/ipa/ipu3/algorithms/awb.cpp
index 5abd462..4d6e399 100644
--- a/src/ipa/ipu3/algorithms/awb.cpp
+++ b/src/ipa/ipu3/algorithms/awb.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas On Board
  *
- * awb.cpp - AWB control algorithm
+ * AWB control algorithm
  */
 #include "awb.h"
 
diff --git a/src/ipa/ipu3/algorithms/awb.h b/src/ipa/ipu3/algorithms/awb.h
index 7a70854..c020282 100644
--- a/src/ipa/ipu3/algorithms/awb.h
+++ b/src/ipa/ipu3/algorithms/awb.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas On Board
  *
- * awb.h - IPU3 AWB control algorithm
+ * IPU3 AWB control algorithm
  */
 
 #pragma once
diff --git a/src/ipa/ipu3/algorithms/blc.cpp b/src/ipa/ipu3/algorithms/blc.cpp
index e838072..257f40e 100644
--- a/src/ipa/ipu3/algorithms/blc.cpp
+++ b/src/ipa/ipu3/algorithms/blc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google inc.
  *
- * blc.cpp - IPU3 Black Level Correction control
+ * IPU3 Black Level Correction control
  */
 
 #include "blc.h"
diff --git a/src/ipa/ipu3/algorithms/blc.h b/src/ipa/ipu3/algorithms/blc.h
index 292bf67..6274804 100644
--- a/src/ipa/ipu3/algorithms/blc.h
+++ b/src/ipa/ipu3/algorithms/blc.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google inc.
  *
- * black_correction.h - IPU3 Black Level Correction control
+ * IPU3 Black Level Correction control
  */
 
 #pragma once
diff --git a/src/ipa/ipu3/algorithms/tone_mapping.cpp b/src/ipa/ipu3/algorithms/tone_mapping.cpp
index a169894..160338c 100644
--- a/src/ipa/ipu3/algorithms/tone_mapping.cpp
+++ b/src/ipa/ipu3/algorithms/tone_mapping.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google inc.
  *
- * tone_mapping.cpp - IPU3 ToneMapping and Gamma control
+ * IPU3 ToneMapping and Gamma control
  */
 
 #include "tone_mapping.h"
diff --git a/src/ipa/ipu3/algorithms/tone_mapping.h b/src/ipa/ipu3/algorithms/tone_mapping.h
index 5ae35da..b2b3801 100644
--- a/src/ipa/ipu3/algorithms/tone_mapping.h
+++ b/src/ipa/ipu3/algorithms/tone_mapping.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google inc.
  *
- * tone_mapping.h - IPU3 ToneMapping and Gamma control
+ * IPU3 ToneMapping and Gamma control
  */
 
 #pragma once
diff --git a/src/ipa/ipu3/data/meson.build b/src/ipa/ipu3/data/meson.build
index 1f50b63..0f7cd5c 100644
--- a/src/ipa/ipu3/data/meson.build
+++ b/src/ipa/ipu3/data/meson.build
@@ -5,4 +5,5 @@
 ])
 
 install_data(conf_files,
-             install_dir : ipa_data_dir / 'ipu3')
+             install_dir : ipa_data_dir / 'ipu3',
+             install_tag : 'runtime')
diff --git a/src/ipa/ipu3/ipa_context.cpp b/src/ipa/ipu3/ipa_context.cpp
index 959f314..917d065 100644
--- a/src/ipa/ipu3/ipa_context.cpp
+++ b/src/ipa/ipu3/ipa_context.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * ipa_context.cpp - IPU3 IPA Context
+ * IPU3 IPA Context
  */
 
 #include "ipa_context.h"
@@ -47,6 +47,9 @@
  *
  * \var IPAContext::activeState
  * \brief The current state of IPA algorithms
+ *
+ * \var IPAContext::ctrlMap
+ * \brief A ControlInfoMap::Map of controls populated by the algorithms
  */
 
 /**
diff --git a/src/ipa/ipu3/ipa_context.h b/src/ipa/ipu3/ipa_context.h
index e9a3863..c85d1e3 100644
--- a/src/ipa/ipu3/ipa_context.h
+++ b/src/ipa/ipu3/ipa_context.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * ipa_context.h - IPU3 IPA Context
+ * IPU3 IPA Context
  *
  */
 
@@ -12,6 +12,7 @@
 
 #include <libcamera/base/utils.h>
 
+#include <libcamera/controls.h>
 #include <libcamera/geometry.h>
 
 #include <libipa/fc_queue.h>
@@ -55,6 +56,8 @@
 	struct {
 		uint32_t exposure;
 		double gain;
+		uint32_t constraintMode;
+		uint32_t exposureMode;
 	} agc;
 
 	struct {
@@ -85,6 +88,8 @@
 	IPAActiveState activeState;
 
 	FCQueue<IPAFrameContext> frameContexts;
+
+	ControlInfoMap::Map ctrlMap;
 };
 
 } /* namespace ipa::ipu3 */
diff --git a/src/ipa/ipu3/ipu3.cpp b/src/ipa/ipu3/ipu3.cpp
index 08ee6eb..cdcdf1f 100644
--- a/src/ipa/ipu3/ipu3.cpp
+++ b/src/ipa/ipu3/ipu3.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * ipu3.cpp - IPU3 Image Processing Algorithms
+ * IPU3 Image Processing Algorithms
  */
 
 #include <algorithm>
@@ -189,7 +189,7 @@
 };
 
 IPAIPU3::IPAIPU3()
-	: context_({ {}, {}, { kMaxFrameContexts } })
+	: context_({ {}, {}, { kMaxFrameContexts }, {} })
 {
 }
 
@@ -287,6 +287,7 @@
 							       frameDurations[1],
 							       frameDurations[2]);
 
+	controls.merge(context_.ctrlMap);
 	*ipaControls = ControlInfoMap(std::move(controls), controls::controls);
 }
 
@@ -672,7 +673,7 @@
 const struct IPAModuleInfo ipaModuleInfo = {
 	IPA_MODULE_API_VERSION,
 	1,
-	"PipelineHandlerIPU3",
+	"ipu3",
 	"ipu3",
 };
 
diff --git a/src/ipa/ipu3/meson.build b/src/ipa/ipu3/meson.build
index 66c3984..e76f97c 100644
--- a/src/ipa/ipu3/meson.build
+++ b/src/ipa/ipu3/meson.build
@@ -15,9 +15,8 @@
 mod = shared_module(ipa_name,
                     [ipu3_ipa_sources, libcamera_generated_ipa_headers],
                     name_prefix : '',
-                    include_directories : [ipa_includes, libipa_includes],
-                    dependencies : libcamera_private,
-                    link_with : libipa,
+                    include_directories : [ipa_includes],
+                    dependencies : [libcamera_private, libipa_dep],
                     install : true,
                     install_dir : ipa_install_dir)
 
diff --git a/src/ipa/ipu3/module.h b/src/ipa/ipu3/module.h
index d94fc45..60f65cc 100644
--- a/src/ipa/ipu3/module.h
+++ b/src/ipa/ipu3/module.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas On Board
  *
- * module.h - IPU3 IPA Module
+ * IPU3 IPA Module
  */
 
 #pragma once
diff --git a/src/ipa/libipa/agc_mean_luminance.cpp b/src/ipa/libipa/agc_mean_luminance.cpp
new file mode 100644
index 0000000..f97ef11
--- /dev/null
+++ b/src/ipa/libipa/agc_mean_luminance.cpp
@@ -0,0 +1,577 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Ideas on Board Oy
+ *
+ * Base class for mean luminance AGC algorithms
+ */
+
+#include "agc_mean_luminance.h"
+
+#include <cmath>
+
+#include <libcamera/base/log.h>
+#include <libcamera/control_ids.h>
+
+#include "exposure_mode_helper.h"
+
+using namespace libcamera::controls;
+
+/**
+ * \file agc_mean_luminance.h
+ * \brief Base class implementing mean luminance AEGC
+ */
+
+namespace libcamera {
+
+using namespace std::literals::chrono_literals;
+
+LOG_DEFINE_CATEGORY(AgcMeanLuminance)
+
+namespace ipa {
+
+/*
+ * Number of frames for which to run the algorithm at full speed, before slowing
+ * down to prevent large and jarring changes in exposure from frame to frame.
+ */
+static constexpr uint32_t kNumStartupFrames = 10;
+
+/*
+ * Default relative luminance target
+ *
+ * This value should be chosen so that when the camera points at a grey target,
+ * the resulting image brightness looks "right". Custom values can be passed
+ * as the relativeLuminanceTarget value in sensor tuning files.
+ */
+static constexpr double kDefaultRelativeLuminanceTarget = 0.16;
+
+/**
+ * \struct AgcMeanLuminance::AgcConstraint
+ * \brief The boundaries and target for an AeConstraintMode constraint
+ *
+ * This structure describes an AeConstraintMode constraint for the purposes of
+ * this algorithm. These constraints are expressed as a pair of quantile
+ * boundaries for a histogram, along with a luminance target and a bounds-type.
+ * The algorithm uses the constraints by ensuring that the defined portion of a
+ * luminance histogram (I.E. lying between the two quantiles) is above or below
+ * the given luminance value.
+ */
+
+/**
+ * \enum AgcMeanLuminance::AgcConstraint::Bound
+ * \brief Specify whether the constraint defines a lower or upper bound
+ * \var AgcMeanLuminance::AgcConstraint::Lower
+ * \brief The constraint defines a lower bound
+ * \var AgcMeanLuminance::AgcConstraint::Upper
+ * \brief The constraint defines an upper bound
+ */
+
+/**
+ * \var AgcMeanLuminance::AgcConstraint::bound
+ * \brief The type of constraint bound
+ */
+
+/**
+ * \var AgcMeanLuminance::AgcConstraint::qLo
+ * \brief The lower quantile to use for the constraint
+ */
+
+/**
+ * \var AgcMeanLuminance::AgcConstraint::qHi
+ * \brief The upper quantile to use for the constraint
+ */
+
+/**
+ * \var AgcMeanLuminance::AgcConstraint::yTarget
+ * \brief The luminance target for the constraint
+ */
+
+/**
+ * \class AgcMeanLuminance
+ * \brief A mean-based auto-exposure algorithm
+ *
+ * This algorithm calculates a shutter time, analogue and digital gain such that
+ * the normalised mean luminance value of an image is driven towards a target,
+ * which itself is discovered from tuning data. The algorithm is a two-stage
+ * process.
+ *
+ * In the first stage, an initial gain value is derived by iteratively comparing
+ * the gain-adjusted mean luminance across the entire image against a target,
+ * and selecting a value which pushes it as closely as possible towards the
+ * target.
+ *
+ * In the second stage we calculate the gain required to drive the average of a
+ * section of a histogram to a target value, where the target and the boundaries
+ * of the section of the histogram used in the calculation are taken from the
+ * values defined for the currently configured AeConstraintMode within the
+ * tuning data. This class provides a helper function to parse those tuning data
+ * to discover the constraints, and so requires a specific format for those
+ * data which is described in \ref parseTuningData(). The gain from the first
+ * stage is then clamped to the gain from this stage.
+ *
+ * The final gain is used to adjust the effective exposure value of the image,
+ * and that new exposure value is divided into shutter time, analogue gain and
+ * digital gain according to the selected AeExposureMode. This class uses the
+ * \ref ExposureModeHelper class to assist in that division, and expects the
+ * data needed to initialise that class to be present in tuning data in a
+ * format described in \ref parseTuningData().
+ *
+ * In order to be able to use this algorithm an IPA module needs to be able to
+ * do the following:
+ *
+ * 1. Provide a luminance estimation across an entire image.
+ * 2. Provide a luminance Histogram for the image to use in calculating
+ *    constraint compliance. The precision of the Histogram that is available
+ *    will determine the supportable precision of the constraints.
+ *
+ * IPA modules that want to use this class to implement their AEGC algorithm
+ * should derive it and provide an overriding estimateLuminance() function for
+ * this class to use. They must call parseTuningData() in init(), and must also
+ * call setLimits() and resetFrameCounter() in configure(). They may then use
+ * calculateNewEv() in process(). If the limits passed to setLimits() change for
+ * any reason (for example, in response to a FrameDurationLimit control being
+ * passed in queueRequest()) then setLimits() must be called again with the new
+ * values.
+ */
+
+AgcMeanLuminance::AgcMeanLuminance()
+	: frameCount_(0), filteredExposure_(0s), relativeLuminanceTarget_(0)
+{
+}
+
+AgcMeanLuminance::~AgcMeanLuminance() = default;
+
+void AgcMeanLuminance::parseRelativeLuminanceTarget(const YamlObject &tuningData)
+{
+	relativeLuminanceTarget_ =
+		tuningData["relativeLuminanceTarget"].get<double>(kDefaultRelativeLuminanceTarget);
+}
+
+void AgcMeanLuminance::parseConstraint(const YamlObject &modeDict, int32_t id)
+{
+	for (const auto &[boundName, content] : modeDict.asDict()) {
+		if (boundName != "upper" && boundName != "lower") {
+			LOG(AgcMeanLuminance, Warning)
+				<< "Ignoring unknown constraint bound '" << boundName << "'";
+			continue;
+		}
+
+		unsigned int idx = static_cast<unsigned int>(boundName == "upper");
+		AgcConstraint::Bound bound = static_cast<AgcConstraint::Bound>(idx);
+		double qLo = content["qLo"].get<double>().value_or(0.98);
+		double qHi = content["qHi"].get<double>().value_or(1.0);
+		double yTarget =
+			content["yTarget"].getList<double>().value_or(std::vector<double>{ 0.5 }).at(0);
+
+		AgcConstraint constraint = { bound, qLo, qHi, yTarget };
+
+		if (!constraintModes_.count(id))
+			constraintModes_[id] = {};
+
+		if (idx)
+			constraintModes_[id].push_back(constraint);
+		else
+			constraintModes_[id].insert(constraintModes_[id].begin(), constraint);
+	}
+}
+
+int AgcMeanLuminance::parseConstraintModes(const YamlObject &tuningData)
+{
+	std::vector<ControlValue> availableConstraintModes;
+
+	const YamlObject &yamlConstraintModes = tuningData[controls::AeConstraintMode.name()];
+	if (yamlConstraintModes.isDictionary()) {
+		for (const auto &[modeName, modeDict] : yamlConstraintModes.asDict()) {
+			if (AeConstraintModeNameValueMap.find(modeName) ==
+			    AeConstraintModeNameValueMap.end()) {
+				LOG(AgcMeanLuminance, Warning)
+					<< "Skipping unknown constraint mode '" << modeName << "'";
+				continue;
+			}
+
+			if (!modeDict.isDictionary()) {
+				LOG(AgcMeanLuminance, Error)
+					<< "Invalid constraint mode '" << modeName << "'";
+				return -EINVAL;
+			}
+
+			parseConstraint(modeDict,
+					AeConstraintModeNameValueMap.at(modeName));
+			availableConstraintModes.push_back(
+				AeConstraintModeNameValueMap.at(modeName));
+		}
+	}
+
+	/*
+	 * If the tuning data file contains no constraints then we use the
+	 * default constraint that the IPU3/RkISP1 Agc algorithms were adhering
+	 * to anyway before centralisation; this constraint forces the top 2% of
+	 * the histogram to be at least 0.5.
+	 */
+	if (constraintModes_.empty()) {
+		AgcConstraint constraint = {
+			AgcConstraint::Bound::Lower,
+			0.98,
+			1.0,
+			0.5
+		};
+
+		constraintModes_[controls::ConstraintNormal].insert(
+			constraintModes_[controls::ConstraintNormal].begin(),
+			constraint);
+		availableConstraintModes.push_back(
+			AeConstraintModeNameValueMap.at("ConstraintNormal"));
+	}
+
+	controls_[&controls::AeConstraintMode] = ControlInfo(availableConstraintModes);
+
+	return 0;
+}
+
+int AgcMeanLuminance::parseExposureModes(const YamlObject &tuningData)
+{
+	std::vector<ControlValue> availableExposureModes;
+
+	const YamlObject &yamlExposureModes = tuningData[controls::AeExposureMode.name()];
+	if (yamlExposureModes.isDictionary()) {
+		for (const auto &[modeName, modeValues] : yamlExposureModes.asDict()) {
+			if (AeExposureModeNameValueMap.find(modeName) ==
+			    AeExposureModeNameValueMap.end()) {
+				LOG(AgcMeanLuminance, Warning)
+					<< "Skipping unknown exposure mode '" << modeName << "'";
+				continue;
+			}
+
+			if (!modeValues.isDictionary()) {
+				LOG(AgcMeanLuminance, Error)
+					<< "Invalid exposure mode '" << modeName << "'";
+				return -EINVAL;
+			}
+
+			std::vector<uint32_t> shutters =
+				modeValues["shutter"].getList<uint32_t>().value_or(std::vector<uint32_t>{});
+			std::vector<double> gains =
+				modeValues["gain"].getList<double>().value_or(std::vector<double>{});
+
+			if (shutters.size() != gains.size()) {
+				LOG(AgcMeanLuminance, Error)
+					<< "Shutter and gain array sizes unequal";
+				return -EINVAL;
+			}
+
+			if (shutters.empty()) {
+				LOG(AgcMeanLuminance, Error)
+					<< "Shutter and gain arrays are empty";
+				return -EINVAL;
+			}
+
+			std::vector<std::pair<utils::Duration, double>> stages;
+			for (unsigned int i = 0; i < shutters.size(); i++) {
+				stages.push_back({
+					std::chrono::microseconds(shutters[i]),
+					gains[i]
+				});
+			}
+
+			std::shared_ptr<ExposureModeHelper> helper =
+				std::make_shared<ExposureModeHelper>(stages);
+
+			exposureModeHelpers_[AeExposureModeNameValueMap.at(modeName)] = helper;
+			availableExposureModes.push_back(AeExposureModeNameValueMap.at(modeName));
+		}
+	}
+
+	/*
+	 * If we don't have any exposure modes in the tuning data we create an
+	 * ExposureModeHelper using an empty vector of stages. This will result
+	 * in the ExposureModeHelper simply driving the shutter as high as
+	 * possible before touching gain.
+	 */
+	if (availableExposureModes.empty()) {
+		int32_t exposureModeId = AeExposureModeNameValueMap.at("ExposureNormal");
+		std::vector<std::pair<utils::Duration, double>> stages = { };
+
+		std::shared_ptr<ExposureModeHelper> helper =
+			std::make_shared<ExposureModeHelper>(stages);
+
+		exposureModeHelpers_[exposureModeId] = helper;
+		availableExposureModes.push_back(exposureModeId);
+	}
+
+	controls_[&controls::AeExposureMode] = ControlInfo(availableExposureModes);
+
+	return 0;
+}
+
+/**
+ * \brief Parse tuning data for AeConstraintMode and AeExposureMode controls
+ * \param[in] tuningData the YamlObject representing the tuning data
+ *
+ * This function parses tuning data to build the list of allowed values for the
+ * AeConstraintMode and AeExposureMode controls. Those tuning data must provide
+ * the data in a specific format; the Agc algorithm's tuning data should contain
+ * a dictionary called AeConstraintMode containing per-mode setting dictionaries
+ * with the key being a value from \ref controls::AeConstraintModeNameValueMap.
+ * Each mode dict may contain either a "lower" or "upper" key or both, for
+ * example:
+ *
+ * \code{.unparsed}
+ * algorithms:
+ *   - Agc:
+ *       AeConstraintMode:
+ *         ConstraintNormal:
+ *           lower:
+ *             qLo: 0.98
+ *             qHi: 1.0
+ *             yTarget: 0.5
+ *         ConstraintHighlight:
+ *           lower:
+ *             qLo: 0.98
+ *             qHi: 1.0
+ *             yTarget: 0.5
+ *           upper:
+ *             qLo: 0.98
+ *             qHi: 1.0
+ *             yTarget: 0.8
+ *
+ * \endcode
+ *
+ * For the AeExposureMode control the data should contain a dictionary called
+ * AeExposureMode containing per-mode setting dictionaries with the key being a
+ * value from \ref controls::AeExposureModeNameValueMap. Each mode dict should
+ * contain an array of shutter times with the key "shutter" and an array of gain
+ * values with the key "gain", in this format:
+ *
+ * \code{.unparsed}
+ * algorithms:
+ *   - Agc:
+ *       AeExposureMode:
+ *         ExposureNormal:
+ *           shutter: [ 100, 10000, 30000, 60000, 120000 ]
+ *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
+ *         ExposureShort:
+ *           shutter: [ 100, 10000, 30000, 60000, 120000 ]
+ *           gain: [ 2.0, 4.0, 6.0, 8.0, 10.0 ]
+ *
+ * \endcode
+ *
+ * \return 0 on success or a negative error code
+ */
+int AgcMeanLuminance::parseTuningData(const YamlObject &tuningData)
+{
+	int ret;
+
+	parseRelativeLuminanceTarget(tuningData);
+
+	ret = parseConstraintModes(tuningData);
+	if (ret)
+		return ret;
+
+	return parseExposureModes(tuningData);
+}
+
+/**
+ * \brief Set the ExposureModeHelper limits for this class
+ * \param[in] minShutter Minimum shutter time to allow
+ * \param[in] maxShutter Maximum shutter time to allow
+ * \param[in] minGain Minimum gain to allow
+ * \param[in] maxGain Maximum gain to allow
+ *
+ * This function calls \ref ExposureModeHelper::setLimits() for each
+ * ExposureModeHelper that has been created for this class.
+ */
+void AgcMeanLuminance::setLimits(utils::Duration minShutter,
+				 utils::Duration maxShutter,
+				 double minGain, double maxGain)
+{
+	for (auto &[id, helper] : exposureModeHelpers_)
+		helper->setLimits(minShutter, maxShutter, minGain, maxGain);
+}
+
+/**
+ * \fn AgcMeanLuminance::constraintModes()
+ * \brief Get the constraint modes that have been parsed from tuning data
+ */
+
+/**
+ * \fn AgcMeanLuminance::exposureModeHelpers()
+ * \brief Get the ExposureModeHelpers that have been parsed from tuning data
+ */
+
+/**
+ * \fn AgcMeanLuminance::controls()
+ * \brief Get the controls that have been generated after parsing tuning data
+ */
+
+/**
+ * \fn AgcMeanLuminance::estimateLuminance(const double gain)
+ * \brief Estimate the luminance of an image, adjusted by a given gain
+ * \param[in] gain The gain with which to adjust the luminance estimate
+ *
+ * This function estimates the average relative luminance of the frame that
+ * would be output by the sensor if an additional \a gain was applied. It is a
+ * pure virtual function because estimation of luminance is a hardware-specific
+ * operation, which depends wholly on the format of the stats that are delivered
+ * to libcamera from the ISP. Derived classes must override this function with
+ * one that calculates the normalised mean luminance value across the entire
+ * image.
+ *
+ * \return The normalised relative luminance of the image
+ */
+
+/**
+ * \brief Estimate the initial gain needed to achieve a relative luminance
+ * target
+ * \return The calculated initial gain
+ */
+double AgcMeanLuminance::estimateInitialGain() const
+{
+	double yTarget = relativeLuminanceTarget_;
+	double yGain = 1.0;
+
+	/*
+	* To account for non-linearity caused by saturation, the value needs to
+	* be estimated in an iterative process, as multiplying by a gain will
+	* not increase the relative luminance by the same factor if some image
+	* regions are saturated.
+	*/
+	for (unsigned int i = 0; i < 8; i++) {
+		double yValue = estimateLuminance(yGain);
+		double extra_gain = std::min(10.0, yTarget / (yValue + .001));
+
+		yGain *= extra_gain;
+		LOG(AgcMeanLuminance, Debug) << "Y value: " << yValue
+				<< ", Y target: " << yTarget
+				<< ", gives gain " << yGain;
+
+		if (utils::abs_diff(extra_gain, 1.0) < 0.01)
+			break;
+	}
+
+	return yGain;
+}
+
+/**
+ * \brief Clamp gain within the bounds of a defined constraint
+ * \param[in] constraintModeIndex The index of the constraint to adhere to
+ * \param[in] hist A histogram over which to calculate inter-quantile means
+ * \param[in] gain The gain to clamp
+ *
+ * \return The gain clamped within the constraint bounds
+ */
+double AgcMeanLuminance::constraintClampGain(uint32_t constraintModeIndex,
+					     const Histogram &hist,
+					     double gain)
+{
+	std::vector<AgcConstraint> &constraints = constraintModes_[constraintModeIndex];
+	for (const AgcConstraint &constraint : constraints) {
+		double newGain = constraint.yTarget * hist.bins() /
+				 hist.interQuantileMean(constraint.qLo, constraint.qHi);
+
+		if (constraint.bound == AgcConstraint::Bound::Lower &&
+		    newGain > gain)
+			gain = newGain;
+
+		if (constraint.bound == AgcConstraint::Bound::Upper &&
+		    newGain < gain)
+			gain = newGain;
+	}
+
+	return gain;
+}
+
+/**
+ * \brief Apply a filter on the exposure value to limit the speed of changes
+ * \param[in] exposureValue The target exposure from the AGC algorithm
+ *
+ * The speed of the filter is adaptive, and will produce the target quicker
+ * during startup, or when the target exposure is within 20% of the most recent
+ * filter output.
+ *
+ * \return The filtered exposure
+ */
+utils::Duration AgcMeanLuminance::filterExposure(utils::Duration exposureValue)
+{
+	double speed = 0.2;
+
+	/* Adapt instantly if we are in startup phase. */
+	if (frameCount_ < kNumStartupFrames)
+		speed = 1.0;
+
+	/*
+	 * If we are close to the desired result, go faster to avoid making
+	 * multiple micro-adjustments.
+	 * \todo Make this customisable?
+	 */
+	if (filteredExposure_ < 1.2 * exposureValue &&
+	    filteredExposure_ > 0.8 * exposureValue)
+		speed = sqrt(speed);
+
+	filteredExposure_ = speed * exposureValue +
+			    filteredExposure_ * (1.0 - speed);
+
+	return filteredExposure_;
+}
+
+/**
+ * \brief Calculate the new exposure value and splut it between shutter time and gain
+ * \param[in] constraintModeIndex The index of the current constraint mode
+ * \param[in] exposureModeIndex The index of the current exposure mode
+ * \param[in] yHist A Histogram from the ISP statistics to use in constraining
+ * the calculated gain
+ * \param[in] effectiveExposureValue The EV applied to the frame from which the
+ * statistics in use derive
+ *
+ * Calculate a new exposure value to try to obtain the target. The calculated
+ * exposure value is filtered to prevent rapid changes from frame to frame, and
+ * divided into shutter time, analogue and digital gain.
+ *
+ * \return Tuple of shutter time, analogue gain, and digital gain
+ */
+std::tuple<utils::Duration, double, double>
+AgcMeanLuminance::calculateNewEv(uint32_t constraintModeIndex,
+				 uint32_t exposureModeIndex,
+				 const Histogram &yHist,
+				 utils::Duration effectiveExposureValue)
+{
+	/*
+	 * The pipeline handler should validate that we have received an allowed
+	 * value for AeExposureMode.
+	 */
+	std::shared_ptr<ExposureModeHelper> exposureModeHelper =
+		exposureModeHelpers_.at(exposureModeIndex);
+
+	double gain = estimateInitialGain();
+	gain = constraintClampGain(constraintModeIndex, yHist, gain);
+
+	/*
+	 * We don't check whether we're already close to the target, because
+	 * even if the effective exposure value is the same as the last frame's
+	 * we could have switched to an exposure mode that would require a new
+	 * pass through the splitExposure() function.
+	 */
+
+	utils::Duration newExposureValue = effectiveExposureValue * gain;
+
+	/*
+	 * We filter the exposure value to make sure changes are not too jarring
+	 * from frame to frame.
+	 */
+	newExposureValue = filterExposure(newExposureValue);
+
+	frameCount_++;
+	return exposureModeHelper->splitExposure(newExposureValue);
+}
+
+/**
+ * \fn AgcMeanLuminance::resetFrameCount()
+ * \brief Reset the frame counter
+ *
+ * This function resets the internal frame counter, which exists to help the
+ * algorithm decide whether it should respond instantly or not. The expectation
+ * is for derived classes to call this function before each camera start call in
+ * their configure() function.
+ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/agc_mean_luminance.h b/src/ipa/libipa/agc_mean_luminance.h
new file mode 100644
index 0000000..576d28b
--- /dev/null
+++ b/src/ipa/libipa/agc_mean_luminance.h
@@ -0,0 +1,98 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024 Ideas on Board Oy
+ *
+ agc_mean_luminance.h - Base class for mean luminance AGC algorithms
+ */
+
+#pragma once
+
+#include <map>
+#include <memory>
+#include <tuple>
+#include <vector>
+
+#include <libcamera/base/utils.h>
+
+#include <libcamera/controls.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "exposure_mode_helper.h"
+#include "histogram.h"
+
+namespace libcamera {
+
+namespace ipa {
+
+class AgcMeanLuminance
+{
+public:
+	AgcMeanLuminance();
+	virtual ~AgcMeanLuminance();
+
+	struct AgcConstraint {
+		enum class Bound {
+			Lower = 0,
+			Upper = 1
+		};
+		Bound bound;
+		double qLo;
+		double qHi;
+		double yTarget;
+	};
+
+	int parseTuningData(const YamlObject &tuningData);
+
+	void setLimits(utils::Duration minShutter, utils::Duration maxShutter,
+		       double minGain, double maxGain);
+
+	std::map<int32_t, std::vector<AgcConstraint>> constraintModes()
+	{
+		return constraintModes_;
+	}
+
+	std::map<int32_t, std::shared_ptr<ExposureModeHelper>> exposureModeHelpers()
+	{
+		return exposureModeHelpers_;
+	}
+
+	ControlInfoMap::Map controls()
+	{
+		return controls_;
+	}
+
+	std::tuple<utils::Duration, double, double>
+	calculateNewEv(uint32_t constraintModeIndex, uint32_t exposureModeIndex,
+		       const Histogram &yHist, utils::Duration effectiveExposureValue);
+
+	void resetFrameCount()
+	{
+		frameCount_ = 0;
+	}
+
+private:
+	virtual double estimateLuminance(const double gain) const = 0;
+
+	void parseRelativeLuminanceTarget(const YamlObject &tuningData);
+	void parseConstraint(const YamlObject &modeDict, int32_t id);
+	int parseConstraintModes(const YamlObject &tuningData);
+	int parseExposureModes(const YamlObject &tuningData);
+	double estimateInitialGain() const;
+	double constraintClampGain(uint32_t constraintModeIndex,
+				   const Histogram &hist,
+				   double gain);
+	utils::Duration filterExposure(utils::Duration exposureValue);
+
+	uint64_t frameCount_;
+	utils::Duration filteredExposure_;
+	double relativeLuminanceTarget_;
+
+	std::map<int32_t, std::vector<AgcConstraint>> constraintModes_;
+	std::map<int32_t, std::shared_ptr<ExposureModeHelper>> exposureModeHelpers_;
+	ControlInfoMap::Map controls_;
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/algorithm.cpp b/src/ipa/libipa/algorithm.cpp
index bc1c29a..201efdf 100644
--- a/src/ipa/libipa/algorithm.cpp
+++ b/src/ipa/libipa/algorithm.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas On Board
  *
- * algorithm.cpp - IPA control algorithm interface
+ * IPA control algorithm interface
  */
 
 #include "algorithm.h"
diff --git a/src/ipa/libipa/algorithm.h b/src/ipa/libipa/algorithm.h
index 987e3e4..9a19dbd 100644
--- a/src/ipa/libipa/algorithm.h
+++ b/src/ipa/libipa/algorithm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas On Board
  *
- * algorithm.h - ISP control algorithm interface
+ * ISP control algorithm interface
  */
 #pragma once
 
diff --git a/src/ipa/libipa/camera_sensor_helper.cpp b/src/ipa/libipa/camera_sensor_helper.cpp
index f0ecc38..782ff99 100644
--- a/src/ipa/libipa/camera_sensor_helper.cpp
+++ b/src/ipa/libipa/camera_sensor_helper.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_sensor_helper.cpp - Helper class that performs sensor-specific
+ * Helper class that performs sensor-specific
  * parameter computations
  */
 #include "camera_sensor_helper.h"
@@ -369,30 +369,26 @@
 class CameraSensorHelperAr0521 : public CameraSensorHelper
 {
 public:
-	uint32_t gainCode(double gain) const override;
-	double gain(uint32_t gainCode) const override;
+	uint32_t gainCode(double gain) const override
+	{
+		gain = std::clamp(gain, 1.0, 15.5);
+		unsigned int coarse = std::log2(gain);
+		unsigned int fine = (gain / (1 << coarse) - 1) * kStep_;
+
+		return (coarse << 4) | (fine & 0xf);
+	}
+
+	double gain(uint32_t gainCode) const override
+	{
+		unsigned int coarse = gainCode >> 4;
+		unsigned int fine = gainCode & 0xf;
+
+		return (1 << coarse) * (1 + fine / kStep_);
+	}
 
 private:
 	static constexpr double kStep_ = 16;
 };
-
-uint32_t CameraSensorHelperAr0521::gainCode(double gain) const
-{
-	gain = std::clamp(gain, 1.0, 15.5);
-	unsigned int coarse = std::log2(gain);
-	unsigned int fine = (gain / (1 << coarse) - 1) * kStep_;
-
-	return (coarse << 4) | (fine & 0xf);
-}
-
-double CameraSensorHelperAr0521::gain(uint32_t gainCode) const
-{
-	unsigned int coarse = gainCode >> 4;
-	unsigned int fine = gainCode & 0xf;
-
-	return (1 << coarse) * (1 + fine / kStep_);
-}
-
 REGISTER_CAMERA_SENSOR_HELPER("ar0521", CameraSensorHelperAr0521)
 
 class CameraSensorHelperImx219 : public CameraSensorHelper
@@ -417,6 +413,17 @@
 };
 REGISTER_CAMERA_SENSOR_HELPER("imx258", CameraSensorHelperImx258)
 
+class CameraSensorHelperImx283 : public CameraSensorHelper
+{
+public:
+	CameraSensorHelperImx283()
+	{
+		gainType_ = AnalogueGainLinear;
+		gainConstants_.linear = { 0, 2048, -1, 2048 };
+	}
+};
+REGISTER_CAMERA_SENSOR_HELPER("imx283", CameraSensorHelperImx283)
+
 class CameraSensorHelperImx290 : public CameraSensorHelper
 {
 public:
@@ -444,6 +451,28 @@
 };
 REGISTER_CAMERA_SENSOR_HELPER("imx327", CameraSensorHelperImx327)
 
+class CameraSensorHelperImx335 : public CameraSensorHelper
+{
+public:
+	CameraSensorHelperImx335()
+	{
+		gainType_ = AnalogueGainExponential;
+		gainConstants_.exp = { 1.0, expGainDb(0.3) };
+	}
+};
+REGISTER_CAMERA_SENSOR_HELPER("imx335", CameraSensorHelperImx335)
+
+class CameraSensorHelperImx415 : public CameraSensorHelper
+{
+public:
+	CameraSensorHelperImx415()
+	{
+		gainType_ = AnalogueGainExponential;
+		gainConstants_.exp = { 1.0, expGainDb(0.3) };
+	}
+};
+REGISTER_CAMERA_SENSOR_HELPER("imx415", CameraSensorHelperImx415)
+
 class CameraSensorHelperImx477 : public CameraSensorHelper
 {
 public:
@@ -547,6 +576,17 @@
 };
 REGISTER_CAMERA_SENSOR_HELPER("ov5693", CameraSensorHelperOv5693)
 
+class CameraSensorHelperOv64a40 : public CameraSensorHelper
+{
+public:
+	CameraSensorHelperOv64a40()
+	{
+		gainType_ = AnalogueGainLinear;
+		gainConstants_.linear = { 1, 0, 0, 128 };
+	}
+};
+REGISTER_CAMERA_SENSOR_HELPER("ov64a40", CameraSensorHelperOv64a40)
+
 class CameraSensorHelperOv8858 : public CameraSensorHelper
 {
 public:
diff --git a/src/ipa/libipa/camera_sensor_helper.h b/src/ipa/libipa/camera_sensor_helper.h
index 3ea1806..0d99073 100644
--- a/src/ipa/libipa/camera_sensor_helper.h
+++ b/src/ipa/libipa/camera_sensor_helper.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_sensor_helper.h - Helper class that performs sensor-specific parameter computations
+ * Helper class that performs sensor-specific parameter computations
  */
 
 #pragma once
@@ -88,7 +88,7 @@
 	}
 
 private:
-	std::unique_ptr<CameraSensorHelper> createInstance() const
+	std::unique_ptr<CameraSensorHelper> createInstance() const override
 	{
 		return std::make_unique<_Helper>();
 	}
diff --git a/src/ipa/libipa/exposure_mode_helper.cpp b/src/ipa/libipa/exposure_mode_helper.cpp
new file mode 100644
index 0000000..7703bec
--- /dev/null
+++ b/src/ipa/libipa/exposure_mode_helper.cpp
@@ -0,0 +1,243 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Helper class that performs computations relating to exposure
+ */
+#include "exposure_mode_helper.h"
+
+#include <algorithm>
+
+#include <libcamera/base/log.h>
+
+/**
+ * \file exposure_mode_helper.h
+ * \brief Helper class that performs computations relating to exposure
+ *
+ * AEGC algorithms have a need to split exposure between shutter time, analogue
+ * and digital gain. Multiple implementations do so based on paired stages of
+ * shutter time and gain limits; provide a helper to avoid duplicating the code.
+ */
+
+namespace libcamera {
+
+using namespace std::literals::chrono_literals;
+
+LOG_DEFINE_CATEGORY(ExposureModeHelper)
+
+namespace ipa {
+
+/**
+ * \class ExposureModeHelper
+ * \brief Class for splitting exposure into shutter time and total gain
+ *
+ * The ExposureModeHelper class provides a standard interface through which an
+ * AEGC algorithm can divide exposure between shutter time and gain. It is
+ * configured with a set of shutter time and gain pairs and works by initially
+ * fixing gain at 1.0 and increasing shutter time up to the shutter time value
+ * from the first pair in the set in an attempt to meet the required exposure
+ * value.
+ *
+ * If the required exposure is not achievable by the first shutter time value
+ * alone it ramps gain up to the value from the first pair in the set. If the
+ * required exposure is still not met it then allows shutter time to ramp up to
+ * the shutter time value from the second pair in the set, and continues in this
+ * vein until either the required exposure time is met, or else the hardware's
+ * shutter time or gain limits are reached.
+ *
+ * This method allows users to strike a balance between a well-exposed image and
+ * an acceptable frame-rate, as opposed to simply maximising shutter time
+ * followed by gain. The same helpers can be used to perform the latter
+ * operation if needed by passing an empty set of pairs to the initialisation
+ * function.
+ *
+ * The gain values may exceed a camera sensor's analogue gain limits if either
+ * it or the IPA is also capable of digital gain. The configure() function must
+ * be called with the hardware's limits to inform the helper of those
+ * constraints. Any gain that is needed will be applied as analogue gain first
+ * until the hardware's limit is reached, following which digital gain will be
+ * used.
+ */
+
+/**
+ * \brief Construct an ExposureModeHelper instance
+ * \param[in] stages The vector of paired shutter time and gain limits
+ *
+ * The input stages are shutter time and _total_ gain pairs; the gain
+ * encompasses both analogue and digital gain.
+ *
+ * The vector of stages may be empty. In that case, the helper will simply use
+ * the runtime limits set through setShutterGainLimits() instead.
+ */
+ExposureModeHelper::ExposureModeHelper(const Span<std::pair<utils::Duration, double>> stages)
+{
+	minShutter_ = 0us;
+	maxShutter_ = 0us;
+	minGain_ = 0;
+	maxGain_ = 0;
+
+	for (const auto &[s, g] : stages) {
+		shutters_.push_back(s);
+		gains_.push_back(g);
+	}
+}
+
+/**
+ * \brief Set the shutter time and gain limits
+ * \param[in] minShutter The minimum shutter time supported
+ * \param[in] maxShutter The maximum shutter time supported
+ * \param[in] minGain The minimum analogue gain supported
+ * \param[in] maxGain The maximum analogue gain supported
+ *
+ * This function configures the shutter time and analogue gain limits that need
+ * to be adhered to as the helper divides up exposure. Note that this function
+ * *must* be called whenever those limits change and before splitExposure() is
+ * used.
+ *
+ * If the algorithm using the helpers needs to indicate that either shutter time
+ * or analogue gain or both should be fixed it can do so by setting both the
+ * minima and maxima to the same value.
+ */
+void ExposureModeHelper::setLimits(utils::Duration minShutter,
+				   utils::Duration maxShutter,
+				   double minGain, double maxGain)
+{
+	minShutter_ = minShutter;
+	maxShutter_ = maxShutter;
+	minGain_ = minGain;
+	maxGain_ = maxGain;
+}
+
+utils::Duration ExposureModeHelper::clampShutter(utils::Duration shutter) const
+{
+	return std::clamp(shutter, minShutter_, maxShutter_);
+}
+
+double ExposureModeHelper::clampGain(double gain) const
+{
+	return std::clamp(gain, minGain_, maxGain_);
+}
+
+/**
+ * \brief Split exposure time into shutter time and gain
+ * \param[in] exposure Exposure time
+ *
+ * This function divides a given exposure time into shutter time, analogue and
+ * digital gain by iterating through stages of shutter time and gain limits. At
+ * each stage the current stage's shutter time limit is multiplied by the
+ * previous stage's gain limit (or 1.0 initially) to see if the combination of
+ * the two can meet the required exposure time. If they cannot then the current
+ * stage's shutter time limit is multiplied by the same stage's gain limit to
+ * see if that combination can meet the required exposure time. If they cannot
+ * then the function moves to consider the next stage.
+ *
+ * When a combination of shutter time and gain _stage_ limits are found that are
+ * sufficient to meet the required exposure time, the function attempts to
+ * reduce shutter time as much as possible whilst fixing gain and still meeting
+ * the exposure time. If a _runtime_ limit prevents shutter time from being
+ * lowered enough to meet the exposure time with gain fixed at the stage limit,
+ * gain is also lowered to compensate.
+ *
+ * Once the shutter time and gain values are ascertained, gain is assigned as
+ * analogue gain as much as possible, with digital gain only in use if the
+ * maximum analogue gain runtime limit is unable to accommodate the exposure
+ * value.
+ *
+ * If no combination of shutter time and gain limits is found that meets the
+ * required exposure time, the helper falls-back to simply maximising the
+ * shutter time first, followed by analogue gain, followed by digital gain.
+ *
+ * \return Tuple of shutter time, analogue gain, and digital gain
+ */
+std::tuple<utils::Duration, double, double>
+ExposureModeHelper::splitExposure(utils::Duration exposure) const
+{
+	ASSERT(maxShutter_);
+	ASSERT(maxGain_);
+
+	bool gainFixed = minGain_ == maxGain_;
+	bool shutterFixed = minShutter_ == maxShutter_;
+
+	/*
+	 * There's no point entering the loop if we cannot change either gain
+	 * nor shutter anyway.
+	 */
+	if (shutterFixed && gainFixed)
+		return { minShutter_, minGain_, exposure / (minShutter_ * minGain_) };
+
+	utils::Duration shutter;
+	double stageGain = 1.0;
+	double gain;
+
+	for (unsigned int stage = 0; stage < gains_.size(); stage++) {
+		double lastStageGain = stage == 0 ? 1.0 : clampGain(gains_[stage - 1]);
+		utils::Duration stageShutter = clampShutter(shutters_[stage]);
+		stageGain = clampGain(gains_[stage]);
+
+		/*
+		 * We perform the clamping on both shutter and gain in case the
+		 * helper has had limits set that prevent those values being
+		 * lowered beyond a certain minimum...this can happen at runtime
+		 * for various reasons and so would not be known when the stage
+		 * limits are initialised.
+		 */
+
+		if (stageShutter * lastStageGain >= exposure) {
+			shutter = clampShutter(exposure / clampGain(lastStageGain));
+			gain = clampGain(exposure / shutter);
+
+			return { shutter, gain, exposure / (shutter * gain) };
+		}
+
+		if (stageShutter * stageGain >= exposure) {
+			shutter = clampShutter(exposure / clampGain(stageGain));
+			gain = clampGain(exposure / shutter);
+
+			return { shutter, gain, exposure / (shutter * gain) };
+		}
+	}
+
+	/*
+	 * From here on all we can do is max out the shutter time, followed by
+	 * the analogue gain. If we still haven't achieved the target we send
+	 * the rest of the exposure time to digital gain. If we were given no
+	 * stages to use then the default stageGain of 1.0 is used so that
+	 * shutter time is maxed before gain is touched at all.
+	 */
+	shutter = clampShutter(exposure / clampGain(stageGain));
+	gain = clampGain(exposure / shutter);
+
+	return { shutter, gain, exposure / (shutter * gain) };
+}
+
+/**
+ * \fn ExposureModeHelper::minShutter()
+ * \brief Retrieve the configured minimum shutter time limit set through
+ * setShutterGainLimits()
+ * \return The minShutter_ value
+ */
+
+/**
+ * \fn ExposureModeHelper::maxShutter()
+ * \brief Retrieve the configured maximum shutter time set through
+ * setShutterGainLimits()
+ * \return The maxShutter_ value
+ */
+
+/**
+ * \fn ExposureModeHelper::minGain()
+ * \brief Retrieve the configured minimum gain set through
+ * setShutterGainLimits()
+ * \return The minGain_ value
+ */
+
+/**
+ * \fn ExposureModeHelper::maxGain()
+ * \brief Retrieve the configured maximum gain set through
+ * setShutterGainLimits()
+ * \return The maxGain_ value
+ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/exposure_mode_helper.h b/src/ipa/libipa/exposure_mode_helper.h
new file mode 100644
index 0000000..85c665d
--- /dev/null
+++ b/src/ipa/libipa/exposure_mode_helper.h
@@ -0,0 +1,53 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Helper class that performs computations relating to exposure
+ */
+
+#pragma once
+
+#include <tuple>
+#include <utility>
+#include <vector>
+
+#include <libcamera/base/span.h>
+#include <libcamera/base/utils.h>
+
+namespace libcamera {
+
+namespace ipa {
+
+class ExposureModeHelper
+{
+public:
+	ExposureModeHelper(const Span<std::pair<utils::Duration, double>> stages);
+	~ExposureModeHelper() = default;
+
+	void setLimits(utils::Duration minShutter, utils::Duration maxShutter,
+		       double minGain, double maxGain);
+
+	std::tuple<utils::Duration, double, double>
+	splitExposure(utils::Duration exposure) const;
+
+	utils::Duration minShutter() const { return minShutter_; }
+	utils::Duration maxShutter() const { return maxShutter_; }
+	double minGain() const { return minGain_; }
+	double maxGain() const { return maxGain_; }
+
+private:
+	utils::Duration clampShutter(utils::Duration shutter) const;
+	double clampGain(double gain) const;
+
+	std::vector<utils::Duration> shutters_;
+	std::vector<double> gains_;
+
+	utils::Duration minShutter_;
+	utils::Duration maxShutter_;
+	double minGain_;
+	double maxGain_;
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/fc_queue.cpp b/src/ipa/libipa/fc_queue.cpp
index e812faa..0365e91 100644
--- a/src/ipa/libipa/fc_queue.cpp
+++ b/src/ipa/libipa/fc_queue.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * fc_queue.cpp - IPA Frame context queue
+ * IPA Frame context queue
  */
 
 #include "fc_queue.h"
diff --git a/src/ipa/libipa/fc_queue.h b/src/ipa/libipa/fc_queue.h
index a589e7e..24d9e82 100644
--- a/src/ipa/libipa/fc_queue.h
+++ b/src/ipa/libipa/fc_queue.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * fc_queue.h - IPA Frame context queue
+ * IPA Frame context queue
  */
 
 #pragma once
diff --git a/src/ipa/libipa/histogram.cpp b/src/ipa/libipa/histogram.cpp
index 6b5cde8..5fbfadf 100644
--- a/src/ipa/libipa/histogram.cpp
+++ b/src/ipa/libipa/histogram.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * histogram.cpp - histogram calculations
+ * histogram calculations
  */
 #include "histogram.h"
 
@@ -29,18 +29,34 @@
  */
 
 /**
+ * \fn Histogram::Histogram()
+ * \brief Construct an empty Histogram
+ *
+ * This empty constructor exists largely to allow Histograms to be embedded in
+ * other classes which may be created before the contents of the Histogram are
+ * known.
+ */
+
+/**
  * \brief Create a cumulative histogram
- * \param[in] data A pre-sorted histogram to be passed
+ * \param[in] data A (non-cumulative) histogram
  */
 Histogram::Histogram(Span<const uint32_t> data)
 {
-	cumulative_.reserve(data.size());
-	cumulative_.push_back(0);
-	for (const uint32_t &value : data)
-		cumulative_.push_back(cumulative_.back() + value);
+	cumulative_.resize(data.size() + 1);
+	cumulative_[0] = 0;
+	for (const auto &[i, value] : utils::enumerate(data))
+		cumulative_[i + 1] = cumulative_[i] + value;
 }
 
 /**
+ * \fn Histogram::Histogram(Span<const uint32_t> data, Transform transform)
+ * \brief Create a cumulative histogram
+ * \param[in] data A (non-cumulative) histogram
+ * \param[in] transform The transformation function to apply to every bin
+ */
+
+/**
  * \fn Histogram::bins()
  * \brief Retrieve the number of bins currently used by the Histogram
  * \return Number of bins
diff --git a/src/ipa/libipa/histogram.h b/src/ipa/libipa/histogram.h
index 05bb4b8..032adca 100644
--- a/src/ipa/libipa/histogram.h
+++ b/src/ipa/libipa/histogram.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * histogram.h - histogram calculation interface
+ * histogram calculation interface
  */
 
 #pragma once
@@ -10,10 +10,11 @@
 #include <assert.h>
 #include <limits.h>
 #include <stdint.h>
-
+#include <type_traits>
 #include <vector>
 
 #include <libcamera/base/span.h>
+#include <libcamera/base/utils.h>
 
 namespace libcamera {
 
@@ -22,7 +23,19 @@
 class Histogram
 {
 public:
+	Histogram() { cumulative_.push_back(0); }
 	Histogram(Span<const uint32_t> data);
+
+	template<typename Transform,
+		 std::enable_if_t<std::is_invocable_v<Transform, uint32_t>> * = nullptr>
+	Histogram(Span<const uint32_t> data, Transform transform)
+	{
+		cumulative_.resize(data.size() + 1);
+		cumulative_[0] = 0;
+		for (const auto &[i, value] : utils::enumerate(data))
+			cumulative_[i + 1] = cumulative_[i] + transform(value);
+	}
+
 	size_t bins() const { return cumulative_.size() - 1; }
 	uint64_t total() const { return cumulative_[cumulative_.size() - 1]; }
 	uint64_t cumulativeFrequency(double bin) const;
diff --git a/src/ipa/libipa/matrix.cpp b/src/ipa/libipa/matrix.cpp
new file mode 100644
index 0000000..8346f0d
--- /dev/null
+++ b/src/ipa/libipa/matrix.cpp
@@ -0,0 +1,149 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Matrix and related operations
+ */
+
+#include "matrix.h"
+
+#include <libcamera/base/log.h>
+
+/**
+ * \file matrix.h
+ * \brief Matrix class
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(Matrix)
+
+namespace ipa {
+
+/**
+ * \class Matrix
+ * \brief Matrix class
+ * \tparam T Type of numerical values to be stored in the matrix
+ * \tparam Rows Number of rows in the matrix
+ * \tparam Cols Number of columns in the matrix
+ */
+
+/**
+ * \fn Matrix::Matrix()
+ * \brief Construct a zero matrix
+ */
+
+/**
+ * \fn Matrix::Matrix(const std::vector<T> &data)
+ * \brief Construct a matrix from supplied data
+ * \param[in] data Data from which to construct a matrix
+ *
+ * \a data is a one-dimensional vector and will be turned into a matrix in
+ * row-major order. The size of \a data must be equal to the product of the
+ * number of rows and columns of the matrix (Rows x Cols).
+ */
+
+/**
+ * \fn Matrix::identity()
+ * \brief Construct an identity matrix
+ */
+
+/**
+ * \fn Matrix::toString()
+ * \brief Assemble and return a string describing the matrix
+ * \return A string describing the matrix
+ */
+
+/**
+ * \fn Span<const T, Cols> Matrix::operator[](size_t i) const
+ * \brief Index to a row in the matrix
+ * \param[in] i Index of row to retrieve
+ *
+ * This operator[] returns a Span, which can then be indexed into again with
+ * another operator[], allowing a convenient m[i][j] to access elements of the
+ * matrix. Note that the lifetime of the Span returned by this first-level
+ * operator[] is bound to that of the Matrix itself, so it is not recommended
+ * to save the Span that is the result of this operator[].
+ *
+ * \return Row \a i from the matrix, as a Span
+ */
+
+/**
+ * \fn Matrix::operator[](size_t i)
+ * \copydoc Matrix::operator[](size_t i) const
+ */
+
+/**
+ * \fn Matrix<T, Rows, Cols> &Matrix::operator*=(U d)
+ * \brief Multiply the matrix by a scalar in-place
+ * \tparam U Type of the numerical scalar value
+ * \param d The scalar multiplier
+ * \return Product of this matrix and scalar \a d
+ */
+
+/**
+ * \fn Matrix::Matrix<U, Rows, Cols> operator*(T d, const Matrix<U, Rows, Cols> &m)
+ * \brief Multiply the matrix by a scalar
+ * \tparam T Type of the numerical scalar value
+ * \tparam U Type of numerical values in the matrix
+ * \tparam Rows Number of rows in the matrix
+ * \tparam Cols Number of columns in the matrix
+ * \param d The scalar multiplier
+ * \param m The matrix
+ * \return Product of scalar \a d and matrix \a m
+ */
+
+/**
+ * \fn Matrix::Matrix<U, Rows, Cols> operator*(const Matrix<U, Rows, Cols> &m, T d)
+ * \copydoc operator*(T d, const Matrix<U, Rows, Cols> &m)
+ */
+
+/**
+ * \fn Matrix<T, R1, C2> operator*(const Matrix<T, R1, C1> &m1, const Matrix<T, R2, C2> &m2)
+ * \brief Matrix multiplication
+ * \tparam T Type of numerical values in the matrices
+ * \tparam R1 Number of rows in the first matrix
+ * \tparam C1 Number of columns in the first matrix
+ * \tparam R2 Number of rows in the second matrix
+ * \tparam C2 Number of columns in the second matrix
+ * \param m1 Multiplicand matrix
+ * \param m2 Multiplier matrix
+ * \return Matrix product of matrices \a m1 and \a m2
+ */
+
+/**
+ * \fn Matrix<T, Rows, Cols> operator+(const Matrix<T, Rows, Cols> &m1, const Matrix<T, Rows, Cols> &m2)
+ * \brief Matrix addition
+ * \tparam T Type of numerical values in the matrices
+ * \tparam Rows Number of rows in the matrices
+ * \tparam Cols Number of columns in the matrices
+ * \param m1 Summand matrix
+ * \param m2 Summand matrix
+ * \return Matrix sum of matrices \a m1 and \a m2
+ */
+
+#ifndef __DOXYGEN__
+/*
+ * The YAML data shall be a list of numerical values. Its size shall be equal
+ * to the product of the number of rows and columns of the matrix (Rows x
+ * Cols). The values shall be stored in row-major order.
+ */
+bool matrixValidateYaml(const YamlObject &obj, unsigned int size)
+{
+	if (!obj.isList())
+		return false;
+
+	if (obj.size() != size) {
+		LOG(Matrix, Error)
+			<< "Wrong number of values in matrix: expected "
+			<< size << ", got " << obj.size();
+		return false;
+	}
+
+	return true;
+}
+#endif /* __DOXYGEN__ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/matrix.h b/src/ipa/libipa/matrix.h
new file mode 100644
index 0000000..8aa8f34
--- /dev/null
+++ b/src/ipa/libipa/matrix.h
@@ -0,0 +1,204 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Matrix and related operations
+ */
+#pragma once
+
+#include <algorithm>
+#include <cmath>
+#include <sstream>
+#include <vector>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/span.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(Matrix)
+
+namespace ipa {
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int Rows, unsigned int Cols,
+	 std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, unsigned int Rows, unsigned int Cols>
+#endif /* __DOXYGEN__ */
+class Matrix
+{
+public:
+	Matrix()
+	{
+		data_.fill(static_cast<T>(0));
+	}
+
+	Matrix(const std::vector<T> &data)
+	{
+		std::copy(data.begin(), data.end(), data_.begin());
+	}
+
+	static Matrix identity()
+	{
+		Matrix ret;
+		for (size_t i = 0; i < std::min(Rows, Cols); i++)
+			ret[i][i] = static_cast<T>(1);
+		return ret;
+	}
+
+	~Matrix() = default;
+
+	const std::string toString() const
+	{
+		std::stringstream out;
+
+		out << "Matrix { ";
+		for (unsigned int i = 0; i < Rows; i++) {
+			out << "[ ";
+			for (unsigned int j = 0; j < Cols; j++) {
+				out << (*this)[i][j];
+				out << ((j + 1 < Cols) ? ", " : " ");
+			}
+			out << ((i + 1 < Rows) ? "], " : "]");
+		}
+		out << " }";
+
+		return out.str();
+	}
+
+	Span<const T, Cols> operator[](size_t i) const
+	{
+		return Span<const T, Cols>{ &data_.data()[i * Cols], Cols };
+	}
+
+	Span<T, Cols> operator[](size_t i)
+	{
+		return Span<T, Cols>{ &data_.data()[i * Cols], Cols };
+	}
+
+#ifndef __DOXYGEN__
+	template<typename U, std::enable_if_t<std::is_arithmetic_v<U>>>
+#else
+	template<typename U>
+#endif /* __DOXYGEN__ */
+	Matrix<T, Rows, Cols> &operator*=(U d)
+	{
+		for (unsigned int i = 0; i < Rows * Cols; i++)
+			data_[i] *= d;
+		return *this;
+	}
+
+private:
+	std::array<T, Rows * Cols> data_;
+};
+
+#ifndef __DOXYGEN__
+template<typename T, typename U, unsigned int Rows, unsigned int Cols,
+	 std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, typename U, unsigned int Rows, unsigned int Cols>
+#endif /* __DOXYGEN__ */
+Matrix<U, Rows, Cols> operator*(T d, const Matrix<U, Rows, Cols> &m)
+{
+	Matrix<U, Rows, Cols> result;
+
+	for (unsigned int i = 0; i < Rows; i++) {
+		for (unsigned int j = 0; j < Cols; j++)
+			result[i][j] = d * m[i][j];
+	}
+
+	return result;
+}
+
+#ifndef __DOXYGEN__
+template<typename T, typename U, unsigned int Rows, unsigned int Cols,
+	 std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, typename U, unsigned int Rows, unsigned int Cols>
+#endif /* __DOXYGEN__ */
+Matrix<U, Rows, Cols> operator*(const Matrix<U, Rows, Cols> &m, T d)
+{
+	return d * m;
+}
+
+#ifndef __DOXYGEN__
+template<typename T,
+	 unsigned int R1, unsigned int C1,
+	 unsigned int R2, unsigned int C2,
+	 std::enable_if_t<C1 == R2> * = nullptr>
+#else
+template<typename T, unsigned int R1, unsigned int C1, unsigned int R2, unsigned in C2>
+#endif /* __DOXYGEN__ */
+Matrix<T, R1, C2> operator*(const Matrix<T, R1, C1> &m1, const Matrix<T, R2, C2> &m2)
+{
+	Matrix<T, R1, C2> result;
+
+	for (unsigned int i = 0; i < R1; i++) {
+		for (unsigned int j = 0; j < C2; j++) {
+			T sum = 0;
+
+			for (unsigned int k = 0; k < C1; k++)
+				sum += m1[i][k] * m2[k][j];
+
+			result[i][j] = sum;
+		}
+	}
+
+	return result;
+}
+
+template<typename T, unsigned int Rows, unsigned int Cols>
+Matrix<T, Rows, Cols> operator+(const Matrix<T, Rows, Cols> &m1, const Matrix<T, Rows, Cols> &m2)
+{
+	Matrix<T, Rows, Cols> result;
+
+	for (unsigned int i = 0; i < Rows; i++) {
+		for (unsigned int j = 0; j < Cols; j++)
+			result[i][j] = m1[i][j] + m2[i][j];
+	}
+
+	return result;
+}
+
+#ifndef __DOXYGEN__
+bool matrixValidateYaml(const YamlObject &obj, unsigned int size);
+#endif /* __DOXYGEN__ */
+
+} /* namespace ipa */
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int Rows, unsigned int Cols>
+std::ostream &operator<<(std::ostream &out, const ipa::Matrix<T, Rows, Cols> &m)
+{
+	out << m.toString();
+	return out;
+}
+
+template<typename T, unsigned int Rows, unsigned int Cols>
+struct YamlObject::Getter<ipa::Matrix<T, Rows, Cols>> {
+	std::optional<ipa::Matrix<T, Rows, Cols>> get(const YamlObject &obj) const
+	{
+		if (!ipa::matrixValidateYaml(obj, Rows * Cols))
+			return std::nullopt;
+
+		ipa::Matrix<T, Rows, Cols> matrix;
+		T *data = &matrix[0][0];
+
+		unsigned int i = 0;
+		for (const YamlObject &entry : obj.asList()) {
+			const auto value = entry.get<T>();
+			if (!value)
+				return std::nullopt;
+
+			data[i++] = *value;
+		}
+
+		return matrix;
+	}
+};
+#endif /* __DOXYGEN__ */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/matrix_interpolator.cpp b/src/ipa/libipa/matrix_interpolator.cpp
new file mode 100644
index 0000000..04ca177
--- /dev/null
+++ b/src/ipa/libipa/matrix_interpolator.cpp
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Helper class for interpolating maps of matrices
+ */
+#include "matrix_interpolator.h"
+
+#include <algorithm>
+#include <string>
+
+#include <libcamera/base/log.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "matrix.h"
+
+/**
+ * \file matrix_interpolator.h
+ * \brief Helper class for interpolating maps of matrices
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(MatrixInterpolator)
+
+namespace ipa {
+
+/**
+ * \class MatrixInterpolator
+ * \brief Class for storing, retrieving, and interpolating matrices
+ * \tparam T Type of numerical values to be stored in the matrices
+ * \tparam R Number of rows in the matrices
+ * \tparam C Number of columns in the matrices
+ *
+ * The main use case is to pass a map from color temperatures to corresponding
+ * matrices (eg. color correction), and then requesting a matrix for a specific
+ * color temperature. This class will abstract away the interpolation portion.
+ */
+
+/**
+ * \fn MatrixInterpolator::MatrixInterpolator(const std::map<unsigned int, Matrix<T, R, C>> &matrices)
+ * \brief Construct a matrix interpolator from a map of matrices
+ * \param matrices Map from which to construct the matrix interpolator
+ */
+
+/**
+ * \fn MatrixInterpolator::reset()
+ * \brief Reset the matrix interpolator content to a single identity matrix
+ */
+
+/**
+ * \fn int MatrixInterpolator<T, R, C>::readYaml()
+ * \brief Initialize an MatrixInterpolator instance from yaml
+ * \tparam T Type of data stored in the matrices
+ * \tparam R Number of rows of the matrices
+ * \tparam C Number of columns of the matrices
+ * \param[in] yaml The yaml object that contains the map of unsigned integers to matrices
+ * \param[in] key_name The name of the key in the yaml object
+ * \param[in] matrix_name The name of the matrix in the yaml object
+ *
+ * The yaml object is expected to be a list of maps. Each map has two or more
+ * pairs: one of \a key_name to the key value (usually color temperature), and
+ * one or more of \a matrix_name to the matrix. This is a bit difficult to
+ * explain, so here is an example (in python, as it is easier to parse than
+ * yaml):
+ *       [
+ *               {
+ *                   'ct': 2860,
+ *                   'ccm': [ 2.12089, -0.52461, -0.59629,
+ *                           -0.85342,  2.80445, -0.95103,
+ *                           -0.26897, -1.14788,  2.41685 ],
+ *                   'offsets': [ 0, 0, 0 ]
+ *               },
+ *
+ *               {
+ *                   'ct': 2960,
+ *                   'ccm': [ 2.26962, -0.54174, -0.72789,
+ *                           -0.77008,  2.60271, -0.83262,
+ *                           -0.26036, -1.51254,  2.77289 ],
+ *                   'offsets': [ 0, 0, 0 ]
+ *               },
+ *
+ *               {
+ *                   'ct': 3603,
+ *                   'ccm': [ 2.18644, -0.66148, -0.52496,
+ *                           -0.77828,  2.69474, -0.91645,
+ *                           -0.25239, -0.83059,  2.08298 ],
+ *                   'offsets': [ 0, 0, 0 ]
+ *               },
+ *       ]
+ *
+ * In this case, \a key_name would be 'ct', and \a matrix_name can be either
+ * 'ccm' or 'offsets'. This way multiple matrix interpolators can be defined in
+ * one set of color temperature ranges in the tuning file, and they can be
+ * retrieved separately with the \a matrix_name parameter.
+ *
+ * \return Zero on success, negative error code otherwise
+ */
+
+/**
+ * \fn Matrix<T, R, C> MatrixInterpolator<T, R, C>::get(unsigned int key)
+ * \brief Retrieve a matrix from the list of matrices, interpolating if necessary
+ * \param[in] key The unsigned integer key of the matrix to retrieve
+ * \return The matrix corresponding to the color temperature
+ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/matrix_interpolator.h b/src/ipa/libipa/matrix_interpolator.h
new file mode 100644
index 0000000..087c4fd
--- /dev/null
+++ b/src/ipa/libipa/matrix_interpolator.h
@@ -0,0 +1,122 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Helper class for interpolating maps of matrices
+ */
+
+#pragma once
+
+#include <algorithm>
+#include <map>
+#include <string>
+#include <tuple>
+
+#include <libcamera/base/log.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "matrix.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(MatrixInterpolator)
+
+namespace ipa {
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int R, unsigned int C,
+	 std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, unsigned int R, unsigned int C>
+#endif /* __DOXYGEN__ */
+class MatrixInterpolator
+{
+public:
+	MatrixInterpolator()
+	{
+		reset();
+	}
+
+	MatrixInterpolator(const std::map<unsigned int, Matrix<T, R, C>> &matrices)
+	{
+		for (const auto &pair : matrices)
+			matrices_[pair.first] = pair.second;
+	}
+
+	~MatrixInterpolator() {}
+
+	void reset()
+	{
+		matrices_.clear();
+		matrices_[0] = Matrix<T, R, C>::identity();
+	}
+
+	int readYaml(const libcamera::YamlObject &yaml,
+		     const std::string &key_name,
+		     const std::string &matrix_name)
+	{
+		matrices_.clear();
+
+		if (!yaml.isList()) {
+			LOG(MatrixInterpolator, Error) << "yaml object must be a list";
+			return -EINVAL;
+		}
+
+		for (const auto &value : yaml.asList()) {
+			unsigned int ct = std::stoul(value[key_name].get<std::string>(""));
+			std::optional<Matrix<T, R, C>> matrix =
+				value[matrix_name].get<Matrix<T, R, C>>();
+			if (!matrix) {
+				LOG(MatrixInterpolator, Error) << "Failed to read matrix";
+				return -EINVAL;
+			}
+
+			matrices_[ct] = *matrix;
+
+			LOG(MatrixInterpolator, Debug)
+				<< "Read matrix '" << matrix_name << "' for key '"
+				<< key_name << "' " << ct << ": "
+				<< matrices_[ct].toString();
+		}
+
+		if (matrices_.size() < 1) {
+			LOG(MatrixInterpolator, Error) << "Need at least one matrix";
+			return -EINVAL;
+		}
+
+		return 0;
+	}
+
+	Matrix<T, R, C> get(unsigned int ct)
+	{
+		ASSERT(matrices_.size() > 0);
+
+		if (matrices_.size() == 1 ||
+		    ct <= matrices_.begin()->first)
+			return matrices_.begin()->second;
+
+		if (ct >= matrices_.rbegin()->first)
+			return matrices_.rbegin()->second;
+
+		if (matrices_.find(ct) != matrices_.end())
+			return matrices_[ct];
+
+		/* The above four guarantee that this will succeed */
+		auto iter = matrices_.upper_bound(ct);
+		unsigned int ctUpper = iter->first;
+		unsigned int ctLower = (--iter)->first;
+
+		double lambda = (ct - ctLower) / static_cast<double>(ctUpper - ctLower);
+		Matrix<T, R, C> ret =
+			lambda * matrices_[ctUpper] + (1.0 - lambda) * matrices_[ctLower];
+		return ret;
+	}
+
+private:
+	std::map<unsigned int, Matrix<T, R, C>> matrices_;
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/meson.build b/src/ipa/libipa/meson.build
index 016b8e0..eff8ce2 100644
--- a/src/ipa/libipa/meson.build
+++ b/src/ipa/libipa/meson.build
@@ -1,19 +1,31 @@
 # SPDX-License-Identifier: CC0-1.0
 
 libipa_headers = files([
+    'agc_mean_luminance.h',
     'algorithm.h',
     'camera_sensor_helper.h',
+    'exposure_mode_helper.h',
     'fc_queue.h',
     'histogram.h',
+    'matrix.h',
+    'matrix_interpolator.h',
     'module.h',
+    'pwl.h',
+    'vector.h',
 ])
 
 libipa_sources = files([
+    'agc_mean_luminance.cpp',
     'algorithm.cpp',
     'camera_sensor_helper.cpp',
+    'exposure_mode_helper.cpp',
     'fc_queue.cpp',
     'histogram.cpp',
+    'matrix.cpp',
+    'matrix_interpolator.cpp',
     'module.cpp',
+    'pwl.cpp',
+    'vector.cpp',
 ])
 
 libipa_includes = include_directories('..')
@@ -21,3 +33,7 @@
 libipa = static_library('ipa', [libipa_sources, libipa_headers],
                         include_directories : ipa_includes,
                         dependencies : libcamera_private)
+
+libipa_dep = declare_dependency(sources : libipa_headers,
+                                include_directories : libipa_includes,
+                                link_with : libipa)
diff --git a/src/ipa/libipa/module.cpp b/src/ipa/libipa/module.cpp
index ee01f12..64ca914 100644
--- a/src/ipa/libipa/module.cpp
+++ b/src/ipa/libipa/module.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas On Board
  *
- * module.cpp - IPA Module
+ * IPA Module
  */
 
 #include "module.h"
diff --git a/src/ipa/libipa/module.h b/src/ipa/libipa/module.h
index 4149a35..0fb5191 100644
--- a/src/ipa/libipa/module.h
+++ b/src/ipa/libipa/module.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas On Board
  *
- * module.h - IPA module
+ * IPA module
  */
 
 #pragma once
diff --git a/src/ipa/libipa/pwl.cpp b/src/ipa/libipa/pwl.cpp
new file mode 100644
index 0000000..9b21375
--- /dev/null
+++ b/src/ipa/libipa/pwl.cpp
@@ -0,0 +1,459 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ * Copyright (C) 2024, Ideas on Board Oy
+ *
+ * Piecewise linear functions
+ */
+
+#include "pwl.h"
+
+#include <assert.h>
+#include <cmath>
+#include <sstream>
+#include <stdexcept>
+
+/**
+ * \file pwl.h
+ * \brief Piecewise linear functions
+ */
+
+namespace libcamera {
+
+namespace ipa {
+
+/**
+ * \class Pwl
+ * \brief Describe a univariate piecewise linear function in two-dimensional
+ * real space
+ *
+ * A piecewise linear function is a univariate function that maps reals to
+ * reals, and it is composed of multiple straight-line segments.
+ *
+ * While a mathematical piecewise linear function would usually be defined by
+ * a list of linear functions and for which values of the domain they apply,
+ * this Pwl class is instead defined by a list of points at which these line
+ * segments intersect. These intersecting points are known as knots.
+ *
+ * https://en.wikipedia.org/wiki/Piecewise_linear_function
+ *
+ * A consequence of the Pwl class being defined by knots instead of linear
+ * functions is that the values of the piecewise linear function past the ends
+ * of the function are constants as opposed to linear functions. In a
+ * mathematical piecewise linear function that is defined by multiple linear
+ * functions, the ends of the function are also linear functions and hence grow
+ * to infinity (or negative infinity). However, since this Pwl class is defined
+ * by knots, the y-value of the leftmost and rightmost knots will hold for all
+ * x values to negative infinity and positive infinity, respectively.
+ */
+
+/**
+ * \typedef Pwl::Point
+ * \brief Describe a point in two-dimensional real space
+ */
+
+/**
+ * \class Pwl::Interval
+ * \brief Describe an interval in one-dimensional real space
+ */
+
+/**
+ * \fn Pwl::Interval::Interval(double _start, double _end)
+ * \brief Construct an interval
+ * \param[in] _start Start of the interval
+ * \param[in] _end End of the interval
+ */
+
+/**
+ * \fn Pwl::Interval::contains
+ * \brief Check if a given value falls within the interval
+ * \param[in] value Value to check
+ * \return True if the value falls within the interval, including its bounds,
+ * or false otherwise
+ */
+
+/**
+ * \fn Pwl::Interval::clamp
+ * \brief Clamp a value such that it is within the interval
+ * \param[in] value Value to clamp
+ * \return The clamped value
+ */
+
+/**
+ * \fn Pwl::Interval::length
+ * \brief Compute the length of the interval
+ * \return The length of the interval
+ */
+
+/**
+ * \var Pwl::Interval::start
+ * \brief Start of the interval
+ */
+
+/**
+ * \var Pwl::Interval::end
+ * \brief End of the interval
+ */
+
+/**
+ * \brief Construct an empty piecewise linear function
+ */
+Pwl::Pwl()
+{
+}
+
+/**
+ * \brief Construct a piecewise linear function from a list of 2D points
+ * \param[in] points Vector of points from which to construct the piecewise
+ * linear function
+ *
+ * \a points must be in ascending order of x-value.
+ */
+Pwl::Pwl(const std::vector<Point> &points)
+	: points_(points)
+{
+}
+
+/**
+ * \copydoc Pwl::Pwl(const std::vector<Point> &points)
+ *
+ * The contents of the \a points vector is moved to the newly constructed Pwl
+ * instance.
+ */
+Pwl::Pwl(std::vector<Point> &&points)
+	: points_(std::move(points))
+{
+}
+
+/**
+ * \brief Append a point to the end of the piecewise linear function
+ * \param[in] x x-coordinate of the point to add to the piecewise linear function
+ * \param[in] y y-coordinate of the point to add to the piecewise linear function
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * The point's x-coordinate must be greater than the x-coordinate of the last
+ * (= greatest) point already in the piecewise linear function.
+ */
+void Pwl::append(double x, double y, const double eps)
+{
+	if (points_.empty() || points_.back().x() + eps < x)
+		points_.push_back(Point({ x, y }));
+}
+
+/**
+ * \brief Prepend a point to the beginning of the piecewise linear function
+ * \param[in] x x-coordinate of the point to add to the piecewise linear function
+ * \param[in] y y-coordinate of the point to add to the piecewise linear function
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * The point's x-coordinate must be less than the x-coordinate of the first
+ * (= smallest) point already in the piecewise linear function.
+ */
+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 }));
+}
+
+/**
+ * \fn Pwl::empty() const
+ * \brief Check if the piecewise linear function is empty
+ * \return True if there are no points in the function, false otherwise
+ */
+
+/**
+ * \fn Pwl::size() const
+ * \brief Retrieve the number of points in the piecewise linear function
+ * \return The number of points in the piecewise linear function
+ */
+
+/**
+ * \brief Get the domain of the piecewise linear function
+ * \return An interval representing the domain
+ */
+Pwl::Interval Pwl::domain() const
+{
+	return Interval(points_[0].x(), points_[points_.size() - 1].x());
+}
+
+/**
+ * \brief Get the range of the piecewise linear function
+ * \return An interval representing the range
+ */
+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);
+}
+
+/**
+ * \brief Evaluate the piecewise linear function
+ * \param[in] x The x value to input into the function
+ * \param[inout] span Initial guess for span
+ * \param[in] updateSpan Set to true to update span
+ *
+ * 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.
+ *
+ *  \return The result of evaluating the piecewise linear function at position \a x
+ */
+double Pwl::eval(double x, int *span, bool updateSpan) const
+{
+	int index = findSpan(x, span && *span != -1
+					? *span
+					: points_.size() / 2 - 1);
+	if (span && updateSpan)
+		*span = index;
+	return points_[index].y() +
+	       (x - points_[index].x()) * (points_[index + 1].y() - points_[index].y()) /
+		       (points_[index + 1].x() - points_[index].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;
+}
+
+/**
+ * \brief Compute the inverse function
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * The output includes whether the resulting inverse function is a proper
+ * (true) inverse, or only a best effort (e.g. input was non-monotonic).
+ *
+ * \return A pair of the inverse piecewise linear function, and whether or not
+ * the result is a proper/true inverse
+ */
+std::pair<Pwl, bool> Pwl::inverse(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.
+	 */
+	bool trueInverse = !(neither || (appended && prepended));
+
+	return { inverse, trueInverse };
+}
+
+/**
+ * \brief Compose two piecewise linear functions together
+ * \param[in] other The "other" piecewise linear function
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * The "this" function is done first, and "other" after.
+ *
+ * \return The composed piecewise linear function
+ */
+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({ Point({ 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;
+}
+
+/**
+ * \brief Apply function to (x, y) values at every control point
+ * \param[in] f Function to be applied
+ */
+void Pwl::map(std::function<void(double x, double y)> f) const
+{
+	for (auto &pt : points_)
+		f(pt.x(), pt.y());
+}
+
+/**
+ * \brief Apply function to (x, y0, y1) values wherever either Pwl has a
+ * control point.
+ * \param[in] pwl0 First piecewise linear function
+ * \param[in] pwl1 Second piecewise linear function
+ * \param[in] f Function to be applied
+ *
+ * This applies the function \a f to every parameter (x, y0, y1), where x is
+ * the combined list of x-values from \a pwl0 and \a pwl1, y0 is the y-value
+ * for the given x in \a pwl0, and y1 is the y-value for the same x in \a pwl1.
+ */
+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));
+	}
+}
+
+/**
+ * \brief Combine two Pwls
+ * \param[in] pwl0 First piecewise linear function
+ * \param[in] pwl1 Second piecewise linear function
+ * \param[in] f Function to be applied
+ * \param[in] eps Epsilon for the minimum x distance between points (optional)
+ *
+ * Create a new Pwl where the y values are given by running \a f wherever
+ * either pwl has a knot.
+ *
+ * \return The combined pwl
+ */
+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;
+}
+
+/**
+ * \brief Multiply the piecewise linear function
+ * \param[in] d Scalar multiplier to multiply the function by
+ * \return This function, after it has been multiplied by \a d
+ */
+Pwl &Pwl::operator*=(double d)
+{
+	for (auto &pt : points_)
+		pt[1] *= d;
+	return *this;
+}
+
+/**
+ * \brief Assemble and return a string describing the piecewise linear function
+ * \return A string describing the piecewise linear function
+ */
+std::string Pwl::toString() const
+{
+	std::stringstream ss;
+	ss << "Pwl { ";
+	for (auto &p : points_)
+		ss << "(" << p.x() << ", " << p.y() << ") ";
+	ss << "}";
+	return ss.str();
+}
+
+} /* namespace ipa */
+
+#ifndef __DOXYGEN__
+/*
+ * The YAML data shall be a list of numerical values with an even number of
+ * elements. They are parsed in pairs into x and y points in the piecewise
+ * linear function, and added in order. x must be monotonically increasing.
+ */
+template<>
+std::optional<ipa::Pwl>
+YamlObject::Getter<ipa::Pwl>::get(const YamlObject &obj) const
+{
+	if (!obj.size() || obj.size() % 2)
+		return std::nullopt;
+
+	ipa::Pwl pwl;
+
+	const auto &list = obj.asList();
+
+	for (auto it = list.begin(); it != list.end(); it++) {
+		auto x = it->get<double>();
+		if (!x)
+			return std::nullopt;
+		auto y = (++it)->get<double>();
+		if (!y)
+			return std::nullopt;
+
+		pwl.append(*x, *y);
+	}
+
+	if (pwl.size() != obj.size() / 2)
+		return std::nullopt;
+
+	return pwl;
+}
+#endif /* __DOXYGEN__ */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/pwl.h b/src/ipa/libipa/pwl.h
new file mode 100644
index 0000000..b6f9349
--- /dev/null
+++ b/src/ipa/libipa/pwl.h
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2019, Raspberry Pi Ltd
+ *
+ * Piecewise linear functions interface
+ */
+#pragma once
+
+#include <algorithm>
+#include <cmath>
+#include <functional>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "vector.h"
+
+namespace libcamera {
+
+namespace ipa {
+
+class Pwl
+{
+public:
+	using Point = Vector<double, 2>;
+
+	struct Interval {
+		Interval(double _start, double _end)
+			: start(_start), end(_end) {}
+
+		bool contains(double value)
+		{
+			return value >= start && value <= end;
+		}
+
+		double clamp(double value)
+		{
+			return std::clamp(value, start, end);
+		}
+
+		double length() const { return end - start; }
+
+		double start, end;
+	};
+
+	Pwl();
+	Pwl(const std::vector<Point> &points);
+	Pwl(std::vector<Point> &&points);
+
+	void append(double x, double y, double eps = 1e-6);
+
+	bool empty() const { return points_.empty(); }
+	size_t size() const { return points_.size(); }
+
+	Interval domain() const;
+	Interval range() const;
+
+	double eval(double x, int *span = nullptr,
+		    bool updateSpan = true) const;
+
+	std::pair<Pwl, bool> inverse(double eps = 1e-6) const;
+	Pwl compose(const Pwl &other, double eps = 1e-6) const;
+
+	void map(std::function<void(double x, double y)> f) const;
+
+	static Pwl
+	combine(const Pwl &pwl0, const Pwl &pwl1,
+		std::function<double(double x, double y0, double y1)> f,
+		double eps = 1e-6);
+
+	Pwl &operator*=(double d);
+
+	std::string toString() const;
+
+private:
+	static void map2(const Pwl &pwl0, const Pwl &pwl1,
+			 std::function<void(double x, double y0, double y1)> f);
+	void prepend(double x, double y, double eps = 1e-6);
+	int findSpan(double x, int span) const;
+
+	std::vector<Point> points_;
+};
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/vector.cpp b/src/ipa/libipa/vector.cpp
new file mode 100644
index 0000000..bd00b01
--- /dev/null
+++ b/src/ipa/libipa/vector.cpp
@@ -0,0 +1,168 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Vector and related operations
+ */
+
+#include "vector.h"
+
+#include <libcamera/base/log.h>
+
+/**
+ * \file vector.h
+ * \brief Vector class
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(Vector)
+
+namespace ipa {
+
+/**
+ * \class Vector
+ * \brief Vector class
+ * \tparam T Type of numerical values to be stored in the vector
+ * \tparam Rows Number of dimension of the vector (= number of elements)
+ */
+
+/**
+ * \fn Vector::Vector()
+ * \brief Construct a zero vector
+ */
+
+/**
+ * \fn Vector::Vector(const std::array<T, Rows> &data)
+ * \brief Construct vector from supplied data
+ * \param data Data from which to construct a vector
+ *
+ * The size of \a data must be equal to the dimension size Rows of the vector.
+ */
+
+/**
+ * \fn T Vector::operator[](size_t i) const
+ * \brief Index to an element in the vector
+ * \param i Index of element to retrieve
+ * \return Element at index \a i from the vector
+ */
+
+/**
+ * \fn T &Vector::operator[](size_t i)
+ * \copydoc Vector::operator[](size_t i) const
+ */
+
+/**
+ * \fn Vector::x()
+ * \brief Convenience function to access the first element of the vector
+ * \return The first element of the vector
+ */
+
+/**
+ * \fn Vector::y()
+ * \brief Convenience function to access the second element of the vector
+ * \return The second element of the vector
+ */
+
+/**
+ * \fn Vector::z()
+ * \brief Convenience function to access the third element of the vector
+ * \return The third element of the vector
+ */
+
+/**
+ * \fn Vector::operator-() const
+ * \brief Negate a Vector by negating both all of its coordinates
+ * \return The negated vector
+ */
+
+/**
+ * \fn Vector::operator-(Vector const &other) const
+ * \brief Subtract one vector from another
+ * \param[in] other The other vector
+ * \return The difference of \a other from this vector
+ */
+
+/**
+ * \fn Vector::operator+()
+ * \brief Add two vectors together
+ * \param[in] other The other vector
+ * \return The sum of the two vectors
+ */
+
+/**
+ * \fn Vector::operator*(const Vector<T, Rows> &other) const
+ * \brief Compute the dot product
+ * \param[in] other The other vector
+ * \return The dot product of the two vectors
+ */
+
+/**
+ * \fn Vector::operator*(T factor) const
+ * \brief Multiply the vector by a scalar
+ * \param[in] factor The factor
+ * \return The vector multiplied by \a factor
+ */
+
+/**
+ * \fn Vector::operator/()
+ * \brief Divide the vector by a scalar
+ * \param[in] factor The factor
+ * \return The vector divided by \a factor
+ */
+
+/**
+ * \fn Vector::length2()
+ * \brief Get the squared length of the vector
+ * \return The squared length of the vector
+ */
+
+/**
+ * \fn Vector::length()
+ * \brief Get the length of the vector
+ * \return The length of the vector
+ */
+
+/**
+ * \fn Vector<T, Rows> operator*(const Matrix<T, Rows, Cols> &m, const Vector<T, Cols> &v)
+ * \brief Multiply a matrix by a vector
+ * \tparam T Numerical type of the contents of the matrix and vector
+ * \tparam Rows The number of rows in the matrix
+ * \tparam Cols The number of columns in the matrix (= rows in the vector)
+ * \param m The matrix
+ * \param v The vector
+ * \return Product of matrix \a m and vector \a v
+ */
+
+/**
+ * \fn bool operator==(const Vector<T, Rows> &lhs, const Vector<T, Rows> &rhs)
+ * \brief Compare vectors for equality
+ * \return True if the two vectors are equal, false otherwise
+ */
+
+/**
+ * \fn bool operator!=(const Vector<T, Rows> &lhs, const Vector<T, Rows> &rhs)
+ * \brief Compare vectors for inequality
+ * \return True if the two vectors are not equal, false otherwise
+ */
+
+#ifndef __DOXYGEN__
+bool vectorValidateYaml(const YamlObject &obj, unsigned int size)
+{
+	if (!obj.isList())
+		return false;
+
+	if (obj.size() != size) {
+		LOG(Vector, Error)
+			<< "Wrong number of values in YAML vector: expected "
+			<< size << ", got " << obj.size();
+		return false;
+	}
+
+	return true;
+}
+#endif /* __DOXYGEN__ */
+
+} /* namespace ipa */
+
+} /* namespace libcamera */
diff --git a/src/ipa/libipa/vector.h b/src/ipa/libipa/vector.h
new file mode 100644
index 0000000..556e096
--- /dev/null
+++ b/src/ipa/libipa/vector.h
@@ -0,0 +1,219 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Vector and related operations
+ */
+#pragma once
+
+#include <algorithm>
+#include <array>
+#include <cmath>
+#include <sstream>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/span.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "matrix.h"
+
+namespace libcamera {
+
+LOG_DECLARE_CATEGORY(Vector)
+
+namespace ipa {
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int Rows,
+	 std::enable_if_t<std::is_arithmetic_v<T>> * = nullptr>
+#else
+template<typename T, unsigned int Rows>
+#endif /* __DOXYGEN__ */
+class Vector
+{
+public:
+	constexpr Vector() = default;
+
+	constexpr Vector(const std::array<T, Rows> &data)
+	{
+		for (unsigned int i = 0; i < Rows; i++)
+			data_[i] = data[i];
+	}
+
+	const T &operator[](size_t i) const
+	{
+		ASSERT(i < data_.size());
+		return data_[i];
+	}
+
+	T &operator[](size_t i)
+	{
+		ASSERT(i < data_.size());
+		return data_[i];
+	}
+
+#ifndef __DOXYGEN__
+	template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 1>>
+#endif /* __DOXYGEN__ */
+	constexpr T x() const
+	{
+		return data_[0];
+	}
+
+#ifndef __DOXYGEN__
+	template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 2>>
+#endif /* __DOXYGEN__ */
+	constexpr T y() const
+	{
+		return data_[1];
+	}
+
+#ifndef __DOXYGEN__
+	template<bool Dependent = false, typename = std::enable_if_t<Dependent || Rows >= 3>>
+#endif /* __DOXYGEN__ */
+	constexpr T z() const
+	{
+		return data_[2];
+	}
+
+	constexpr Vector<T, Rows> operator-() const
+	{
+		Vector<T, Rows> ret;
+		for (unsigned int i = 0; i < Rows; i++)
+			ret[i] = -data_[i];
+		return ret;
+	}
+
+	constexpr Vector<T, Rows> operator-(const Vector<T, Rows> &other) const
+	{
+		Vector<T, Rows> ret;
+		for (unsigned int i = 0; i < Rows; i++)
+			ret[i] = data_[i] - other[i];
+		return ret;
+	}
+
+	constexpr Vector<T, Rows> operator+(const Vector<T, Rows> &other) const
+	{
+		Vector<T, Rows> ret;
+		for (unsigned int i = 0; i < Rows; i++)
+			ret[i] = data_[i] + other[i];
+		return ret;
+	}
+
+	constexpr T operator*(const Vector<T, Rows> &other) const
+	{
+		T ret = 0;
+		for (unsigned int i = 0; i < Rows; i++)
+			ret += data_[i] * other[i];
+		return ret;
+	}
+
+	constexpr Vector<T, Rows> operator*(T factor) const
+	{
+		Vector<T, Rows> ret;
+		for (unsigned int i = 0; i < Rows; i++)
+			ret[i] = data_[i] * factor;
+		return ret;
+	}
+
+	constexpr Vector<T, Rows> operator/(T factor) const
+	{
+		Vector<T, Rows> ret;
+		for (unsigned int i = 0; i < Rows; i++)
+			ret[i] = data_[i] / factor;
+		return ret;
+	}
+
+	constexpr double length2() const
+	{
+		double ret = 0;
+		for (unsigned int i = 0; i < Rows; i++)
+			ret += data_[i] * data_[i];
+		return ret;
+	}
+
+	constexpr double length() const
+	{
+		return std::sqrt(length2());
+	}
+
+private:
+	std::array<T, Rows> data_;
+};
+
+template<typename T, unsigned int Rows, unsigned int Cols>
+Vector<T, Rows> operator*(const Matrix<T, Rows, Cols> &m, const Vector<T, Cols> &v)
+{
+	Vector<T, Rows> result;
+
+	for (unsigned int i = 0; i < Rows; i++) {
+		T sum = 0;
+		for (unsigned int j = 0; j < Cols; j++)
+			sum += m[i][j] * v[j];
+		result[i] = sum;
+	}
+
+	return result;
+}
+
+template<typename T, unsigned int Rows>
+bool operator==(const Vector<T, Rows> &lhs, const Vector<T, Rows> &rhs)
+{
+	for (unsigned int i = 0; i < Rows; i++) {
+		if (lhs[i] != rhs[i])
+			return false;
+	}
+
+	return true;
+}
+
+template<typename T, unsigned int Rows>
+bool operator!=(const Vector<T, Rows> &lhs, const Vector<T, Rows> &rhs)
+{
+	return !(lhs == rhs);
+}
+
+#ifndef __DOXYGEN__
+bool vectorValidateYaml(const YamlObject &obj, unsigned int size);
+#endif /* __DOXYGEN__ */
+
+} /* namespace ipa */
+
+#ifndef __DOXYGEN__
+template<typename T, unsigned int Rows>
+std::ostream &operator<<(std::ostream &out, const ipa::Vector<T, Rows> &v)
+{
+	out << "Vector { ";
+	for (unsigned int i = 0; i < Rows; i++) {
+		out << v[i];
+		out << ((i + 1 < Rows) ? ", " : " ");
+	}
+	out << " }";
+
+	return out;
+}
+
+template<typename T, unsigned int Rows>
+struct YamlObject::Getter<ipa::Vector<T, Rows>> {
+	std::optional<ipa::Vector<T, Rows>> get(const YamlObject &obj) const
+	{
+		if (!ipa::vectorValidateYaml(obj, Rows))
+			return std::nullopt;
+
+		ipa::Vector<T, Rows> vector;
+
+		unsigned int i = 0;
+		for (const YamlObject &entry : obj.asList()) {
+			const auto value = entry.get<T>();
+			if (!value)
+				return std::nullopt;
+			vector[i++] = *value;
+		}
+
+		return vector;
+	}
+};
+#endif /* __DOXYGEN__ */
+
+} /* namespace libcamera */
diff --git a/src/ipa/meson.build b/src/ipa/meson.build
index 903eb52..0ad4631 100644
--- a/src/ipa/meson.build
+++ b/src/ipa/meson.build
@@ -41,6 +41,9 @@
 
 subdirs = []
 foreach pipeline : pipelines
+    # The current implementation expects the IPA module name to match the
+    # pipeline name.
+    # \todo Make the IPA naming scheme more flexible.
     if not ipa_modules.contains(pipeline)
         continue
     endif
@@ -72,5 +75,6 @@
     # install time, which invalidates the signatures.
     meson.add_install_script('ipa-sign-install.sh',
                              ipa_priv_key.full_path(),
-                             enabled_ipa_modules)
+                             enabled_ipa_modules,
+                             install_tag : 'runtime')
 endif
diff --git a/src/ipa/rkisp1/algorithms/agc.cpp b/src/ipa/rkisp1/algorithms/agc.cpp
index e5aeb34..f12f8b6 100644
--- a/src/ipa/rkisp1/algorithms/agc.cpp
+++ b/src/ipa/rkisp1/algorithms/agc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * agc.cpp - AGC/AEC mean-based control algorithm
+ * AGC/AEC mean-based control algorithm
  */
 
 #include "agc.h"
@@ -10,6 +10,8 @@
 #include <algorithm>
 #include <chrono>
 #include <cmath>
+#include <tuple>
+#include <vector>
 
 #include <libcamera/base/log.h>
 #include <libcamera/base/utils.h>
@@ -17,6 +19,8 @@
 #include <libcamera/control_ids.h>
 #include <libcamera/ipa/core_ipa_interface.h>
 
+#include "libcamera/internal/yaml_parser.h"
+
 #include "libipa/histogram.h"
 
 /**
@@ -36,35 +40,121 @@
 
 LOG_DEFINE_CATEGORY(RkISP1Agc)
 
-/* Minimum limit for analogue gain value */
-static constexpr double kMinAnalogueGain = 1.0;
+int Agc::parseMeteringModes(IPAContext &context, const YamlObject &tuningData)
+{
+	if (!tuningData.isDictionary())
+		LOG(RkISP1Agc, Warning)
+			<< "'AeMeteringMode' parameter not found in tuning file";
 
-/* \todo Honour the FrameDurationLimits control instead of hardcoding a limit */
-static constexpr utils::Duration kMaxShutterSpeed = 60ms;
+	for (const auto &[key, value] : tuningData.asDict()) {
+		if (controls::AeMeteringModeNameValueMap.find(key) ==
+		    controls::AeMeteringModeNameValueMap.end()) {
+			LOG(RkISP1Agc, Warning)
+				<< "Skipping unknown metering mode '" << key << "'";
+			continue;
+		}
 
-/* Number of frames to wait before calculating stats on minimum exposure */
-static constexpr uint32_t kNumStartupFrames = 10;
+		std::vector<uint8_t> weights =
+			value.getList<uint8_t>().value_or(std::vector<uint8_t>{});
+		if (weights.size() != context.hw->numHistogramWeights) {
+			LOG(RkISP1Agc, Warning)
+				<< "Failed to read metering mode'" << key << "'";
+			continue;
+		}
 
-/* Target value to reach for the top 2% of the histogram */
-static constexpr double kEvGainTarget = 0.5;
+		meteringModes_[controls::AeMeteringModeNameValueMap.at(key)] = weights;
+	}
 
-/*
- * Relative luminance target.
- *
- * It's a number that's chosen so that, when the camera points at a grey
- * target, the resulting image brightness is considered right.
- *
- * \todo Why is the value different between IPU3 and RkISP1 ?
- */
-static constexpr double kRelativeLuminanceTarget = 0.4;
+	if (meteringModes_.empty()) {
+		LOG(RkISP1Agc, Warning)
+			<< "No metering modes read from tuning file; defaulting to matrix";
+		int32_t meteringModeId = controls::AeMeteringModeNameValueMap.at("MeteringMatrix");
+		std::vector<uint8_t> weights(context.hw->numHistogramWeights, 1);
+
+		meteringModes_[meteringModeId] = weights;
+	}
+
+	std::vector<ControlValue> meteringModes;
+	std::vector<int> meteringModeKeys = utils::map_keys(meteringModes_);
+	std::transform(meteringModeKeys.begin(), meteringModeKeys.end(),
+		       std::back_inserter(meteringModes),
+		       [](int x) { return ControlValue(x); });
+	context.ctrlMap[&controls::AeMeteringMode] = ControlInfo(meteringModes);
+
+	return 0;
+}
+
+uint8_t Agc::computeHistogramPredivider(const Size &size,
+					enum rkisp1_cif_isp_histogram_mode mode)
+{
+	/*
+	 * The maximum number of pixels that could potentially be in one bin is
+	 * if all the pixels of the image are in it, multiplied by 3 for the
+	 * three color channels. The counter for each bin is 16 bits wide, so
+	 * `factor` thus contains the number of times we'd wrap around. This is
+	 * obviously the number of pixels that we need to skip to make sure
+	 * that we don't wrap around, but we compute the square root of it
+	 * instead, as the skip that we need to program is for both the x and y
+	 * directions.
+	 *
+	 * Even though it looks like dividing into a counter of 65536 would
+	 * overflow by 1, this is apparently fine according to the hardware
+	 * documentation, and this successfully gets the expected documented
+	 * predivider size for cases where:
+	 * (width / predivider) * (height / predivider) * 3 == 65536.
+	 *
+	 * There's a bit of extra rounding math to make sure the rounding goes
+	 * the correct direction so that the square of the step is big enough
+	 * to encompass the `factor` number of pixels that we need to skip.
+	 *
+	 * \todo Take into account weights. That is, if the weights are low
+	 * enough we can potentially reduce the predivider to increase
+	 * precision. This needs some investigation however, as this hardware
+	 * behavior is undocumented and is only an educated guess.
+	 */
+	int count = mode == RKISP1_CIF_ISP_HISTOGRAM_MODE_RGB_COMBINED ? 3 : 1;
+	double factor = size.width * size.height * count / 65536.0;
+	double root = std::sqrt(factor);
+	uint8_t predivider = static_cast<uint8_t>(std::ceil(root));
+
+	return std::clamp<uint8_t>(predivider, 3, 127);
+}
 
 Agc::Agc()
-	: frameCount_(0), numCells_(0), numHistBins_(0), filteredExposure_(0s)
 {
 	supportsRaw_ = true;
 }
 
 /**
+ * \brief Initialise the AGC algorithm from tuning files
+ * \param[in] context The shared IPA context
+ * \param[in] tuningData The YamlObject containing Agc tuning data
+ *
+ * This function calls the base class' tuningData parsers to discover which
+ * control values are supported.
+ *
+ * \return 0 on success or errors from the base class
+ */
+int Agc::init(IPAContext &context, const YamlObject &tuningData)
+{
+	int ret;
+
+	ret = parseTuningData(tuningData);
+	if (ret)
+		return ret;
+
+	const YamlObject &yamlMeteringModes = tuningData["AeMeteringMode"];
+	ret = parseMeteringModes(context, yamlMeteringModes);
+	if (ret)
+		return ret;
+
+	context.ctrlMap[&controls::AeEnable] = ControlInfo(false, true);
+	context.ctrlMap.merge(controls());
+
+	return 0;
+}
+
+/**
  * \brief Configure the AGC given a configInfo
  * \param[in] context The shared IPA context
  * \param[in] configInfo The IPA configuration data
@@ -81,18 +171,19 @@
 	context.activeState.agc.manual.exposure = context.activeState.agc.automatic.exposure;
 	context.activeState.agc.autoEnabled = !context.configuration.raw;
 
+	context.activeState.agc.constraintMode =
+		static_cast<controls::AeConstraintModeEnum>(constraintModes().begin()->first);
+	context.activeState.agc.exposureMode =
+		static_cast<controls::AeExposureModeEnum>(exposureModeHelpers().begin()->first);
+	context.activeState.agc.meteringMode =
+		static_cast<controls::AeMeteringModeEnum>(meteringModes_.begin()->first);
+
 	/*
-	 * According to the RkISP1 documentation:
-	 * - versions < V12 have RKISP1_CIF_ISP_AE_MEAN_MAX_V10 entries,
-	 * - versions >= V12 have RKISP1_CIF_ISP_AE_MEAN_MAX_V12 entries.
+	 * \todo This should probably come from FrameDurationLimits instead,
+	 * except it's computed in the IPA and not here so we'd have to
+	 * recompute it.
 	 */
-	if (context.configuration.hw.revision < RKISP1_V12) {
-		numCells_ = RKISP1_CIF_ISP_AE_MEAN_MAX_V10;
-		numHistBins_ = RKISP1_CIF_ISP_HIST_BIN_N_MAX_V10;
-	} else {
-		numCells_ = RKISP1_CIF_ISP_AE_MEAN_MAX_V12;
-		numHistBins_ = RKISP1_CIF_ISP_HIST_BIN_N_MAX_V12;
-	}
+	context.activeState.agc.maxFrameDuration = context.configuration.sensor.maxShutterSpeed;
 
 	/*
 	 * Define the measurement window for AGC as a centered rectangle
@@ -103,11 +194,13 @@
 	context.configuration.agc.measureWindow.h_size = 3 * configInfo.outputSize.width / 4;
 	context.configuration.agc.measureWindow.v_size = 3 * configInfo.outputSize.height / 4;
 
-	/*
-	 * \todo Use the upcoming per-frame context API that will provide a
-	 * frame index
-	 */
-	frameCount_ = 0;
+	setLimits(context.configuration.sensor.minShutterSpeed,
+		  context.configuration.sensor.maxShutterSpeed,
+		  context.configuration.sensor.minAnalogueGain,
+		  context.configuration.sensor.maxAnalogueGain);
+
+	resetFrameCount();
+
 	return 0;
 }
 
@@ -154,6 +247,34 @@
 		frameContext.agc.exposure = agc.manual.exposure;
 		frameContext.agc.gain = agc.manual.gain;
 	}
+
+	const auto &meteringMode = controls.get(controls::AeMeteringMode);
+	if (meteringMode) {
+		frameContext.agc.updateMetering = agc.meteringMode != *meteringMode;
+		agc.meteringMode =
+			static_cast<controls::AeMeteringModeEnum>(*meteringMode);
+	}
+	frameContext.agc.meteringMode = agc.meteringMode;
+
+	const auto &exposureMode = controls.get(controls::AeExposureMode);
+	if (exposureMode)
+		agc.exposureMode =
+			static_cast<controls::AeExposureModeEnum>(*exposureMode);
+	frameContext.agc.exposureMode = agc.exposureMode;
+
+	const auto &constraintMode = controls.get(controls::AeConstraintMode);
+	if (constraintMode)
+		agc.constraintMode =
+			static_cast<controls::AeConstraintModeEnum>(*constraintMode);
+	frameContext.agc.constraintMode = agc.constraintMode;
+
+	const auto &frameDurationLimits = controls.get(controls::FrameDurationLimits);
+	if (frameDurationLimits) {
+		utils::Duration maxFrameDuration =
+			std::chrono::milliseconds((*frameDurationLimits).back());
+		agc.maxFrameDuration = maxFrameDuration;
+	}
+	frameContext.agc.maxFrameDuration = agc.maxFrameDuration;
 }
 
 /**
@@ -167,7 +288,7 @@
 		frameContext.agc.gain = context.activeState.agc.automatic.gain;
 	}
 
-	if (frame > 0)
+	if (frame > 0 && !frameContext.agc.updateMetering)
 		return;
 
 	/* Configure the measurement window. */
@@ -185,11 +306,20 @@
 	params->meas.hst_config.meas_window = context.configuration.agc.measureWindow;
 	/* Produce the luminance histogram. */
 	params->meas.hst_config.mode = RKISP1_CIF_ISP_HISTOGRAM_MODE_Y_HISTOGRAM;
+
 	/* Set an average weighted histogram. */
-	for (unsigned int histBin = 0; histBin < numHistBins_; histBin++)
-		params->meas.hst_config.hist_weight[histBin] = 1;
-	/* Step size can't be less than 3. */
-	params->meas.hst_config.histogram_predivider = 4;
+	Span<uint8_t> weights{
+		params->meas.hst_config.hist_weight,
+		context.hw->numHistogramWeights
+	};
+	std::vector<uint8_t> &modeWeights = meteringModes_.at(frameContext.agc.meteringMode);
+	std::copy(modeWeights.begin(), modeWeights.end(), weights.begin());
+
+	struct rkisp1_cif_isp_window window = params->meas.hst_config.meas_window;
+	Size windowSize = { window.h_size, window.v_size };
+	params->meas.hst_config.histogram_predivider =
+		computeHistogramPredivider(windowSize,
+					   static_cast<rkisp1_cif_isp_histogram_mode>(params->meas.hst_config.mode));
 
 	/* Update the configuration for histogram. */
 	params->module_cfg_update |= RKISP1_CIF_ISP_MODULE_HST;
@@ -198,127 +328,29 @@
 	params->module_en_update |= RKISP1_CIF_ISP_MODULE_HST;
 }
 
-/**
- * \brief Apply a filter on the exposure value to limit the speed of changes
- * \param[in] exposureValue The target exposure from the AGC algorithm
- *
- * The speed of the filter is adaptive, and will produce the target quicker
- * during startup, or when the target exposure is within 20% of the most recent
- * filter output.
- *
- * \return The filtered exposure
- */
-utils::Duration Agc::filterExposure(utils::Duration exposureValue)
+void Agc::fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
+		       ControlList &metadata)
 {
-	double speed = 0.2;
+	utils::Duration exposureTime = context.configuration.sensor.lineDuration
+				     * frameContext.sensor.exposure;
+	metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
+	metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
+	metadata.set(controls::AeEnable, frameContext.agc.autoEnabled);
 
-	/* Adapt instantly if we are in startup phase. */
-	if (frameCount_ < kNumStartupFrames)
-		speed = 1.0;
+	/* \todo Use VBlank value calculated from each frame exposure. */
+	uint32_t vTotal = context.configuration.sensor.size.height
+			+ context.configuration.sensor.defVBlank;
+	utils::Duration frameDuration = context.configuration.sensor.lineDuration
+				      * vTotal;
+	metadata.set(controls::FrameDuration, frameDuration.get<std::micro>());
 
-	/*
-	 * If we are close to the desired result, go faster to avoid making
-	 * multiple micro-adjustments.
-	 * \todo Make this customisable?
-	 */
-	if (filteredExposure_ < 1.2 * exposureValue &&
-	    filteredExposure_ > 0.8 * exposureValue)
-		speed = sqrt(speed);
-
-	filteredExposure_ = speed * exposureValue +
-			    filteredExposure_ * (1.0 - speed);
-
-	LOG(RkISP1Agc, Debug) << "After filtering, exposure " << filteredExposure_;
-
-	return filteredExposure_;
-}
-
-/**
- * \brief Estimate the new exposure and gain values
- * \param[inout] context The shared IPA Context
- * \param[in] frameContext The FrameContext for this frame
- * \param[in] yGain The gain calculated on the current brightness level
- * \param[in] iqMeanGain The gain calculated based on the relative luminance target
- */
-void Agc::computeExposure(IPAContext &context, IPAFrameContext &frameContext,
-			  double yGain, double iqMeanGain)
-{
-	IPASessionConfiguration &configuration = context.configuration;
-	IPAActiveState &activeState = context.activeState;
-
-	/* Get the effective exposure and gain applied on the sensor. */
-	uint32_t exposure = frameContext.sensor.exposure;
-	double analogueGain = frameContext.sensor.gain;
-
-	/* Use the highest of the two gain estimates. */
-	double evGain = std::max(yGain, iqMeanGain);
-
-	utils::Duration minShutterSpeed = configuration.sensor.minShutterSpeed;
-	utils::Duration maxShutterSpeed = std::min(configuration.sensor.maxShutterSpeed,
-						   kMaxShutterSpeed);
-
-	double minAnalogueGain = std::max(configuration.sensor.minAnalogueGain,
-					  kMinAnalogueGain);
-	double maxAnalogueGain = configuration.sensor.maxAnalogueGain;
-
-	/* Consider within 1% of the target as correctly exposed. */
-	if (utils::abs_diff(evGain, 1.0) < 0.01)
-		return;
-
-	/* extracted from Rpi::Agc::computeTargetExposure. */
-
-	/* Calculate the shutter time in seconds. */
-	utils::Duration currentShutter = exposure * configuration.sensor.lineDuration;
-
-	/*
-	 * Update the exposure value for the next computation using the values
-	 * of exposure and gain really used by the sensor.
-	 */
-	utils::Duration effectiveExposureValue = currentShutter * analogueGain;
-
-	LOG(RkISP1Agc, Debug) << "Actual total exposure " << currentShutter * analogueGain
-			      << " Shutter speed " << currentShutter
-			      << " Gain " << analogueGain
-			      << " Needed ev gain " << evGain;
-
-	/*
-	 * Calculate the current exposure value for the scene as the latest
-	 * exposure value applied multiplied by the new estimated gain.
-	 */
-	utils::Duration exposureValue = effectiveExposureValue * evGain;
-
-	/* Clamp the exposure value to the min and max authorized. */
-	utils::Duration maxTotalExposure = maxShutterSpeed * maxAnalogueGain;
-	exposureValue = std::min(exposureValue, maxTotalExposure);
-	LOG(RkISP1Agc, Debug) << "Target total exposure " << exposureValue
-			      << ", maximum is " << maxTotalExposure;
-
-	/*
-	 * Divide the exposure value as new exposure and gain values.
-	 * \todo estimate if we need to desaturate
-	 */
-	exposureValue = filterExposure(exposureValue);
-
-	/*
-	 * Push the shutter time up to the maximum first, and only then
-	 * increase the gain.
-	 */
-	utils::Duration shutterTime = std::clamp<utils::Duration>(exposureValue / minAnalogueGain,
-								  minShutterSpeed, maxShutterSpeed);
-	double stepGain = std::clamp(exposureValue / shutterTime,
-				     minAnalogueGain, maxAnalogueGain);
-	LOG(RkISP1Agc, Debug) << "Divided up shutter and gain are "
-			      << shutterTime << " and "
-			      << stepGain;
-
-	/* Update the estimated exposure and gain. */
-	activeState.agc.automatic.exposure = shutterTime / configuration.sensor.lineDuration;
-	activeState.agc.automatic.gain = stepGain;
+	metadata.set(controls::AeMeteringMode, frameContext.agc.meteringMode);
+	metadata.set(controls::AeExposureMode, frameContext.agc.exposureMode);
+	metadata.set(controls::AeConstraintMode, frameContext.agc.constraintMode);
 }
 
 /**
  * \brief Estimate the relative luminance of the frame with a given gain
- * \param[in] ae The RkISP1 statistics and ISP results
  * \param[in] gain The gain to apply to the frame
  *
  * This function estimates the average relative luminance of the frame that
@@ -332,8 +364,6 @@
  * YUV doesn't take into account the fact that the R, G and B components
  * contribute differently to the relative luminance.
  *
- * \todo Have a dedicated YUV algorithm ?
- *
  * The values are normalized to the [0.0, 1.0] range, where 1.0 corresponds to a
  * theoretical perfect reflector of 100% reference white.
  *
@@ -342,46 +372,17 @@
  *
  * \return The relative luminance
  */
-double Agc::estimateLuminance(const rkisp1_cif_isp_ae_stat *ae,
-			      double gain)
+double Agc::estimateLuminance(double gain) const
 {
 	double ySum = 0.0;
 
 	/* Sum the averages, saturated to 255. */
-	for (unsigned int aeCell = 0; aeCell < numCells_; aeCell++)
-		ySum += std::min(ae->exp_mean[aeCell] * gain, 255.0);
+	for (uint8_t expMean : expMeans_)
+		ySum += std::min(expMean * gain, 255.0);
 
 	/* \todo Weight with the AWB gains */
 
-	return ySum / numCells_ / 255;
-}
-
-/**
- * \brief Estimate the mean value of the top 2% of the histogram
- * \param[in] hist The histogram statistics computed by the ImgU
- * \return The mean value of the top 2% of the histogram
- */
-double Agc::measureBrightness(const rkisp1_cif_isp_hist_stat *hist) const
-{
-	Histogram histogram{ Span<const uint32_t>(hist->hist_bins, numHistBins_) };
-	/* Estimate the quantile mean of the top 2% of the histogram. */
-	return histogram.interQuantileMean(0.98, 1.0);
-}
-
-void Agc::fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
-		       ControlList &metadata)
-{
-	utils::Duration exposureTime = context.configuration.sensor.lineDuration
-				     * frameContext.sensor.exposure;
-	metadata.set(controls::AnalogueGain, frameContext.sensor.gain);
-	metadata.set(controls::ExposureTime, exposureTime.get<std::micro>());
-
-	/* \todo Use VBlank value calculated from each frame exposure. */
-	uint32_t vTotal = context.configuration.sensor.size.height
-			+ context.configuration.sensor.defVBlank;
-	utils::Duration frameDuration = context.configuration.sensor.lineDuration
-				      * vTotal;
-	metadata.set(controls::FrameDuration, frameDuration.get<std::micro>());
+	return ySum / expMeans_.size() / 255;
 }
 
 /**
@@ -415,38 +416,47 @@
 	const rkisp1_cif_isp_stat *params = &stats->params;
 	ASSERT(stats->meas_type & RKISP1_CIF_ISP_STAT_AUTOEXP);
 
-	const rkisp1_cif_isp_ae_stat *ae = &params->ae;
-	const rkisp1_cif_isp_hist_stat *hist = &params->hist;
+	/* The lower 4 bits are fractional and meant to be discarded. */
+	Histogram hist({ params->hist.hist_bins, context.hw->numHistogramBins },
+		       [](uint32_t x) { return x >> 4; });
+	expMeans_ = { params->ae.exp_mean, context.hw->numAeCells };
 
-	double iqMean = measureBrightness(hist);
-	double iqMeanGain = kEvGainTarget * numHistBins_ / iqMean;
+	utils::Duration maxShutterSpeed =
+		std::clamp(frameContext.agc.maxFrameDuration,
+			   context.configuration.sensor.minShutterSpeed,
+			   context.configuration.sensor.maxShutterSpeed);
+	setLimits(context.configuration.sensor.minShutterSpeed,
+		  maxShutterSpeed,
+		  context.configuration.sensor.minAnalogueGain,
+		  context.configuration.sensor.maxAnalogueGain);
 
 	/*
-	 * Estimate the gain needed to achieve a relative luminance target. To
-	 * account for non-linearity caused by saturation, the value needs to be
-	 * estimated in an iterative process, as multiplying by a gain will not
-	 * increase the relative luminance by the same factor if some image
-	 * regions are saturated.
+	 * The Agc algorithm needs to know the effective exposure value that was
+	 * applied to the sensor when the statistics were collected.
 	 */
-	double yGain = 1.0;
-	double yTarget = kRelativeLuminanceTarget;
+	utils::Duration exposureTime = context.configuration.sensor.lineDuration
+				       * frameContext.sensor.exposure;
+	double analogueGain = frameContext.sensor.gain;
+	utils::Duration effectiveExposureValue = exposureTime * analogueGain;
 
-	for (unsigned int i = 0; i < 8; i++) {
-		double yValue = estimateLuminance(ae, yGain);
-		double extra_gain = std::min(10.0, yTarget / (yValue + .001));
+	utils::Duration shutterTime;
+	double aGain, dGain;
+	std::tie(shutterTime, aGain, dGain) =
+		calculateNewEv(frameContext.agc.constraintMode,
+			       frameContext.agc.exposureMode,
+			       hist, effectiveExposureValue);
 
-		yGain *= extra_gain;
-		LOG(RkISP1Agc, Debug) << "Y value: " << yValue
-				      << ", Y target: " << yTarget
-				      << ", gives gain " << yGain;
-		if (extra_gain < 1.01)
-			break;
-	}
+	LOG(RkISP1Agc, Debug)
+		<< "Divided up shutter, analogue gain and digital gain are "
+		<< shutterTime << ", " << aGain << " and " << dGain;
 
-	computeExposure(context, frameContext, yGain, iqMeanGain);
-	frameCount_++;
+	IPAActiveState &activeState = context.activeState;
+	/* Update the estimated exposure and gain. */
+	activeState.agc.automatic.exposure = shutterTime / context.configuration.sensor.lineDuration;
+	activeState.agc.automatic.gain = aGain;
 
 	fillMetadata(context, frameContext, metadata);
+	expMeans_ = {};
 }
 
 REGISTER_IPA_ALGORITHM(Agc, "Agc")
diff --git a/src/ipa/rkisp1/algorithms/agc.h b/src/ipa/rkisp1/algorithms/agc.h
index 8a22263..9ceaa82 100644
--- a/src/ipa/rkisp1/algorithms/agc.h
+++ b/src/ipa/rkisp1/algorithms/agc.h
@@ -2,29 +2,34 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * agc.h - RkISP1 AGC/AEC mean-based control algorithm
+ * RkISP1 AGC/AEC mean-based control algorithm
  */
 
 #pragma once
 
 #include <linux/rkisp1-config.h>
 
+#include <libcamera/base/span.h>
 #include <libcamera/base/utils.h>
 
 #include <libcamera/geometry.h>
 
+#include "libipa/agc_mean_luminance.h"
+#include "libipa/histogram.h"
+
 #include "algorithm.h"
 
 namespace libcamera {
 
 namespace ipa::rkisp1::algorithms {
 
-class Agc : public Algorithm
+class Agc : public Algorithm, public AgcMeanLuminance
 {
 public:
 	Agc();
 	~Agc() = default;
 
+	int init(IPAContext &context, const YamlObject &tuningData) override;
 	int configure(IPAContext &context, const IPACameraSensorInfo &configInfo) override;
 	void queueRequest(IPAContext &context,
 			  const uint32_t frame,
@@ -39,20 +44,17 @@
 		     ControlList &metadata) override;
 
 private:
-	void computeExposure(IPAContext &Context, IPAFrameContext &frameContext,
-			     double yGain, double iqMeanGain);
-	utils::Duration filterExposure(utils::Duration exposureValue);
-	double estimateLuminance(const rkisp1_cif_isp_ae_stat *ae, double gain);
-	double measureBrightness(const rkisp1_cif_isp_hist_stat *hist) const;
+	int parseMeteringModes(IPAContext &context, const YamlObject &tuningData);
+	uint8_t computeHistogramPredivider(const Size &size,
+					   enum rkisp1_cif_isp_histogram_mode mode);
+
 	void fillMetadata(IPAContext &context, IPAFrameContext &frameContext,
 			  ControlList &metadata);
+	double estimateLuminance(double gain) const override;
 
-	uint64_t frameCount_;
+	Span<const uint8_t> expMeans_;
 
-	uint32_t numCells_;
-	uint32_t numHistBins_;
-
-	utils::Duration filteredExposure_;
+	std::map<int32_t, std::vector<uint8_t>> meteringModes_;
 };
 
 } /* namespace ipa::rkisp1::algorithms */
diff --git a/src/ipa/rkisp1/algorithms/algorithm.h b/src/ipa/rkisp1/algorithms/algorithm.h
index 9454c9a..715cfcd 100644
--- a/src/ipa/rkisp1/algorithms/algorithm.h
+++ b/src/ipa/rkisp1/algorithms/algorithm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas On Board
  *
- * algorithm.h - RkISP1 control algorithm interface
+ * RkISP1 control algorithm interface
  */
 
 #pragma once
diff --git a/src/ipa/rkisp1/algorithms/awb.cpp b/src/ipa/rkisp1/algorithms/awb.cpp
index 744f4a3..a01fe5d 100644
--- a/src/ipa/rkisp1/algorithms/awb.cpp
+++ b/src/ipa/rkisp1/algorithms/awb.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * awb.cpp - AWB control algorithm
+ * AWB control algorithm
  */
 
 #include "awb.h"
diff --git a/src/ipa/rkisp1/algorithms/awb.h b/src/ipa/rkisp1/algorithms/awb.h
index 9d45a44..06c9289 100644
--- a/src/ipa/rkisp1/algorithms/awb.h
+++ b/src/ipa/rkisp1/algorithms/awb.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * awb.h - AWB control algorithm
+ * AWB control algorithm
  */
 
 #pragma once
diff --git a/src/ipa/rkisp1/algorithms/blc.cpp b/src/ipa/rkisp1/algorithms/blc.cpp
index 15324fb..d2e7435 100644
--- a/src/ipa/rkisp1/algorithms/blc.cpp
+++ b/src/ipa/rkisp1/algorithms/blc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * blc.cpp - RkISP1 Black Level Correction control
+ * RkISP1 Black Level Correction control
  */
 
 #include "blc.h"
diff --git a/src/ipa/rkisp1/algorithms/blc.h b/src/ipa/rkisp1/algorithms/blc.h
index 0b1a2d4..460ebcc 100644
--- a/src/ipa/rkisp1/algorithms/blc.h
+++ b/src/ipa/rkisp1/algorithms/blc.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * blc.h - RkISP1 Black Level Correction control
+ * RkISP1 Black Level Correction control
  */
 
 #pragma once
diff --git a/src/ipa/rkisp1/algorithms/ccm.cpp b/src/ipa/rkisp1/algorithms/ccm.cpp
new file mode 100644
index 0000000..c1f5403
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/ccm.cpp
@@ -0,0 +1,147 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ *
+ * RkISP1 Color Correction Matrix control algorithm
+ */
+
+#include "ccm.h"
+
+#include <algorithm>
+#include <chrono>
+#include <cmath>
+#include <tuple>
+#include <vector>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
+
+#include <libcamera/ipa/core_ipa_interface.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "../utils.h"
+#include "libipa/matrix_interpolator.h"
+
+/**
+ * \file ccm.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+/**
+ * \class Ccm
+ * \brief A color correction matrix algorithm
+ */
+
+LOG_DEFINE_CATEGORY(RkISP1Ccm)
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int Ccm::init([[maybe_unused]] IPAContext &context, const YamlObject &tuningData)
+{
+	int ret = ccm_.readYaml(tuningData["ccms"], "ct", "ccm");
+	if (ret < 0) {
+		LOG(RkISP1Ccm, Warning)
+			<< "Failed to parse 'ccm' "
+			<< "parameter from tuning file; falling back to unit matrix";
+		ccm_.reset();
+	}
+
+	ret = offsets_.readYaml(tuningData["ccms"], "ct", "offsets");
+	if (ret < 0) {
+		LOG(RkISP1Ccm, Warning)
+			<< "Failed to parse 'offsets' "
+			<< "parameter from tuning file; falling back to zero offsets";
+		/*
+		 * MatrixInterpolator::reset() resets to identity matrices
+		 * while here we need zero matrices so we need to construct it
+		 * ourselves.
+		 */
+		Matrix<int16_t, 3, 1> m({ 0, 0, 0 });
+		std::map<unsigned int, Matrix<int16_t, 3, 1>> matrices = { { 0, m } };
+		offsets_ = MatrixInterpolator<int16_t, 3, 1>(matrices);
+	}
+
+	return 0;
+}
+
+void Ccm::setParameters(rkisp1_params_cfg *params,
+			const Matrix<float, 3, 3> &matrix,
+			const Matrix<int16_t, 3, 1> &offsets)
+{
+	struct rkisp1_cif_isp_ctk_config &config = params->others.ctk_config;
+
+	/*
+	 * 4 bit integer and 7 bit fractional, ranging from -8 (0x400) to
+	 * +7.992 (0x3ff)
+	 */
+	for (unsigned int i = 0; i < 3; i++) {
+		for (unsigned int j = 0; j < 3; j++)
+			config.coeff[i][j] =
+				utils::floatingToFixedPoint<4, 7, uint16_t, double>(matrix[i][j]);
+	}
+
+	for (unsigned int i = 0; i < 3; i++)
+		config.ct_offset[i] = offsets[i][0] & 0xfff;
+
+	LOG(RkISP1Ccm, Debug) << "Setting matrix " << matrix;
+	LOG(RkISP1Ccm, Debug) << "Setting offsets " << offsets;
+
+	params->module_en_update |= RKISP1_CIF_ISP_MODULE_CTK;
+	params->module_ens |= RKISP1_CIF_ISP_MODULE_CTK;
+	params->module_cfg_update |= RKISP1_CIF_ISP_MODULE_CTK;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void Ccm::prepare(IPAContext &context, const uint32_t frame,
+		  IPAFrameContext &frameContext,
+		  rkisp1_params_cfg *params)
+{
+	uint32_t ct = context.activeState.awb.temperatureK;
+
+	/*
+	 * \todo The colour temperature will likely be noisy, add filtering to
+	 * avoid updating the CCM matrix all the time.
+	 */
+	if (frame > 0 && ct == ct_)
+		return;
+
+	ct_ = ct;
+	Matrix<float, 3, 3> ccm = ccm_.get(ct);
+	Matrix<int16_t, 3, 1> offsets = offsets_.get(ct);
+
+	frameContext.ccm.ccm = ccm;
+
+	setParameters(params, ccm, offsets);
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::process
+ */
+void Ccm::process([[maybe_unused]] IPAContext &context,
+		  [[maybe_unused]] const uint32_t frame,
+		  IPAFrameContext &frameContext,
+		  [[maybe_unused]] const rkisp1_stat_buffer *stats,
+		  ControlList &metadata)
+{
+	float m[9];
+	for (unsigned int i = 0; i < 3; i++) {
+		for (unsigned int j = 0; j < 3; j++)
+			m[i] = frameContext.ccm.ccm[i][j];
+	}
+	metadata.set(controls::ColourCorrectionMatrix, m);
+}
+
+REGISTER_IPA_ALGORITHM(Ccm, "Ccm")
+
+} /* namespace ipa::rkisp1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/ccm.h b/src/ipa/rkisp1/algorithms/ccm.h
new file mode 100644
index 0000000..30cb882
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/ccm.h
@@ -0,0 +1,49 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ *
+ * RkISP1 Color Correction Matrix control algorithm
+ */
+
+#pragma once
+
+#include <linux/rkisp1-config.h>
+
+#include "libipa/matrix.h"
+#include "libipa/matrix_interpolator.h"
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+class Ccm : public Algorithm
+{
+public:
+	Ccm() {}
+	~Ccm() = default;
+
+	int init(IPAContext &context, const YamlObject &tuningData) override;
+	void prepare(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     rkisp1_params_cfg *params) override;
+	void process(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     const rkisp1_stat_buffer *stats,
+		     ControlList &metadata) override;
+
+private:
+	void parseYaml(const YamlObject &tuningData);
+	void setParameters(rkisp1_params_cfg *params,
+			   const Matrix<float, 3, 3> &matrix,
+			   const Matrix<int16_t, 3, 1> &offsets);
+
+	unsigned int ct_;
+	MatrixInterpolator<float, 3, 3> ccm_;
+	MatrixInterpolator<int16_t, 3, 1> offsets_;
+};
+
+} /* namespace ipa::rkisp1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/cproc.cpp b/src/ipa/rkisp1/algorithms/cproc.cpp
index eaa56c3..ef0931b 100644
--- a/src/ipa/rkisp1/algorithms/cproc.cpp
+++ b/src/ipa/rkisp1/algorithms/cproc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * cproc.cpp - RkISP1 Color Processing control
+ * RkISP1 Color Processing control
  */
 
 #include "cproc.h"
@@ -33,20 +33,71 @@
 
 LOG_DEFINE_CATEGORY(RkISP1CProc)
 
+namespace {
+
+constexpr float kDefaultBrightness = 0.0f;
+constexpr float kDefaultContrast = 1.0f;
+constexpr float kDefaultSaturation = 1.0f;
+
+int convertBrightness(const float v)
+{
+	return std::clamp<int>(std::lround(v * 128), -128, 127);
+}
+
+int convertContrastOrSaturation(const float v)
+{
+	return std::clamp<int>(std::lround(v * 128), 0, 255);
+}
+
+} /* namespace */
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int ColorProcessing::init(IPAContext &context,
+			  [[maybe_unused]] const YamlObject &tuningData)
+{
+	auto &cmap = context.ctrlMap;
+
+	cmap[&controls::Brightness] = ControlInfo(-1.0f, 0.993f, kDefaultBrightness);
+	cmap[&controls::Contrast] = ControlInfo(0.0f, 1.993f, kDefaultContrast);
+	cmap[&controls::Saturation] = ControlInfo(0.0f, 1.993f, kDefaultSaturation);
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::configure
+ */
+int ColorProcessing::configure(IPAContext &context,
+			       [[maybe_unused]] const IPACameraSensorInfo &configInfo)
+{
+	auto &cproc = context.activeState.cproc;
+
+	cproc.brightness = convertBrightness(kDefaultBrightness);
+	cproc.contrast = convertContrastOrSaturation(kDefaultContrast);
+	cproc.saturation = convertContrastOrSaturation(kDefaultSaturation);
+
+	return 0;
+}
+
 /**
  * \copydoc libcamera::ipa::Algorithm::queueRequest
  */
 void ColorProcessing::queueRequest(IPAContext &context,
-				   [[maybe_unused]] const uint32_t frame,
+				   const uint32_t frame,
 				   IPAFrameContext &frameContext,
 				   const ControlList &controls)
 {
 	auto &cproc = context.activeState.cproc;
 	bool update = false;
 
+	if (frame == 0)
+		update = true;
+
 	const auto &brightness = controls.get(controls::Brightness);
 	if (brightness) {
-		int value = std::clamp<int>(std::lround(*brightness * 128), -128, 127);
+		int value = convertBrightness(*brightness);
 		if (cproc.brightness != value) {
 			cproc.brightness = value;
 			update = true;
@@ -57,7 +108,7 @@
 
 	const auto &contrast = controls.get(controls::Contrast);
 	if (contrast) {
-		int value = std::clamp<int>(std::lround(*contrast * 128), 0, 255);
+		int value = convertContrastOrSaturation(*contrast);
 		if (cproc.contrast != value) {
 			cproc.contrast = value;
 			update = true;
@@ -68,7 +119,7 @@
 
 	const auto saturation = controls.get(controls::Saturation);
 	if (saturation) {
-		int value = std::clamp<int>(std::lround(*saturation * 128), 0, 255);
+		int value = convertContrastOrSaturation(*saturation);
 		if (cproc.saturation != value) {
 			cproc.saturation = value;
 			update = true;
diff --git a/src/ipa/rkisp1/algorithms/cproc.h b/src/ipa/rkisp1/algorithms/cproc.h
index ba6e901..e50e720 100644
--- a/src/ipa/rkisp1/algorithms/cproc.h
+++ b/src/ipa/rkisp1/algorithms/cproc.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * cproc.h - RkISP1 Color Processing control
+ * RkISP1 Color Processing control
  */
 
 #pragma once
@@ -21,6 +21,9 @@
 	ColorProcessing() = default;
 	~ColorProcessing() = default;
 
+	int init(IPAContext &context, const YamlObject &tuningData) override;
+	int configure(IPAContext &context,
+		      const IPACameraSensorInfo &configInfo) override;
 	void queueRequest(IPAContext &context, const uint32_t frame,
 			  IPAFrameContext &frameContext,
 			  const ControlList &controls) override;
diff --git a/src/ipa/rkisp1/algorithms/dpcc.cpp b/src/ipa/rkisp1/algorithms/dpcc.cpp
index 80a1b73..b5a339e 100644
--- a/src/ipa/rkisp1/algorithms/dpcc.cpp
+++ b/src/ipa/rkisp1/algorithms/dpcc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * dpcc.cpp - RkISP1 Defect Pixel Cluster Correction control
+ * RkISP1 Defect Pixel Cluster Correction control
  */
 
 #include "dpcc.h"
diff --git a/src/ipa/rkisp1/algorithms/dpcc.h b/src/ipa/rkisp1/algorithms/dpcc.h
index b1fac7d..d39b7be 100644
--- a/src/ipa/rkisp1/algorithms/dpcc.h
+++ b/src/ipa/rkisp1/algorithms/dpcc.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * dpcc.h - RkISP1 Defect Pixel Cluster Correction control
+ * RkISP1 Defect Pixel Cluster Correction control
  */
 
 #pragma once
diff --git a/src/ipa/rkisp1/algorithms/dpf.cpp b/src/ipa/rkisp1/algorithms/dpf.cpp
index 5bd7e59..abf9572 100644
--- a/src/ipa/rkisp1/algorithms/dpf.cpp
+++ b/src/ipa/rkisp1/algorithms/dpf.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * dpf.cpp - RkISP1 Denoise Pre-Filter control
+ * RkISP1 Denoise Pre-Filter control
  */
 
 #include "dpf.h"
diff --git a/src/ipa/rkisp1/algorithms/dpf.h b/src/ipa/rkisp1/algorithms/dpf.h
index 58f29f7..da0115b 100644
--- a/src/ipa/rkisp1/algorithms/dpf.h
+++ b/src/ipa/rkisp1/algorithms/dpf.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * dpf.h - RkISP1 Denoise Pre-Filter control
+ * RkISP1 Denoise Pre-Filter control
  */
 
 #pragma once
diff --git a/src/ipa/rkisp1/algorithms/filter.cpp b/src/ipa/rkisp1/algorithms/filter.cpp
index 4b89c05..9752248 100644
--- a/src/ipa/rkisp1/algorithms/filter.cpp
+++ b/src/ipa/rkisp1/algorithms/filter.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * filter.cpp - RkISP1 Filter control
+ * RkISP1 Filter control
  */
 
 #include "filter.h"
diff --git a/src/ipa/rkisp1/algorithms/filter.h b/src/ipa/rkisp1/algorithms/filter.h
index 3fd882e..d595811 100644
--- a/src/ipa/rkisp1/algorithms/filter.h
+++ b/src/ipa/rkisp1/algorithms/filter.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * filter.h - RkISP1 Filter control
+ * RkISP1 Filter control
  */
 
 #pragma once
diff --git a/src/ipa/rkisp1/algorithms/goc.cpp b/src/ipa/rkisp1/algorithms/goc.cpp
new file mode 100644
index 0000000..a82cee3
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/goc.cpp
@@ -0,0 +1,148 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ *
+ * RkISP1 Gamma out control
+ */
+#include "goc.h"
+
+#include <cmath>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
+
+#include <libcamera/control_ids.h>
+
+#include "libcamera/internal/yaml_parser.h"
+
+#include "linux/rkisp1-config.h"
+
+/**
+ * \file goc.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+/**
+ * \class GammaOutCorrection
+ * \brief RkISP1 Gamma out correction
+ *
+ * This algorithm implements the gamma out curve for the RkISP1. It defaults to
+ * a gamma value of 2.2.
+ *
+ * As gamma is internally represented as a piecewise linear function with only
+ * 17 knots, the difference between gamma=2.2 and sRGB gamma is minimal.
+ * Therefore sRGB gamma was not implemented as special case.
+ *
+ * Useful links:
+ * - https://www.cambridgeincolour.com/tutorials/gamma-correction.htm
+ * - https://en.wikipedia.org/wiki/SRGB
+ */
+
+LOG_DEFINE_CATEGORY(RkISP1Gamma)
+
+const float kDefaultGamma = 2.2f;
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::init
+ */
+int GammaOutCorrection::init(IPAContext &context, const YamlObject &tuningData)
+{
+	if (context.hw->numGammaOutSamples !=
+	    RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10) {
+		LOG(RkISP1Gamma, Error)
+			<< "Gamma is not implemented for RkISP1 V12";
+		return -EINVAL;
+	}
+
+	defaultGamma_ = tuningData["gamma"].get<double>(kDefaultGamma);
+	context.ctrlMap[&controls::Gamma] = ControlInfo(0.1f, 10.0f, defaultGamma_);
+
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::configure
+ */
+int GammaOutCorrection::configure(IPAContext &context,
+				  [[maybe_unused]] const IPACameraSensorInfo &configInfo)
+{
+	context.activeState.goc.gamma = defaultGamma_;
+	return 0;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::queueRequest
+ */
+void GammaOutCorrection::queueRequest(IPAContext &context, const uint32_t frame,
+				      IPAFrameContext &frameContext,
+				      const ControlList &controls)
+{
+	if (frame == 0)
+		frameContext.goc.update = true;
+
+	const auto &gamma = controls.get(controls::Gamma);
+	if (gamma) {
+		context.activeState.goc.gamma = *gamma;
+		frameContext.goc.update = true;
+		LOG(RkISP1Gamma, Debug) << "Set gamma to " << *gamma;
+	}
+
+	frameContext.goc.gamma = context.activeState.goc.gamma;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::prepare
+ */
+void GammaOutCorrection::prepare(IPAContext &context,
+				 [[maybe_unused]] const uint32_t frame,
+				 IPAFrameContext &frameContext,
+				 rkisp1_params_cfg *params)
+{
+	ASSERT(context.hw->numGammaOutSamples ==
+	       RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10);
+
+	/*
+	 * The logarithmic segments as specified in the reference.
+	 * Plus an additional 0 to make the loop easier
+	 */
+	static constexpr std::array<unsigned int, RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10> segments = {
+		64, 64, 64, 64, 128, 128, 128, 128, 256,
+		256, 256, 512, 512, 512, 512, 512, 0
+	};
+	__u16 *gamma_y = params->others.goc_config.gamma_y;
+
+	if (!frameContext.goc.update)
+		return;
+
+	unsigned x = 0;
+	for (const auto [i, size] : utils::enumerate(segments)) {
+		gamma_y[i] = std::pow(x / 4096.0, 1.0 / frameContext.goc.gamma) * 1023.0;
+		x += size;
+	}
+
+	params->others.goc_config.mode = RKISP1_CIF_ISP_GOC_MODE_LOGARITHMIC;
+	params->module_cfg_update |= RKISP1_CIF_ISP_MODULE_GOC;
+	params->module_en_update |= RKISP1_CIF_ISP_MODULE_GOC;
+	params->module_ens |= RKISP1_CIF_ISP_MODULE_GOC;
+}
+
+/**
+ * \copydoc libcamera::ipa::Algorithm::process
+ */
+void GammaOutCorrection::process([[maybe_unused]] IPAContext &context,
+				 [[maybe_unused]] const uint32_t frame,
+				 IPAFrameContext &frameContext,
+				 [[maybe_unused]] const rkisp1_stat_buffer *stats,
+				 ControlList &metadata)
+{
+	metadata.set(controls::Gamma, frameContext.goc.gamma);
+}
+
+REGISTER_IPA_ALGORITHM(GammaOutCorrection, "GammaOutCorrection")
+
+} /* namespace ipa::rkisp1::algorithms */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/goc.h b/src/ipa/rkisp1/algorithms/goc.h
new file mode 100644
index 0000000..0e05d7c
--- /dev/null
+++ b/src/ipa/rkisp1/algorithms/goc.h
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas On Board
+ *
+ * RkISP1 Gamma out control
+ */
+
+#pragma once
+
+#include "algorithm.h"
+
+namespace libcamera {
+
+namespace ipa::rkisp1::algorithms {
+
+class GammaOutCorrection : public Algorithm
+{
+public:
+	GammaOutCorrection() = default;
+	~GammaOutCorrection() = default;
+
+	int init(IPAContext &context, const YamlObject &tuningData) override;
+	int configure(IPAContext &context,
+		      const IPACameraSensorInfo &configInfo) override;
+	void queueRequest(IPAContext &context,
+			  const uint32_t frame,
+			  IPAFrameContext &frameContext,
+			  const ControlList &controls) override;
+	void prepare(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     rkisp1_params_cfg *params) override;
+	void process(IPAContext &context, const uint32_t frame,
+		     IPAFrameContext &frameContext,
+		     const rkisp1_stat_buffer *stats,
+		     ControlList &metadata) override;
+
+private:
+	float defaultGamma_;
+};
+
+} /* namespace ipa::rkisp1::algorithms */
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/algorithms/gsl.cpp b/src/ipa/rkisp1/algorithms/gsl.cpp
index b9f8791..9b056c6 100644
--- a/src/ipa/rkisp1/algorithms/gsl.cpp
+++ b/src/ipa/rkisp1/algorithms/gsl.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * gsl.cpp - RkISP1 Gamma Sensor Linearization control
+ * RkISP1 Gamma Sensor Linearization control
  */
 
 #include "gsl.h"
diff --git a/src/ipa/rkisp1/algorithms/gsl.h b/src/ipa/rkisp1/algorithms/gsl.h
index 0f1116a..c404105 100644
--- a/src/ipa/rkisp1/algorithms/gsl.h
+++ b/src/ipa/rkisp1/algorithms/gsl.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * gsl.h - RkISP1 Gamma Sensor Linearization control
+ * RkISP1 Gamma Sensor Linearization control
  */
 
 #pragma once
diff --git a/src/ipa/rkisp1/algorithms/lsc.cpp b/src/ipa/rkisp1/algorithms/lsc.cpp
index a7ccedb..161183f 100644
--- a/src/ipa/rkisp1/algorithms/lsc.cpp
+++ b/src/ipa/rkisp1/algorithms/lsc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * lsc.cpp - RkISP1 Lens Shading Correction control
+ * RkISP1 Lens Shading Correction control
  */
 
 #include "lsc.h"
diff --git a/src/ipa/rkisp1/algorithms/lsc.h b/src/ipa/rkisp1/algorithms/lsc.h
index e2a93a5..5baf592 100644
--- a/src/ipa/rkisp1/algorithms/lsc.h
+++ b/src/ipa/rkisp1/algorithms/lsc.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * lsc.h - RkISP1 Lens Shading Correction control
+ * RkISP1 Lens Shading Correction control
  */
 
 #pragma once
diff --git a/src/ipa/rkisp1/algorithms/meson.build b/src/ipa/rkisp1/algorithms/meson.build
index 93a4832..1734a66 100644
--- a/src/ipa/rkisp1/algorithms/meson.build
+++ b/src/ipa/rkisp1/algorithms/meson.build
@@ -4,10 +4,12 @@
     'agc.cpp',
     'awb.cpp',
     'blc.cpp',
+    'ccm.cpp',
     'cproc.cpp',
     'dpcc.cpp',
     'dpf.cpp',
     'filter.cpp',
+    'goc.cpp',
     'gsl.cpp',
     'lsc.cpp',
 ])
diff --git a/src/ipa/rkisp1/data/meson.build b/src/ipa/rkisp1/data/meson.build
index f5e9fa7..7150e15 100644
--- a/src/ipa/rkisp1/data/meson.build
+++ b/src/ipa/rkisp1/data/meson.build
@@ -8,4 +8,5 @@
 ])
 
 install_data(conf_files,
-             install_dir : ipa_data_dir / 'rkisp1')
+             install_dir : ipa_data_dir / 'rkisp1',
+             install_tag : 'runtime')
diff --git a/src/ipa/rkisp1/ipa_context.cpp b/src/ipa/rkisp1/ipa_context.cpp
index 9bbf368..9f3f576 100644
--- a/src/ipa/rkisp1/ipa_context.cpp
+++ b/src/ipa/rkisp1/ipa_context.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * ipa_context.cpp - RkISP1 IPA Context
+ * RkISP1 IPA Context
  */
 
 #include "ipa_context.h"
@@ -15,6 +15,25 @@
 namespace libcamera::ipa::rkisp1 {
 
 /**
+ * \struct IPAHwSettings
+ * \brief RkISP1 version-specific hardware parameters
+ */
+
+/**
+ * \var IPAHwSettings::numAeCells
+ * \brief Number of cells in the AE exposure means grid
+ *
+ * \var IPAHwSettings::numHistogramBins
+ * \brief Number of bins in the histogram
+ *
+ * \var IPAHwSettings::numHistogramWeights
+ * \brief Number of weights in the histogram grid
+ *
+ * \var IPAHwSettings::numGammaOutSamples
+ * \brief Number of samples in the gamma out table
+ */
+
+/**
  * \struct IPASessionConfiguration
  * \brief Session configuration for the IPA module
  *
@@ -33,14 +52,6 @@
  */
 
 /**
- * \var IPASessionConfiguration::hw
- * \brief RkISP1-specific hardware information
- *
- * \var IPASessionConfiguration::hw.revision
- * \brief Hardware revision of the ISP
- */
-
-/**
  * \var IPASessionConfiguration::awb
  * \brief AWB parameters configuration of the IPA
  *
@@ -126,13 +137,43 @@
  * \var IPAActiveState::agc
  * \brief State for the Automatic Gain Control algorithm
  *
- * The exposure and gain are the latest values computed by the AGC algorithm.
+ * The \a automatic variables track the latest values computed by algorithm
+ * based on the latest processed statistics. All other variables track the
+ * consolidated controls requested in queued requests.
  *
- * \var IPAActiveState::agc.exposure
- * \brief Exposure time expressed as a number of lines
+ * \struct IPAActiveState::agc.manual
+ * \brief Manual exposure time and analog gain (set through requests)
  *
- * \var IPAActiveState::agc.gain
- * \brief Analogue gain multiplier
+ * \var IPAActiveState::agc.manual.exposure
+ * \brief Manual exposure time expressed as a number of lines as set by the
+ * ExposureTime control
+ *
+ * \var IPAActiveState::agc.manual.gain
+ * \brief Manual analogue gain as set by the AnalogueGain control
+ *
+ * \struct IPAActiveState::agc.automatic
+ * \brief Automatic exposure time and analog gain (computed by the algorithm)
+ *
+ * \var IPAActiveState::agc.automatic.exposure
+ * \brief Automatic exposure time expressed as a number of lines
+ *
+ * \var IPAActiveState::agc.automatic.gain
+ * \brief Automatic analogue gain multiplier
+ *
+ * \var IPAActiveState::agc.autoEnabled
+ * \brief Manual/automatic AGC state as set by the AeEnable control
+ *
+ * \var IPAActiveState::agc.constraintMode
+ * \brief Constraint mode as set by the AeConstraintMode control
+ *
+ * \var IPAActiveState::agc.exposureMode
+ * \brief Exposure mode as set by the AeExposureMode control
+ *
+ * \var IPAActiveState::agc.meteringMode
+ * \brief Metering mode as set by the AeMeteringMode control
+ *
+ * \var IPAActiveState::agc.maxFrameDuration
+ * \brief Maximum frame duration as set by the FrameDurationLimits control
  */
 
 /**
@@ -207,6 +248,14 @@
  */
 
 /**
+ * \var IPAActiveState::goc
+ * \brief State for the goc algorithm
+ *
+ * \var IPAActiveState::goc.gamma
+ * \brief Gamma value applied as 1.0/gamma
+ */
+
+/**
  * \struct IPAFrameContext
  * \brief Per-frame context for algorithms
  *
@@ -246,12 +295,30 @@
  * applied to the sensor in order to take effect for this frame.
  *
  * \var IPAFrameContext::agc.exposure
- * \brief Exposure time expressed as a number of lines
+ * \brief Exposure time expressed as a number of lines computed by the algorithm
  *
  * \var IPAFrameContext::agc.gain
- * \brief Analogue gain multiplier
+ * \brief Analogue gain multiplier computed by the algorithm
  *
  * The gain should be adapted to the sensor specific gain code before applying.
+ *
+ * \var IPAFrameContext::agc.autoEnabled
+ * \brief Manual/automatic AGC state as set by the AeEnable control
+ *
+ * \var IPAFrameContext::agc.constraintMode
+ * \brief Constraint mode as set by the AeConstraintMode control
+ *
+ * \var IPAFrameContext::agc.exposureMode
+ * \brief Exposure mode as set by the AeExposureMode control
+ *
+ * \var IPAFrameContext::agc.meteringMode
+ * \brief Metering mode as set by the AeMeteringMode control
+ *
+ * \var IPAFrameContext::agc.maxFrameDuration
+ * \brief Maximum frame duration as set by the FrameDurationLimits control
+ *
+ * \var IPAFrameContext::agc.updateMetering
+ * \brief Indicate if new ISP AGC metering parameters need to be applied
  */
 
 /**
@@ -323,6 +390,18 @@
  */
 
 /**
+ * \var IPAFrameContext::goc
+ * \brief Gamma out correction parameters for this frame
+ *
+ * \var IPAFrameContext::goc.gamma
+ * \brief Gamma value applied as 1.0/gamma
+ *
+ * \var IPAFrameContext::goc.update
+ * \brief Indicates if the goc parameters have been updated compared to the
+ * previous frame
+ */
+
+/**
  * \var IPAFrameContext::sensor
  * \brief Sensor configuration that used been used for this frame
  *
@@ -337,6 +416,9 @@
  * \struct IPAContext
  * \brief Global IPA context data shared between all algorithms
  *
+ * \var IPAContext::hw
+ * \brief RkISP1 version-specific hardware parameters
+ *
  * \var IPAContext::configuration
  * \brief The IPA session configuration, immutable during the session
  *
diff --git a/src/ipa/rkisp1/ipa_context.h b/src/ipa/rkisp1/ipa_context.h
index b9b2065..8602b40 100644
--- a/src/ipa/rkisp1/ipa_context.h
+++ b/src/ipa/rkisp1/ipa_context.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021-2022, Ideas On Board
  *
- * ipa_context.h - RkISP1 IPA Context
+ * RkISP1 IPA Context
  *
  */
 
@@ -12,14 +12,24 @@
 
 #include <libcamera/base/utils.h>
 
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
 #include <libcamera/geometry.h>
 
 #include <libipa/fc_queue.h>
+#include <libipa/matrix.h>
 
 namespace libcamera {
 
 namespace ipa::rkisp1 {
 
+struct IPAHwSettings {
+	unsigned int numAeCells;
+	unsigned int numHistogramBins;
+	unsigned int numHistogramWeights;
+	unsigned int numGammaOutSamples;
+};
+
 struct IPASessionConfiguration {
 	struct {
 		struct rkisp1_cif_isp_window measureWindow;
@@ -45,10 +55,6 @@
 		Size size;
 	} sensor;
 
-	struct {
-		rkisp1_cif_isp_version revision;
-	} hw;
-
 	bool raw;
 };
 
@@ -64,6 +70,10 @@
 		} automatic;
 
 		bool autoEnabled;
+		controls::AeConstraintModeEnum constraintMode;
+		controls::AeExposureModeEnum exposureMode;
+		controls::AeMeteringModeEnum meteringMode;
+		utils::Duration maxFrameDuration;
 	} agc;
 
 	struct {
@@ -98,6 +108,10 @@
 		uint8_t denoise;
 		uint8_t sharpness;
 	} filter;
+
+	struct {
+		double gamma;
+	} goc;
 };
 
 struct IPAFrameContext : public FrameContext {
@@ -105,6 +119,11 @@
 		uint32_t exposure;
 		double gain;
 		bool autoEnabled;
+		controls::AeConstraintModeEnum constraintMode;
+		controls::AeExposureModeEnum exposureMode;
+		controls::AeMeteringModeEnum meteringMode;
+		utils::Duration maxFrameDuration;
+		bool updateMetering;
 	} agc;
 
 	struct {
@@ -137,16 +156,28 @@
 	} filter;
 
 	struct {
+		double gamma;
+		bool update;
+	} goc;
+
+	struct {
 		uint32_t exposure;
 		double gain;
 	} sensor;
+
+	struct {
+		Matrix<float, 3, 3> ccm;
+	} ccm;
 };
 
 struct IPAContext {
+	const IPAHwSettings *hw;
 	IPASessionConfiguration configuration;
 	IPAActiveState activeState;
 
 	FCQueue<IPAFrameContext> frameContexts;
+
+	ControlInfoMap::Map ctrlMap;
 };
 
 } /* namespace ipa::rkisp1 */
diff --git a/src/ipa/rkisp1/meson.build b/src/ipa/rkisp1/meson.build
index e813da5..e8b266f 100644
--- a/src/ipa/rkisp1/meson.build
+++ b/src/ipa/rkisp1/meson.build
@@ -8,6 +8,7 @@
 rkisp1_ipa_sources = files([
     'ipa_context.cpp',
     'rkisp1.cpp',
+    'utils.cpp',
 ])
 
 rkisp1_ipa_sources += rkisp1_ipa_algorithms
@@ -15,9 +16,8 @@
 mod = shared_module(ipa_name,
                     [rkisp1_ipa_sources, libcamera_generated_ipa_headers],
                     name_prefix : '',
-                    include_directories : [ipa_includes, libipa_includes],
-                    dependencies : libcamera_private,
-                    link_with : libipa,
+                    include_directories : [ipa_includes],
+                    dependencies : [libcamera_private, libipa_dep],
                     install : true,
                     install_dir : ipa_install_dir)
 
diff --git a/src/ipa/rkisp1/module.h b/src/ipa/rkisp1/module.h
index 89f8320..16c3e43 100644
--- a/src/ipa/rkisp1/module.h
+++ b/src/ipa/rkisp1/module.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas On Board
  *
- * module.h - RkISP1 IPA Module
+ * RkISP1 IPA Module
  */
 
 #pragma once
diff --git a/src/ipa/rkisp1/rkisp1.cpp b/src/ipa/rkisp1/rkisp1.cpp
index 6544c92..d31cdba 100644
--- a/src/ipa/rkisp1/rkisp1.cpp
+++ b/src/ipa/rkisp1/rkisp1.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * rkisp1.cpp - RkISP1 Image Processing Algorithms
+ * RkISP1 Image Processing Algorithms
  */
 
 #include <algorithm>
@@ -81,12 +81,6 @@
 
 	ControlInfoMap sensorControls_;
 
-	/* revision-specific data */
-	rkisp1_cif_isp_version hwRevision_;
-	unsigned int hwHistBinNMax_;
-	unsigned int hwGammaOutMaxSamples_;
-	unsigned int hwHistogramWeightGridsSize_;
-
 	/* Interface to the Camera Helper */
 	std::unique_ptr<CameraSensorHelper> camHelper_;
 
@@ -96,14 +90,24 @@
 
 namespace {
 
+const IPAHwSettings ipaHwSettingsV10{
+	RKISP1_CIF_ISP_AE_MEAN_MAX_V10,
+	RKISP1_CIF_ISP_HIST_BIN_N_MAX_V10,
+	RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V10,
+	RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10,
+};
+
+const IPAHwSettings ipaHwSettingsV12{
+	RKISP1_CIF_ISP_AE_MEAN_MAX_V12,
+	RKISP1_CIF_ISP_HIST_BIN_N_MAX_V12,
+	RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V12,
+	RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V12,
+};
+
 /* List of controls handled by the RkISP1 IPA */
 const ControlInfoMap::Map rkisp1Controls{
-	{ &controls::AeEnable, ControlInfo(false, true) },
 	{ &controls::AwbEnable, ControlInfo(false, true) },
 	{ &controls::ColourGains, ControlInfo(0.0f, 3.996f, 1.0f) },
-	{ &controls::Brightness, ControlInfo(-1.0f, 0.993f, 0.0f) },
-	{ &controls::Contrast, ControlInfo(0.0f, 1.993f, 1.0f) },
-	{ &controls::Saturation, ControlInfo(0.0f, 1.993f, 1.0f) },
 	{ &controls::Sharpness, ControlInfo(0.0f, 10.0f, 1.0f) },
 	{ &controls::draft::NoiseReductionMode, ControlInfo(controls::draft::NoiseReductionModeValues) },
 };
@@ -111,7 +115,7 @@
 } /* namespace */
 
 IPARkISP1::IPARkISP1()
-	: context_({ {}, {}, { kMaxFrameContexts } })
+	: context_({ {}, {}, {}, { kMaxFrameContexts }, {} })
 {
 }
 
@@ -128,14 +132,11 @@
 	/* \todo Add support for other revisions */
 	switch (hwRevision) {
 	case RKISP1_V10:
-		hwHistBinNMax_ = RKISP1_CIF_ISP_HIST_BIN_N_MAX_V10;
-		hwGammaOutMaxSamples_ = RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V10;
-		hwHistogramWeightGridsSize_ = RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V10;
+	case RKISP1_V_IMX8MP:
+		context_.hw = &ipaHwSettingsV10;
 		break;
 	case RKISP1_V12:
-		hwHistBinNMax_ = RKISP1_CIF_ISP_HIST_BIN_N_MAX_V12;
-		hwGammaOutMaxSamples_ = RKISP1_CIF_ISP_GAMMA_OUT_MAX_SAMPLES_V12;
-		hwHistogramWeightGridsSize_ = RKISP1_CIF_ISP_HISTOGRAM_WEIGHT_GRIDS_SIZE_V12;
+		context_.hw = &ipaHwSettingsV12;
 		break;
 	default:
 		LOG(IPARkISP1, Error)
@@ -146,9 +147,6 @@
 
 	LOG(IPARkISP1, Debug) << "Hardware revision is " << hwRevision;
 
-	/* Cache the value to set it in configure. */
-	hwRevision_ = static_cast<rkisp1_cif_isp_version>(hwRevision);
-
 	camHelper_ = CameraSensorHelperFactoryBase::create(settings.sensorModel);
 	if (!camHelper_) {
 		LOG(IPARkISP1, Error)
@@ -232,9 +230,6 @@
 	context_.activeState = {};
 	context_.frameContexts.clear();
 
-	/* Set the hardware revision for the algorithms. */
-	context_.configuration.hw.revision = hwRevision_;
-
 	const IPACameraSensorInfo &info = ipaConfig.sensorInfo;
 	const ControlInfo vBlank = sensorControls_.find(V4L2_CID_VBLANK)->second;
 	context_.configuration.sensor.defVBlank = vBlank.def().get<int32_t>();
@@ -428,6 +423,7 @@
 							      frameDurations[1],
 							      frameDurations[2]);
 
+	ctrlMap.insert(context_.ctrlMap.begin(), context_.ctrlMap.end());
 	*ipaControls = ControlInfoMap(std::move(ctrlMap), controls::controls);
 }
 
@@ -459,7 +455,7 @@
 const struct IPAModuleInfo ipaModuleInfo = {
 	IPA_MODULE_API_VERSION,
 	1,
-	"PipelineHandlerRkISP1",
+	"rkisp1",
 	"rkisp1",
 };
 
diff --git a/src/ipa/rkisp1/utils.cpp b/src/ipa/rkisp1/utils.cpp
new file mode 100644
index 0000000..960ec64
--- /dev/null
+++ b/src/ipa/rkisp1/utils.cpp
@@ -0,0 +1,42 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Miscellaneous utility functions specific to rkisp1
+ */
+
+#include "utils.h"
+
+/**
+ * \file utils.h
+ */
+
+namespace libcamera {
+
+namespace ipa::rkisp1::utils {
+
+/**
+ * \fn R floatingToFixedPoint(T number)
+ * \brief Convert a floating point number to a fixed-point representation
+ * \tparam I Bit width of the integer part of the fixed-point
+ * \tparam F Bit width of the fractional part of the fixed-point
+ * \tparam R Return type of the fixed-point representation
+ * \tparam T Input type of the floating point representation
+ * \param number The floating point number to convert to fixed point
+ * \return The converted value
+ */
+
+/**
+ * \fn R fixedToFloatingPoint(T number)
+ * \brief Convert a fixed-point number to a floating point representation
+ * \tparam I Bit width of the integer part of the fixed-point
+ * \tparam F Bit width of the fractional part of the fixed-point
+ * \tparam R Return type of the floating point representation
+ * \tparam T Input type of the fixed-point representation
+ * \param number The fixed point number to convert to floating point
+ * \return The converted value
+ */
+
+} /* namespace ipa::rkisp1::utils */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rkisp1/utils.h b/src/ipa/rkisp1/utils.h
new file mode 100644
index 0000000..450f224
--- /dev/null
+++ b/src/ipa/rkisp1/utils.h
@@ -0,0 +1,66 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Miscellaneous utility functions specific to rkisp1
+ */
+
+#pragma once
+
+#include <cmath>
+#include <limits>
+#include <type_traits>
+
+namespace libcamera {
+
+namespace ipa::rkisp1::utils {
+
+#ifndef __DOXYGEN__
+template<unsigned int I, unsigned int F, typename R, typename T,
+	 std::enable_if_t<std::is_integral_v<R> &&
+			  std::is_floating_point_v<T>> * = nullptr>
+#else
+template<unsigned int I, unsigned int F, typename R, typename T>
+#endif
+constexpr R floatingToFixedPoint(T number)
+{
+	static_assert(sizeof(int) >= sizeof(R));
+	static_assert(I + F <= sizeof(R) * 8);
+
+	/*
+	 * The intermediate cast to int is needed on arm platforms to properly
+	 * cast negative values. See
+	 * https://embeddeduse.com/2013/08/25/casting-a-negative-float-to-an-unsigned-int/
+	 */
+	R mask = (1 << (F + I)) - 1;
+	R frac = static_cast<R>(static_cast<int>(std::round(number * (1 << F)))) & mask;
+
+	return frac;
+}
+
+#ifndef __DOXYGEN__
+template<unsigned int I, unsigned int F, typename R, typename T,
+	 std::enable_if_t<std::is_floating_point_v<R> &&
+			  std::is_integral_v<T>> * = nullptr>
+#else
+template<unsigned int I, unsigned int F, typename R, typename T>
+#endif
+constexpr R fixedToFloatingPoint(T number)
+{
+	static_assert(sizeof(int) >= sizeof(T));
+	static_assert(I + F <= sizeof(T) * 8);
+
+	/*
+	 * Recreate the upper bits in case of a negative number by shifting the sign
+	 * bit from the fixed point to the first bit of the unsigned and then right shifting
+	 * by the same amount which keeps the sign bit in place.
+	 * This can be optimized by the compiler quite well.
+	 */
+	int remaining_bits = sizeof(int) * 8 - (I + F);
+	int t = static_cast<int>(static_cast<unsigned>(number) << remaining_bits) >> remaining_bits;
+	return static_cast<R>(t) / static_cast<R>(1 << F);
+}
+
+} /* namespace ipa::rkisp1::utils */
+
+} /* namespace libcamera */
diff --git a/src/ipa/rpi/cam_helper/cam_helper.cpp b/src/ipa/rpi/cam_helper/cam_helper.cpp
index ddd5e9a..ee5d011 100644
--- a/src/ipa/rpi/cam_helper/cam_helper.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * cam_helper.cpp - helper information for different sensors
+ * helper information for different sensors
  */
 
 #include <linux/videodev2.h>
diff --git a/src/ipa/rpi/cam_helper/cam_helper.h b/src/ipa/rpi/cam_helper/cam_helper.h
index 58a4b20..4a4ab5e 100644
--- a/src/ipa/rpi/cam_helper/cam_helper.h
+++ b/src/ipa/rpi/cam_helper/cam_helper.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * cam_helper.h - helper class providing camera information
+ * helper class providing camera information
  */
 #pragma once
 
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx219.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx219.cpp
index c3337ed..91461f7 100644
--- a/src/ipa/rpi/cam_helper/cam_helper_imx219.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx219.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * cam_helper_imx219.cpp - camera helper for imx219 sensor
+ * camera helper for imx219 sensor
  */
 
 #include <assert.h>
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp
index d98b51c..24275e1 100644
--- a/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx290.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Raspberry Pi Ltd
  *
- * cam_helper_imx290.cpp - camera helper for imx290 sensor
+ * camera helper for imx290 sensor
  */
 
 #include <math.h>
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp
index ecb845e..d4a4fa7 100644
--- a/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx296.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * cam_helper_imx296.cpp - Camera helper for IMX296 sensor
+ * Camera helper for IMX296 sensor
  */
 
 #include <algorithm>
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp
index bc769ca..6bd8933 100644
--- a/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx477.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * cam_helper_imx477.cpp - camera helper for imx477 sensor
+ * camera helper for imx477 sensor
  */
 
 #include <algorithm>
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp
index c7262aa..c2de3d4 100644
--- a/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx519.cpp
@@ -3,7 +3,7 @@
  * Based on cam_helper_imx477.cpp
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * cam_helper_imx519.cpp - camera helper for imx519 sensor
+ * camera helper for imx519 sensor
  * Copyright (C) 2021, Arducam Technology co., Ltd.
  */
 
diff --git a/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp b/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp
index dce39cd..63ddb55 100644
--- a/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper_imx708.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Raspberry Pi Ltd
  *
- * cam_helper_imx708.cpp - camera helper for imx708 sensor
+ * camera helper for imx708 sensor
  */
 
 #include <cmath>
@@ -269,7 +269,7 @@
 {
 	size_t step = bpp >> 1; /* bytes per PDAF grid entry */
 
-	if (bpp < 10 || bpp > 12 || len < 194 * step || ptr[0] != 0 || ptr[1] >= 0x40) {
+	if (bpp < 10 || bpp > 14 || len < 194 * step || ptr[0] != 0 || ptr[1] >= 0x40) {
 		LOG(IPARPI, Error) << "PDAF data in unsupported format";
 		return false;
 	}
diff --git a/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp b/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp
index 5a99083..c30b017 100644
--- a/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper_ov5647.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * cam_helper_ov5647.cpp - camera information for ov5647 sensor
+ * camera information for ov5647 sensor
  */
 
 #include <assert.h>
diff --git a/src/ipa/rpi/cam_helper/cam_helper_ov64a40.cpp b/src/ipa/rpi/cam_helper/cam_helper_ov64a40.cpp
new file mode 100644
index 0000000..a8efd38
--- /dev/null
+++ b/src/ipa/rpi/cam_helper/cam_helper_ov64a40.cpp
@@ -0,0 +1,74 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2021, Raspberry Pi Ltd
+ * Copyright (C) 2023, Ideas on Board Oy.
+ *
+ * camera information for ov64a40 sensor
+ */
+
+#include <assert.h>
+
+#include "cam_helper.h"
+
+using namespace RPiController;
+
+class CamHelperOv64a40 : public CamHelper
+{
+public:
+	CamHelperOv64a40();
+	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;
+	double getModeSensitivity(const CameraMode &mode) const override;
+
+private:
+	/*
+	 * Smallest difference between the frame length and integration time,
+	 * in units of lines.
+	 */
+	static constexpr int frameIntegrationDiff = 32;
+};
+
+CamHelperOv64a40::CamHelperOv64a40()
+	: CamHelper({}, frameIntegrationDiff)
+{
+}
+
+uint32_t CamHelperOv64a40::gainCode(double gain) const
+{
+	return static_cast<uint32_t>(gain * 128.0);
+}
+
+double CamHelperOv64a40::gain(uint32_t gainCode) const
+{
+	return static_cast<double>(gainCode) / 128.0;
+}
+
+void CamHelperOv64a40::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;
+}
+
+double CamHelperOv64a40::getModeSensitivity(const CameraMode &mode) const
+{
+	if (mode.binX >= 2 && mode.scaleX >= 4) {
+		return 4.0;
+	} else if (mode.binX >= 2 && mode.scaleX >= 2) {
+		return 2.0;
+	} else {
+		return 1.0;
+	}
+}
+
+static CamHelper *create()
+{
+	return new CamHelperOv64a40();
+}
+
+static RegisterCamHelper reg("ov64a40", &create);
diff --git a/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp b/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp
index 86c5bc4..a65c8ac 100644
--- a/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp
+++ b/src/ipa/rpi/cam_helper/cam_helper_ov9281.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Raspberry Pi Ltd
  *
- * cam_helper_ov9281.cpp - camera information for ov9281 sensor
+ * camera information for ov9281 sensor
  */
 
 #include <assert.h>
diff --git a/src/ipa/rpi/cam_helper/md_parser.h b/src/ipa/rpi/cam_helper/md_parser.h
index 77d557a..227c376 100644
--- a/src/ipa/rpi/cam_helper/md_parser.h
+++ b/src/ipa/rpi/cam_helper/md_parser.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * md_parser.h - image sensor metadata parser interface
+ * image sensor metadata parser interface
  */
 #pragma once
 
diff --git a/src/ipa/rpi/cam_helper/md_parser_smia.cpp b/src/ipa/rpi/cam_helper/md_parser_smia.cpp
index 210787e..c7bdcf9 100644
--- a/src/ipa/rpi/cam_helper/md_parser_smia.cpp
+++ b/src/ipa/rpi/cam_helper/md_parser_smia.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2021, Raspberry Pi Ltd
  *
- * md_parser_smia.cpp - SMIA specification based embedded data parser
+ * SMIA specification based embedded data parser
  */
 
 #include <libcamera/base/log.h>
@@ -86,10 +86,13 @@
 	while (1) {
 		int tag = buffer[currentOffset++];
 
-		if ((bitsPerPixel_ == 10 &&
-		     (currentOffset + 1 - currentLineStart) % 5 == 0) ||
-		    (bitsPerPixel_ == 12 &&
-		     (currentOffset + 1 - currentLineStart) % 3 == 0)) {
+		/* Non-dummy bytes come in even-sized blocks: skip can only ever follow tag */
+		while ((bitsPerPixel_ == 10 &&
+			(currentOffset + 1 - currentLineStart) % 5 == 0) ||
+		       (bitsPerPixel_ == 12 &&
+			(currentOffset + 1 - currentLineStart) % 3 == 0) ||
+		       (bitsPerPixel_ == 14 &&
+			(currentOffset - currentLineStart) % 7 >= 4)) {
 			if (buffer[currentOffset++] != RegSkip)
 				return BadDummy;
 		}
diff --git a/src/ipa/rpi/cam_helper/meson.build b/src/ipa/rpi/cam_helper/meson.build
index bdf2db8..7262505 100644
--- a/src/ipa/rpi/cam_helper/meson.build
+++ b/src/ipa/rpi/cam_helper/meson.build
@@ -9,6 +9,7 @@
     'cam_helper_imx477.cpp',
     'cam_helper_imx519.cpp',
     'cam_helper_imx708.cpp',
+    'cam_helper_ov64a40.cpp',
     'cam_helper_ov9281.cpp',
     'md_parser_smia.cpp',
 ])
diff --git a/src/ipa/rpi/common/ipa_base.cpp b/src/ipa/rpi/common/ipa_base.cpp
index f7e7ad5..ee3848b 100644
--- a/src/ipa/rpi/common/ipa_base.cpp
+++ b/src/ipa/rpi/common/ipa_base.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2023, Raspberry Pi Ltd
  *
- * ipa_base.cpp - Raspberry Pi IPA base class
+ * Raspberry Pi IPA base class
  */
 
 #include "ipa_base.h"
@@ -24,6 +24,7 @@
 #include "controller/ccm_status.h"
 #include "controller/contrast_algorithm.h"
 #include "controller/denoise_algorithm.h"
+#include "controller/hdr_algorithm.h"
 #include "controller/lux_status.h"
 #include "controller/sharpen_algorithm.h"
 #include "controller/statistics.h"
@@ -67,10 +68,12 @@
 	{ &controls::AeFlickerPeriod, ControlInfo(100, 1000000) },
 	{ &controls::Brightness, ControlInfo(-1.0f, 1.0f, 0.0f) },
 	{ &controls::Contrast, ControlInfo(0.0f, 32.0f, 1.0f) },
+	{ &controls::HdrMode, ControlInfo(controls::HdrModeValues) },
 	{ &controls::Sharpness, ControlInfo(0.0f, 16.0f, 1.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) }
+	{ &controls::draft::NoiseReductionMode, ControlInfo(controls::draft::NoiseReductionModeValues) },
+	{ &controls::rpi::StatsOutputEnable, ControlInfo(false, true, false) },
 };
 
 /* IPA controls handled conditionally, if the sensor is not mono */
@@ -100,7 +103,8 @@
 namespace ipa::RPi {
 
 IpaBase::IpaBase()
-	: controller_(), frameCount_(0), mistrustCount_(0), lastRunTimestamp_(0),
+	: controller_(), frameLengths_(FrameLengthsQueueSize, 0s), statsMetadataOutput_(false),
+	  stitchSwapBuffers_(false), frameCount_(0), mistrustCount_(0), lastRunTimestamp_(0),
 	  firstStart_(true), flickerState_({ 0, 0s })
 {
 }
@@ -294,6 +298,8 @@
 		result->controls = std::move(ctrls);
 		setCameraTimeoutValue();
 	}
+	/* Make a note of this as it tells us the HDR status of the first few frames. */
+	hdrStatus_ = agcStatus.hdr;
 
 	/*
 	 * Initialise frame counts, and decide how many frames must be hidden or
@@ -341,6 +347,8 @@
 
 	firstStart_ = false;
 	lastRunTimestamp_ = 0;
+
+	platformStart(controls, result);
 }
 
 void IpaBase::mapBuffers(const std::vector<IPABuffer> &buffers)
@@ -395,11 +403,17 @@
 	 * 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.
+	 *
+	 * Note if the HDR mode has changed, as things like tonemaps may need updating.
 	 */
 	AgcStatus agcStatus;
+	bool hdrChange = false;
 	RPiController::Metadata &delayedMetadata = rpiMetadata_[params.delayContext];
-	if (!delayedMetadata.get<AgcStatus>("agc.status", agcStatus))
+	if (!delayedMetadata.get<AgcStatus>("agc.status", agcStatus)) {
 		rpiMetadata.set("agc.delayed_status", agcStatus);
+		hdrChange = agcStatus.hdr.mode != hdrStatus_.mode;
+		hdrStatus_ = agcStatus.hdr;
+	}
 
 	/*
 	 * This may overwrite the DeviceStatus using values from the sensor
@@ -410,7 +424,7 @@
 	/* Allow a 10% margin on the comparison below. */
 	Duration delta = (frameTimestamp - lastRunTimestamp_) * 1.0ns;
 	if (lastRunTimestamp_ && frameCount_ > dropFrameCount_ &&
-	    delta < controllerMinFrameDuration * 0.9) {
+	    delta < controllerMinFrameDuration * 0.9 && !hdrChange) {
 		/*
 		 * Ensure we merge the previous frame's metadata with the current
 		 * frame. This will not overwrite exposure/gain values for the
@@ -427,11 +441,10 @@
 	}
 
 	/*
-	 * If a statistics buffer has been passed in, call processStats
-	 * directly now before prepare() since the statistics are available in-line
-	 * with the Bayer frame.
+	 * If the statistics are inline (i.e. already available with the Bayer
+	 * frame), call processStats() now before prepare().
 	 */
-	if (params.buffers.stats)
+	if (controller_.getHardwareConfig().statsInline)
 		processStats({ params.buffers, params.ipaContext });
 
 	/* Do we need/want to call prepare? */
@@ -443,15 +456,19 @@
 
 	frameCount_++;
 
+	/* If the statistics are inline the metadata can be returned early. */
+	if (controller_.getHardwareConfig().statsInline)
+		reportMetadata(ipaContext);
+
 	/* Ready to push the input buffer into the ISP. */
-	prepareIspComplete.emit(params.buffers);
+	prepareIspComplete.emit(params.buffers, stitchSwapBuffers_);
 }
 
 void IpaBase::processStats(const ProcessParams &params)
 {
 	unsigned int ipaContext = params.ipaContext % rpiMetadata_.size();
 
-	if (processPending_ && frameCount_ > mistrustCount_) {
+	if (processPending_ && frameCount_ >= mistrustCount_) {
 		RPiController::Metadata &rpiMetadata = rpiMetadata_[ipaContext];
 
 		auto it = buffers_.find(params.buffers.stats);
@@ -477,7 +494,13 @@
 		}
 	}
 
-	reportMetadata(ipaContext);
+	/*
+	 * If the statistics are not inline the metadata must be returned now,
+	 * before the processStatsComplete signal.
+	 */
+	if (!controller_.getHardwareConfig().statsInline)
+		reportMetadata(ipaContext);
+
 	processStatsComplete.emit(params.buffers);
 }
 
@@ -521,6 +544,33 @@
 	mode_.maxLineLength = sensorInfo.maxLineLength * (1.0s / sensorInfo.pixelRate);
 
 	/*
+	 * Ensure that the maximum pixel processing rate does not exceed the ISP
+	 * hardware capabilities. If it does, try adjusting the minimum line
+	 * length to compensate if possible.
+	 */
+	Duration minPixelTime = controller_.getHardwareConfig().minPixelProcessingTime;
+	Duration pixelTime = mode_.minLineLength / mode_.width;
+	if (minPixelTime && pixelTime < minPixelTime) {
+		Duration adjustedLineLength = minPixelTime * mode_.width;
+		if (adjustedLineLength <= mode_.maxLineLength) {
+			LOG(IPARPI, Info)
+				<< "Adjusting mode minimum line length from " << mode_.minLineLength
+				<< " to " << adjustedLineLength << " because of ISP constraints.";
+			mode_.minLineLength = adjustedLineLength;
+		} else {
+			LOG(IPARPI, Error)
+				<< "Sensor minimum line length of " << pixelTime * mode_.width
+				<< " (" << 1us / pixelTime << " MPix/s)"
+				<< " is below the minimum allowable ISP limit of "
+				<< adjustedLineLength
+				<< " (" << 1us / minPixelTime << " MPix/s) ";
+			LOG(IPARPI, Error)
+				<< "THIS WILL CAUSE IMAGE CORRUPTION!!! "
+				<< "Please update the camera sensor driver to allow more horizontal blanking control.";
+		}
+	}
+
+	/*
 	 * Set the frame length limits for the mode to ensure exposure and
 	 * framerate calculations are clipped appropriately.
 	 */
@@ -543,6 +593,12 @@
 	mode_.minAnalogueGain = helper_->gain(gainCtrl.min().get<int32_t>());
 	mode_.maxAnalogueGain = helper_->gain(gainCtrl.max().get<int32_t>());
 
+	/*
+	 * We need to give the helper the min/max frame durations so it can calculate
+	 * the correct exposure limits below.
+	 */
+	helper_->setCameraMode(mode_);
+
 	/* 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();
@@ -632,14 +688,6 @@
 	{ 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 },
@@ -658,9 +706,21 @@
 	{ controls::AfPauseResume, RPiController::AfAlgorithm::AfPauseResume },
 };
 
+static const std::map<int32_t, std::string> HdrModeTable = {
+	{ controls::HdrModeOff, "Off" },
+	{ controls::HdrModeMultiExposureUnmerged, "MultiExposureUnmerged" },
+	{ controls::HdrModeMultiExposure, "MultiExposure" },
+	{ controls::HdrModeSingleExposure, "SingleExposure" },
+	{ controls::HdrModeNight, "Night" },
+};
+
 void IpaBase::applyControls(const ControlList &controls)
 {
+	using RPiController::AgcAlgorithm;
 	using RPiController::AfAlgorithm;
+	using RPiController::ContrastAlgorithm;
+	using RPiController::DenoiseAlgorithm;
+	using RPiController::HdrAlgorithm;
 
 	/* Clear the return metadata buffer. */
 	libcameraMetadata_.clear();
@@ -699,9 +759,9 @@
 			}
 
 			if (ctrl.second.get<bool>() == false)
-				agc->disableAuto(0);
+				agc->disableAuto();
 			else
-				agc->enableAuto(0);
+				agc->enableAuto();
 
 			libcameraMetadata_.set(controls::AeEnable, ctrl.second.get<bool>());
 			break;
@@ -770,7 +830,7 @@
 
 			int32_t idx = ctrl.second.get<int32_t>();
 			if (ConstraintModeTable.count(idx)) {
-				agc->setConstraintMode(0, ConstraintModeTable.at(idx));
+				agc->setConstraintMode(ConstraintModeTable.at(idx));
 				libcameraMetadata_.set(controls::AeConstraintMode, idx);
 			} else {
 				LOG(IPARPI, Error) << "Constraint mode " << idx
@@ -790,7 +850,7 @@
 
 			int32_t idx = ctrl.second.get<int32_t>();
 			if (ExposureModeTable.count(idx)) {
-				agc->setExposureMode(0, ExposureModeTable.at(idx));
+				agc->setExposureMode(ExposureModeTable.at(idx));
 				libcameraMetadata_.set(controls::AeExposureMode, idx);
 			} else {
 				LOG(IPARPI, Error) << "Exposure mode " << idx
@@ -833,12 +893,12 @@
 
 			switch (mode) {
 			case controls::FlickerOff:
-				agc->setFlickerPeriod(0, 0us);
+				agc->setFlickerPeriod(0us);
 
 				break;
 
 			case controls::FlickerManual:
-				agc->setFlickerPeriod(0, flickerState_.manualPeriod);
+				agc->setFlickerPeriod(flickerState_.manualPeriod);
 
 				break;
 
@@ -872,7 +932,7 @@
 			 * first, and the period updated after, or vice versa.
 			 */
 			if (flickerState_.mode == controls::FlickerManual)
-				agc->setFlickerPeriod(0, flickerState_.manualPeriod);
+				agc->setFlickerPeriod(flickerState_.manualPeriod);
 
 			break;
 		}
@@ -1021,36 +1081,11 @@
 			break;
 		}
 
-		case controls::NOISE_REDUCTION_MODE: {
-			RPiController::DenoiseAlgorithm *sdn = dynamic_cast<RPiController::DenoiseAlgorithm *>(
-				controller_.getAlgorithm("SDN"));
-			/* Some platforms may have a combined "denoise" algorithm instead. */
-			if (!sdn)
-				sdn = dynamic_cast<RPiController::DenoiseAlgorithm *>(
-				controller_.getAlgorithm("denoise"));
-			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";
-			}
+		case controls::draft::NOISE_REDUCTION_MODE:
+			/* Handled below in handleControls() */
+			libcameraMetadata_.set(controls::draft::NoiseReductionMode,
+					       ctrl.second.get<int32_t>());
 			break;
-		}
 
 		case controls::AF_MODE:
 			break; /* We already handled this one above */
@@ -1157,6 +1192,61 @@
 			break;
 		}
 
+		case controls::HDR_MODE: {
+			HdrAlgorithm *hdr = dynamic_cast<HdrAlgorithm *>(controller_.getAlgorithm("hdr"));
+			if (!hdr) {
+				LOG(IPARPI, Warning) << "No HDR algorithm available";
+				break;
+			}
+
+			auto mode = HdrModeTable.find(ctrl.second.get<int32_t>());
+			if (mode == HdrModeTable.end()) {
+				LOG(IPARPI, Warning) << "Unrecognised HDR mode";
+				break;
+			}
+
+			AgcAlgorithm *agc = dynamic_cast<AgcAlgorithm *>(controller_.getAlgorithm("agc"));
+			if (!agc) {
+				LOG(IPARPI, Warning) << "HDR requires an AGC algorithm";
+				break;
+			}
+
+			if (hdr->setMode(mode->second) == 0) {
+				agc->setActiveChannels(hdr->getChannels());
+
+				/* We also disable adpative contrast enhancement if HDR is running. */
+				ContrastAlgorithm *contrast =
+					dynamic_cast<ContrastAlgorithm *>(controller_.getAlgorithm("contrast"));
+				if (contrast) {
+					if (mode->second == "Off")
+						contrast->restoreCe();
+					else
+						contrast->enableCe(false);
+				}
+
+				DenoiseAlgorithm *denoise =
+					dynamic_cast<DenoiseAlgorithm *>(controller_.getAlgorithm("denoise"));
+				if (denoise) {
+					/* \todo - make the HDR mode say what denoise it wants? */
+					if (mode->second == "Night")
+						denoise->setConfig("night");
+					else if (mode->second == "SingleExposure")
+						denoise->setConfig("hdr");
+					/* MultiExposure doesn't need extra extra denoise. */
+					else
+						denoise->setConfig("normal");
+				}
+			} else
+				LOG(IPARPI, Warning)
+					<< "HDR mode " << mode->second << " not supported";
+
+			break;
+		}
+
+		case controls::rpi::STATS_OUTPUT_ENABLE:
+			statsMetadataOutput_ = ctrl.second.get<bool>();
+			break;
+
 		default:
 			LOG(IPARPI, Warning)
 				<< "Ctrl " << controls::controls.at(ctrl.first)->name()
@@ -1304,6 +1394,35 @@
 		libcameraMetadata_.set(controls::AfPauseState, p);
 	}
 
+	/*
+	 * THe HDR algorithm sets the HDR channel into the agc.status at the time that those
+	 * AGC parameters were calculated several frames ago, so it comes back to us now in
+	 * the delayed_status. If this frame is too soon after a mode switch for the
+	 * delayed_status to be available, we use the HDR status that came out of the
+	 * switchMode call.
+	 */
+	const AgcStatus *agcStatus = rpiMetadata.getLocked<AgcStatus>("agc.delayed_status");
+	const HdrStatus &hdrStatus = agcStatus ? agcStatus->hdr : hdrStatus_;
+	if (!hdrStatus.mode.empty() && hdrStatus.mode != "Off") {
+		int32_t hdrMode = controls::HdrModeOff;
+		for (auto const &[mode, name] : HdrModeTable) {
+			if (hdrStatus.mode == name) {
+				hdrMode = mode;
+				break;
+			}
+		}
+		libcameraMetadata_.set(controls::HdrMode, hdrMode);
+
+		if (hdrStatus.channel == "short")
+			libcameraMetadata_.set(controls::HdrChannel, controls::HdrChannelShort);
+		else if (hdrStatus.channel == "long")
+			libcameraMetadata_.set(controls::HdrChannel, controls::HdrChannelLong);
+		else if (hdrStatus.channel == "medium")
+			libcameraMetadata_.set(controls::HdrChannel, controls::HdrChannelMedium);
+		else
+			libcameraMetadata_.set(controls::HdrChannel, controls::HdrChannelNone);
+	}
+
 	metadataReady.emit(libcameraMetadata_);
 }
 
diff --git a/src/ipa/rpi/common/ipa_base.h b/src/ipa/rpi/common/ipa_base.h
index 097f436..1a811be 100644
--- a/src/ipa/rpi/common/ipa_base.h
+++ b/src/ipa/rpi/common/ipa_base.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2023, Raspberry Pi Ltd
  *
- * ipa_base.h - Raspberry Pi IPA base class
+ * Raspberry Pi IPA base class
  */
 #pragma once
 
@@ -22,6 +22,7 @@
 #include "controller/agc_status.h"
 #include "controller/camera_mode.h"
 #include "controller/controller.h"
+#include "controller/hdr_status.h"
 #include "controller/metadata.h"
 
 namespace libcamera {
@@ -48,6 +49,11 @@
 	void processStats(const ProcessParams &params) override;
 
 protected:
+	bool monoSensor() const
+	{
+		return monoSensor_;
+	}
+
 	/* Raspberry Pi controller specific defines. */
 	std::unique_ptr<RPiController::CamHelper> helper_;
 	RPiController::Controller controller_;
@@ -61,12 +67,21 @@
 	/* Track the frame length times over FrameLengthsQueueSize frames. */
 	std::deque<utils::Duration> frameLengths_;
 	utils::Duration lastTimeout_;
+	ControlList libcameraMetadata_;
+	bool statsMetadataOutput_;
+
+	/* Remember the HDR status after a mode switch. */
+	HdrStatus hdrStatus_;
+
+	/* Whether the stitch block (if available) needs to swap buffers. */
+	bool stitchSwapBuffers_;
 
 private:
 	/* Number of metadata objects available in the context list. */
 	static constexpr unsigned int numMetadataContexts = 16;
 
 	virtual int32_t platformInit(const InitParams &params, InitResult *result) = 0;
+	virtual int32_t platformStart(const ControlList &controls, StartResult *result) = 0;
 	virtual int32_t platformConfigure(const ConfigParams &params, ConfigResult *result) = 0;
 
 	virtual void platformPrepareIsp(const PrepareParams &params,
@@ -88,7 +103,6 @@
 
 	bool lensPresent_;
 	bool monoSensor_;
-	ControlList libcameraMetadata_;
 
 	std::array<RPiController::Metadata, numMetadataContexts> rpiMetadata_;
 
diff --git a/src/ipa/rpi/controller/af_status.h b/src/ipa/rpi/controller/af_status.h
index 92c0881..c1487cc 100644
--- a/src/ipa/rpi/controller/af_status.h
+++ b/src/ipa/rpi/controller/af_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Raspberry Pi Ltd
  *
- * af_status.h - AF control algorithm status
+ * AF control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/agc_algorithm.h b/src/ipa/rpi/controller/agc_algorithm.h
index b898656..1132de7 100644
--- a/src/ipa/rpi/controller/agc_algorithm.h
+++ b/src/ipa/rpi/controller/agc_algorithm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * agc_algorithm.h - AGC/AEC control algorithm interface
+ * AGC/AEC control algorithm interface
  */
 #pragma once
 
@@ -22,17 +22,16 @@
 	virtual unsigned int getConvergenceFrames() const = 0;
 	virtual std::vector<double> const &getWeights() const = 0;
 	virtual void setEv(unsigned int channel, double ev) = 0;
-	virtual void setFlickerPeriod(unsigned int channel,
-				      libcamera::utils::Duration flickerPeriod) = 0;
+	virtual void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) = 0;
 	virtual void setFixedShutter(unsigned int channel,
 				     libcamera::utils::Duration fixedShutter) = 0;
 	virtual void setMaxShutter(libcamera::utils::Duration maxShutter) = 0;
 	virtual void setFixedAnalogueGain(unsigned int channel, double fixedAnalogueGain) = 0;
 	virtual void setMeteringMode(std::string const &meteringModeName) = 0;
-	virtual void setExposureMode(unsigned int channel, std::string const &exposureModeName) = 0;
-	virtual void setConstraintMode(unsigned int channel, std::string const &contraintModeName) = 0;
-	virtual void enableAuto(unsigned int channel) = 0;
-	virtual void disableAuto(unsigned int channel) = 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;
 	virtual void setActiveChannels(const std::vector<unsigned int> &activeChannels) = 0;
 };
 
diff --git a/src/ipa/rpi/controller/agc_status.h b/src/ipa/rpi/controller/agc_status.h
index e5c4ee2..c7c87b8 100644
--- a/src/ipa/rpi/controller/agc_status.h
+++ b/src/ipa/rpi/controller/agc_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * agc_status.h - AGC/AEC control algorithm status
+ * AGC/AEC control algorithm status
  */
 #pragma once
 
@@ -10,6 +10,8 @@
 
 #include <libcamera/base/utils.h>
 
+#include "hdr_status.h"
+
 /*
  * The AGC algorithm process method should post an AgcStatus into the image
  * metadata under the tag "agc.status".
@@ -37,6 +39,7 @@
 	libcamera::utils::Duration fixedShutter;
 	double fixedAnalogueGain;
 	unsigned int channel;
+	HdrStatus hdr;
 };
 
 struct AgcPrepareStatus {
diff --git a/src/ipa/rpi/controller/algorithm.cpp b/src/ipa/rpi/controller/algorithm.cpp
index a957fde..beed47a 100644
--- a/src/ipa/rpi/controller/algorithm.cpp
+++ b/src/ipa/rpi/controller/algorithm.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * algorithm.cpp - ISP control algorithms
+ * ISP control algorithms
  */
 
 #include "algorithm.h"
diff --git a/src/ipa/rpi/controller/algorithm.h b/src/ipa/rpi/controller/algorithm.h
index 4aa814e..1971bfd 100644
--- a/src/ipa/rpi/controller/algorithm.h
+++ b/src/ipa/rpi/controller/algorithm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * algorithm.h - ISP control algorithm interface
+ * ISP control algorithm interface
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/alsc_status.h b/src/ipa/rpi/controller/alsc_status.h
index 49a9f4a..329e8a3 100644
--- a/src/ipa/rpi/controller/alsc_status.h
+++ b/src/ipa/rpi/controller/alsc_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * alsc_status.h - ALSC (auto lens shading correction) control algorithm status
+ * ALSC (auto lens shading correction) control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/awb_algorithm.h b/src/ipa/rpi/controller/awb_algorithm.h
index 8462c4d..1779b05 100644
--- a/src/ipa/rpi/controller/awb_algorithm.h
+++ b/src/ipa/rpi/controller/awb_algorithm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * awb_algorithm.h - AWB control algorithm interface
+ * AWB control algorithm interface
  */
 #pragma once
 
@@ -16,6 +16,7 @@
 	AwbAlgorithm(Controller *controller) : Algorithm(controller) {}
 	/* An AWB algorithm must provide the following: */
 	virtual unsigned int getConvergenceFrames() const = 0;
+	virtual void initialValues(double &gainR, double &gainB) = 0;
 	virtual void setMode(std::string const &modeName) = 0;
 	virtual void setManualGains(double manualR, double manualB) = 0;
 	virtual void enableAuto() = 0;
diff --git a/src/ipa/rpi/controller/awb_status.h b/src/ipa/rpi/controller/awb_status.h
index dd5a79e..125df1a 100644
--- a/src/ipa/rpi/controller/awb_status.h
+++ b/src/ipa/rpi/controller/awb_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * awb_status.h - AWB control algorithm status
+ * AWB control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/black_level_algorithm.h b/src/ipa/rpi/controller/black_level_algorithm.h
new file mode 100644
index 0000000..ce044e5
--- /dev/null
+++ b/src/ipa/rpi/controller/black_level_algorithm.h
@@ -0,0 +1,23 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2023, Raspberry Pi Ltd
+ *
+ * black level control algorithm interface
+ */
+#pragma once
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+class BlackLevelAlgorithm : public Algorithm
+{
+public:
+	BlackLevelAlgorithm(Controller *controller)
+		: Algorithm(controller) {}
+	/* A black level algorithm must provide the following: */
+	virtual void initialValues(uint16_t &blackLevelR, uint16_t &blackLevelG,
+				   uint16_t &blackLevelB) = 0;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/black_level_status.h b/src/ipa/rpi/controller/black_level_status.h
index fd5e4cc..57a0705 100644
--- a/src/ipa/rpi/controller/black_level_status.h
+++ b/src/ipa/rpi/controller/black_level_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * black_level_status.h - black level control algorithm status
+ * black level control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/cac_status.h b/src/ipa/rpi/controller/cac_status.h
new file mode 100644
index 0000000..adffce4
--- /dev/null
+++ b/src/ipa/rpi/controller/cac_status.h
@@ -0,0 +1,14 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2023 Raspberry Pi Ltd
+ *
+ * CAC (Chromatic Abberation Correction) algorithm status
+ */
+#pragma once
+
+struct CacStatus {
+	std::vector<double> lutRx;
+	std::vector<double> lutRy;
+	std::vector<double> lutBx;
+	std::vector<double> lutBy;
+};
diff --git a/src/ipa/rpi/controller/camera_mode.h b/src/ipa/rpi/controller/camera_mode.h
index 63b1177..4fdb5b8 100644
--- a/src/ipa/rpi/controller/camera_mode.h
+++ b/src/ipa/rpi/controller/camera_mode.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2020, Raspberry Pi Ltd
  *
- * camera_mode.h - description of a particular operating mode of a sensor
+ * description of a particular operating mode of a sensor
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/ccm_algorithm.h b/src/ipa/rpi/controller/ccm_algorithm.h
index e2c4d77..6678ba7 100644
--- a/src/ipa/rpi/controller/ccm_algorithm.h
+++ b/src/ipa/rpi/controller/ccm_algorithm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * ccm_algorithm.h - CCM (colour correction matrix) control algorithm interface
+ * CCM (colour correction matrix) control algorithm interface
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/ccm_status.h b/src/ipa/rpi/controller/ccm_status.h
index 5e28ee7..c81bcd4 100644
--- a/src/ipa/rpi/controller/ccm_status.h
+++ b/src/ipa/rpi/controller/ccm_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * ccm_status.h - CCM (colour correction matrix) control algorithm status
+ * CCM (colour correction matrix) control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/contrast_algorithm.h b/src/ipa/rpi/controller/contrast_algorithm.h
index ce17a4f..2e98335 100644
--- a/src/ipa/rpi/controller/contrast_algorithm.h
+++ b/src/ipa/rpi/controller/contrast_algorithm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * contrast_algorithm.h - contrast (gamma) control algorithm interface
+ * contrast (gamma) control algorithm interface
  */
 #pragma once
 
@@ -17,6 +17,8 @@
 	/* A contrast algorithm must provide the following: */
 	virtual void setBrightness(double brightness) = 0;
 	virtual void setContrast(double contrast) = 0;
+	virtual void enableCe(bool enable) = 0;
+	virtual void restoreCe() = 0;
 };
 
 } /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/contrast_status.h b/src/ipa/rpi/controller/contrast_status.h
index fb9fe4b..1f17587 100644
--- a/src/ipa/rpi/controller/contrast_status.h
+++ b/src/ipa/rpi/controller/contrast_status.h
@@ -2,11 +2,11 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * contrast_status.h - contrast (gamma) control algorithm status
+ * contrast (gamma) control algorithm status
  */
 #pragma once
 
-#include "pwl.h"
+#include "libipa/pwl.h"
 
 /*
  * The "contrast" algorithm creates a gamma curve, optionally doing a little bit
@@ -14,7 +14,7 @@
  */
 
 struct ContrastStatus {
-	RPiController::Pwl gammaCurve;
+	libcamera::ipa::Pwl gammaCurve;
 	double brightness;
 	double contrast;
 };
diff --git a/src/ipa/rpi/controller/controller.cpp b/src/ipa/rpi/controller/controller.cpp
index 14d245d..e013101 100644
--- a/src/ipa/rpi/controller/controller.cpp
+++ b/src/ipa/rpi/controller/controller.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * controller.cpp - ISP controller
+ * ISP controller
  */
 
 #include <assert.h>
@@ -17,6 +17,7 @@
 
 using namespace RPiController;
 using namespace libcamera;
+using namespace std::literals::chrono_literals;
 
 LOG_DEFINE_CATEGORY(RPiController)
 
@@ -31,10 +32,45 @@
 			.agcRegions = { 15 , 1 },
 			.agcZoneWeights = { 15 , 1 },
 			.awbRegions = { 16, 12 },
+			.cacRegions = { 0, 0 },
 			.focusRegions = { 4, 3 },
 			.numHistogramBins = 128,
 			.numGammaPoints = 33,
-			.pipelineWidth = 13
+			.pipelineWidth = 13,
+			.statsInline = false,
+			.minPixelProcessingTime = 0s,
+		}
+	},
+	{
+		"pisp",
+		{
+			.agcRegions = { 0, 0 },
+			.agcZoneWeights = { 15, 15 },
+			.awbRegions = { 32, 32 },
+			.cacRegions = { 8, 8 },
+			.focusRegions = { 8, 8 },
+			.numHistogramBins = 1024,
+			.numGammaPoints = 64,
+			.pipelineWidth = 16,
+			.statsInline = true,
+
+			/*
+			 * The constraint below is on the rate of pixels going
+			 * from CSI2 peripheral to ISP-FE (400Mpix/s, plus tiny
+			 * overheads per scanline, for which 380Mpix/s is a
+			 * conservative bound).
+			 *
+			 * There is a 64kbit data FIFO before the bottleneck,
+			 * which means that in all reasonable cases the
+			 * constraint applies at a timescale >= 1 scanline, so
+			 * adding horizontal blanking can prevent loss.
+			 *
+			 * If the backlog were to grow beyond 64kbit during a
+			 * single scanline, there could still be loss. This
+			 * could happen using 4 lanes at 1.5Gbps at 10bpp with
+			 * frames wider than ~16,000 pixels.
+			 */
+			.minPixelProcessingTime = 1.0us / 380,
 		}
 	},
 };
diff --git a/src/ipa/rpi/controller/controller.h b/src/ipa/rpi/controller/controller.h
index c6af5cd..eff520b 100644
--- a/src/ipa/rpi/controller/controller.h
+++ b/src/ipa/rpi/controller/controller.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * controller.h - ISP controller interface
+ * ISP controller interface
  */
 #pragma once
 
@@ -15,6 +15,7 @@
 #include <vector>
 #include <string>
 
+#include <libcamera/base/utils.h>
 #include "libcamera/internal/yaml_parser.h"
 
 #include "camera_mode.h"
@@ -41,10 +42,13 @@
 		libcamera::Size agcRegions;
 		libcamera::Size agcZoneWeights;
 		libcamera::Size awbRegions;
+		libcamera::Size cacRegions;
 		libcamera::Size focusRegions;
 		unsigned int numHistogramBins;
 		unsigned int numGammaPoints;
 		unsigned int pipelineWidth;
+		bool statsInline;
+		libcamera::utils::Duration minPixelProcessingTime;
 	};
 
 	Controller();
diff --git a/src/ipa/rpi/controller/denoise_algorithm.h b/src/ipa/rpi/controller/denoise_algorithm.h
index 52009ba..b9a2a33 100644
--- a/src/ipa/rpi/controller/denoise_algorithm.h
+++ b/src/ipa/rpi/controller/denoise_algorithm.h
@@ -2,10 +2,12 @@
 /*
  * Copyright (C) 2021, Raspberry Pi Ltd
  *
- * denoise.h - Denoise control algorithm interface
+ * Denoise control algorithm interface
  */
 #pragma once
 
+#include <string>
+
 #include "algorithm.h"
 
 namespace RPiController {
@@ -18,6 +20,8 @@
 	DenoiseAlgorithm(Controller *controller) : Algorithm(controller) {}
 	/* A Denoise algorithm must provide the following: */
 	virtual void setMode(DenoiseMode mode) = 0;
+	/* Some platforms may not be able to define this, so supply a default. */
+	virtual void setConfig([[maybe_unused]] std::string const &name) {}
 };
 
 } /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/denoise_status.h b/src/ipa/rpi/controller/denoise_status.h
index f6b9ee2..eead608 100644
--- a/src/ipa/rpi/controller/denoise_status.h
+++ b/src/ipa/rpi/controller/denoise_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2021, Raspberry Pi Ltd
  *
- * denoise_status.h - Denoise control algorithm status
+ * Denoise control algorithm status
  */
 #pragma once
 
@@ -14,3 +14,22 @@
 	double strength;
 	unsigned int mode;
 };
+
+struct SdnStatus {
+	double noiseConstant;
+	double noiseSlope;
+	double noiseConstant2;
+	double noiseSlope2;
+	double strength;
+};
+
+struct CdnStatus {
+	double strength;
+	double threshold;
+};
+
+struct TdnStatus {
+	double noiseConstant;
+	double noiseSlope;
+	double threshold;
+};
diff --git a/src/ipa/rpi/controller/device_status.cpp b/src/ipa/rpi/controller/device_status.cpp
index c907efd..6810013 100644
--- a/src/ipa/rpi/controller/device_status.cpp
+++ b/src/ipa/rpi/controller/device_status.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Raspberry Pi Ltd
  *
- * device_status.cpp - device (image sensor) status
+ * device (image sensor) status
  */
 #include "device_status.h"
 
diff --git a/src/ipa/rpi/controller/device_status.h b/src/ipa/rpi/controller/device_status.h
index c45db74..518f15b 100644
--- a/src/ipa/rpi/controller/device_status.h
+++ b/src/ipa/rpi/controller/device_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2021, Raspberry Pi Ltd
  *
- * device_status.h - device (image sensor) status
+ * device (image sensor) status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/dpc_status.h b/src/ipa/rpi/controller/dpc_status.h
index 46d0cf3..9f30d5d 100644
--- a/src/ipa/rpi/controller/dpc_status.h
+++ b/src/ipa/rpi/controller/dpc_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * dpc_status.h - DPC (defective pixel correction) control algorithm status
+ * DPC (defective pixel correction) control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/geq_status.h b/src/ipa/rpi/controller/geq_status.h
index 2d749fc..cb107a4 100644
--- a/src/ipa/rpi/controller/geq_status.h
+++ b/src/ipa/rpi/controller/geq_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * geq_status.h - GEQ (green equalisation) control algorithm status
+ * GEQ (green equalisation) control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/hdr_algorithm.h b/src/ipa/rpi/controller/hdr_algorithm.h
new file mode 100644
index 0000000..b889d8f
--- /dev/null
+++ b/src/ipa/rpi/controller/hdr_algorithm.h
@@ -0,0 +1,25 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2023, Raspberry Pi Ltd
+ *
+ * HDR control algorithm interface
+ */
+#pragma once
+
+#include <vector>
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+class HdrAlgorithm : public Algorithm
+{
+public:
+	HdrAlgorithm(Controller *controller)
+		: Algorithm(controller) {}
+	/* An HDR algorithm must provide the following: */
+	virtual int setMode(std::string const &modeName) = 0;
+	virtual std::vector<unsigned int> getChannels() const = 0;
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/hdr_status.h b/src/ipa/rpi/controller/hdr_status.h
new file mode 100644
index 0000000..a495577
--- /dev/null
+++ b/src/ipa/rpi/controller/hdr_status.h
@@ -0,0 +1,19 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2023 Raspberry Pi Ltd
+ *
+ * HDR control algorithm status
+ */
+#pragma once
+
+#include <string>
+
+/*
+ * The HDR algorithm process method should post an HdrStatus into the image
+ * metadata under the tag "hdr.status".
+ */
+
+struct HdrStatus {
+	std::string mode;
+	std::string channel;
+};
diff --git a/src/ipa/rpi/controller/histogram.cpp b/src/ipa/rpi/controller/histogram.cpp
index 0a27ba2..ba5b25d 100644
--- a/src/ipa/rpi/controller/histogram.cpp
+++ b/src/ipa/rpi/controller/histogram.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * histogram.cpp - histogram calculations
+ * histogram calculations
  */
 #include <math.h>
 #include <stdio.h>
@@ -47,7 +47,7 @@
 
 double Histogram::interBinMean(double binLo, double binHi) const
 {
-	assert(binHi > binLo);
+	assert(binHi >= binLo);
 	double sumBinFreq = 0, cumulFreq = 0;
 	for (double binNext = floor(binLo) + 1.0; binNext <= ceil(binHi);
 	     binLo = binNext, binNext += 1.0) {
@@ -57,13 +57,19 @@
 		sumBinFreq += bin * freq;
 		cumulFreq += freq;
 	}
+
+	if (cumulFreq == 0) {
+		/* interval had zero width or contained no weight? */
+		return binHi;
+	}
+
 	/* add 0.5 to give an average for bin mid-points */
 	return sumBinFreq / cumulFreq + 0.5;
 }
 
 double Histogram::interQuantileMean(double qLo, double qHi) const
 {
-	assert(qHi > qLo);
+	assert(qHi >= qLo);
 	double pLo = quantile(qLo);
 	double pHi = quantile(qHi, (int)pLo);
 	return interBinMean(pLo, pHi);
diff --git a/src/ipa/rpi/controller/histogram.h b/src/ipa/rpi/controller/histogram.h
index e2c5509..ab4e5e3 100644
--- a/src/ipa/rpi/controller/histogram.h
+++ b/src/ipa/rpi/controller/histogram.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * histogram.h - histogram calculation interface
+ * histogram calculation interface
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/lux_status.h b/src/ipa/rpi/controller/lux_status.h
index 5eb9faa..d8729f4 100644
--- a/src/ipa/rpi/controller/lux_status.h
+++ b/src/ipa/rpi/controller/lux_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * lux_status.h - Lux control algorithm status
+ * Lux control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/meson.build b/src/ipa/rpi/controller/meson.build
index 20b9cda..74b7488 100644
--- a/src/ipa/rpi/controller/meson.build
+++ b/src/ipa/rpi/controller/meson.build
@@ -5,21 +5,25 @@
     'controller.cpp',
     'device_status.cpp',
     'histogram.cpp',
-    'pwl.cpp',
     'rpi/af.cpp',
     'rpi/agc.cpp',
     'rpi/agc_channel.cpp',
     'rpi/alsc.cpp',
     'rpi/awb.cpp',
     'rpi/black_level.cpp',
+    'rpi/cac.cpp',
     'rpi/ccm.cpp',
     'rpi/contrast.cpp',
+    'rpi/denoise.cpp',
     'rpi/dpc.cpp',
     'rpi/geq.cpp',
+    'rpi/hdr.cpp',
     'rpi/lux.cpp',
     'rpi/noise.cpp',
+    'rpi/saturation.cpp',
     'rpi/sdn.cpp',
     'rpi/sharpen.cpp',
+    'rpi/tonemap.cpp',
 ])
 
 rpi_ipa_controller_deps = [
@@ -27,4 +31,5 @@
 ]
 
 rpi_ipa_controller_lib = static_library('rpi_ipa_controller', rpi_ipa_controller_sources,
+                                        include_directories : libipa_includes,
                                         dependencies : rpi_ipa_controller_deps)
diff --git a/src/ipa/rpi/controller/metadata.h b/src/ipa/rpi/controller/metadata.h
index bf8a239..b4650d2 100644
--- a/src/ipa/rpi/controller/metadata.h
+++ b/src/ipa/rpi/controller/metadata.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2021, Raspberry Pi Ltd
  *
- * metadata.h - general metadata class
+ * general metadata class
  */
 #pragma once
 
@@ -116,6 +116,7 @@
 	 * e.g. std::lock_guard<RPiController::Metadata> lock(metadata)
 	 */
 	void lock() LIBCAMERA_TSA_ACQUIRE() { mutex_.lock(); }
+	auto try_lock() LIBCAMERA_TSA_ACQUIRE() { return mutex_.try_lock(); }
 	void unlock() LIBCAMERA_TSA_RELEASE() { mutex_.unlock(); }
 
 private:
diff --git a/src/ipa/rpi/controller/noise_status.h b/src/ipa/rpi/controller/noise_status.h
index da194f7..1919da3 100644
--- a/src/ipa/rpi/controller/noise_status.h
+++ b/src/ipa/rpi/controller/noise_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * noise_status.h - Noise control algorithm status
+ * Noise control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/pdaf_data.h b/src/ipa/rpi/controller/pdaf_data.h
index 470510f..779b987 100644
--- a/src/ipa/rpi/controller/pdaf_data.h
+++ b/src/ipa/rpi/controller/pdaf_data.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Raspberry Pi Ltd
  *
- * pdaf_data.h - PDAF Metadata
+ * PDAF Metadata
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/pwl.cpp b/src/ipa/rpi/controller/pwl.cpp
deleted file mode 100644
index 70c2e24..0000000
--- a/src/ipa/rpi/controller/pwl.cpp
+++ /dev/null
@@ -1,269 +0,0 @@
-/* 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
deleted file mode 100644
index aacf603..0000000
--- a/src/ipa/rpi/controller/pwl.h
+++ /dev/null
@@ -1,127 +0,0 @@
-/* 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
index a8860dc..c60f7d9 100644
--- a/src/ipa/rpi/controller/region_stats.h
+++ b/src/ipa/rpi/controller/region_stats.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Raspberry Pi Ltd
  *
- * region_stats.h - Raspberry Pi region based statistics container
+ * Raspberry Pi region based statistics container
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/rpi/af.cpp b/src/ipa/rpi/controller/rpi/af.cpp
index ed0c8a9..5ca76dd 100644
--- a/src/ipa/rpi/controller/rpi/af.cpp
+++ b/src/ipa/rpi/controller/rpi/af.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022-2023, Raspberry Pi Ltd
  *
- * af.cpp - Autofocus control algorithm
+ * Autofocus control algorithm
  */
 
 #include "af.h"
@@ -139,7 +139,7 @@
 	readNumber<uint32_t>(skipFrames, params, "skip_frames");
 
 	if (params.contains("map"))
-		map.read(params["map"]);
+		map = params["map"].get<ipa::Pwl>(ipa::Pwl{});
 	else
 		LOG(RPiAf, Warning) << "No map defined";
 
@@ -721,7 +721,7 @@
 
 	if (mode_ == AfModeManual) {
 		LOG(RPiAf, Debug) << "setLensPosition: " << dioptres;
-		ftarget_ = cfg_.map.domain().clip(dioptres);
+		ftarget_ = cfg_.map.domain().clamp(dioptres);
 		changed = !(initted_ && fsmooth_ == ftarget_);
 		updateLensPosition();
 	}
diff --git a/src/ipa/rpi/controller/rpi/af.h b/src/ipa/rpi/controller/rpi/af.h
index 6d2bae6..317a51f 100644
--- a/src/ipa/rpi/controller/rpi/af.h
+++ b/src/ipa/rpi/controller/rpi/af.h
@@ -2,14 +2,15 @@
 /*
  * Copyright (C) 2022-2023, Raspberry Pi Ltd
  *
- * af.h - Autofocus control algorithm
+ * Autofocus control algorithm
  */
 #pragma once
 
 #include "../af_algorithm.h"
 #include "../af_status.h"
 #include "../pdaf_data.h"
-#include "../pwl.h"
+
+#include "libipa/pwl.h"
 
 /*
  * This algorithm implements a hybrid of CDAF and PDAF, favouring PDAF.
@@ -100,7 +101,7 @@
 		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 */
+		libcamera::ipa::Pwl map;       	/* converts dioptres -> lens driver position */
 
 		CfgParams();
 		int read(const libcamera::YamlObject &params);
diff --git a/src/ipa/rpi/controller/rpi/agc.cpp b/src/ipa/rpi/controller/rpi/agc.cpp
index 870cb43..fcf7aec 100644
--- a/src/ipa/rpi/controller/rpi/agc.cpp
+++ b/src/ipa/rpi/controller/rpi/agc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * agc.cpp - AGC/AEC control algorithm
+ * AGC/AEC control algorithm
  */
 
 #include "agc.h"
@@ -74,22 +74,22 @@
 	return 0;
 }
 
-void Agc::disableAuto(unsigned int channelIndex)
+void Agc::disableAuto()
 {
-	if (checkChannel(channelIndex))
-		return;
+	LOG(RPiAgc, Debug) << "disableAuto";
 
-	LOG(RPiAgc, Debug) << "disableAuto for channel " << channelIndex;
-	channelData_[channelIndex].channel.disableAuto();
+	/* All channels are enabled/disabled together. */
+	for (auto &data : channelData_)
+		data.channel.disableAuto();
 }
 
-void Agc::enableAuto(unsigned int channelIndex)
+void Agc::enableAuto()
 {
-	if (checkChannel(channelIndex))
-		return;
+	LOG(RPiAgc, Debug) << "enableAuto";
 
-	LOG(RPiAgc, Debug) << "enableAuto for channel " << channelIndex;
-	channelData_[channelIndex].channel.enableAuto();
+	/* All channels are enabled/disabled together. */
+	for (auto &data : channelData_)
+		data.channel.enableAuto();
 }
 
 unsigned int Agc::getConvergenceFrames() const
@@ -118,14 +118,13 @@
 	channelData_[channelIndex].channel.setEv(ev);
 }
 
-void Agc::setFlickerPeriod(unsigned int channelIndex, Duration flickerPeriod)
+void Agc::setFlickerPeriod(Duration flickerPeriod)
 {
-	if (checkChannel(channelIndex))
-		return;
+	LOG(RPiAgc, Debug) << "setFlickerPeriod " << flickerPeriod;
 
-	LOG(RPiAgc, Debug) << "setFlickerPeriod " << flickerPeriod
-			   << " for channel " << channelIndex;
-	channelData_[channelIndex].channel.setFlickerPeriod(flickerPeriod);
+	/* Flicker period will be the same across all channels. */
+	for (auto &data : channelData_)
+		data.channel.setFlickerPeriod(flickerPeriod);
 }
 
 void Agc::setMaxShutter(Duration maxShutter)
@@ -162,22 +161,22 @@
 		data.channel.setMeteringMode(meteringModeName);
 }
 
-void Agc::setExposureMode(unsigned int channelIndex, std::string const &exposureModeName)
+void Agc::setExposureMode(std::string const &exposureModeName)
 {
-	if (checkChannel(channelIndex))
-		return;
+	LOG(RPiAgc, Debug) << "setExposureMode " << exposureModeName;
 
-	LOG(RPiAgc, Debug) << "setExposureMode " << exposureModeName
-			   << " for channel " << channelIndex;
-	channelData_[channelIndex].channel.setExposureMode(exposureModeName);
+	/* Exposure mode will be the same across all channels. */
+	for (auto &data : channelData_)
+		data.channel.setExposureMode(exposureModeName);
 }
 
-void Agc::setConstraintMode(unsigned int channelIndex, std::string const &constraintModeName)
+void Agc::setConstraintMode(std::string const &constraintModeName)
 {
-	if (checkChannel(channelIndex))
-		return;
+	LOG(RPiAgc, Debug) << "setConstraintMode " << constraintModeName;
 
-	channelData_[channelIndex].channel.setConstraintMode(constraintModeName);
+	/* Constraint mode will be the same across all channels. */
+	for (auto &data : channelData_)
+		data.channel.setConstraintMode(constraintModeName);
 }
 
 template<typename T>
@@ -203,6 +202,7 @@
 
 	LOG(RPiAgc, Debug) << "setActiveChannels " << activeChannels;
 	activeChannels_ = activeChannels;
+	index_ = 0;
 }
 
 void Agc::switchMode(CameraMode const &cameraMode,
@@ -311,15 +311,16 @@
 	 * exist, and call process(). We must make the agc.status metadata record correctly
 	 * which channel this is.
 	 */
+	StatisticsPtr *statsPtr = &stats;
 	if (channelData.statistics && channelData.deviceStatus) {
 		deviceStatus = *channelData.deviceStatus;
-		stats = channelData.statistics;
+		statsPtr = &channelData.statistics;
 	} else {
 		/* Can also happen when new channels start. */
 		LOG(RPiAgc, Debug) << "process: channel " << channelIndex << " not seen yet";
 	}
 
-	channelData.channel.process(stats, deviceStatus, imageMetadata, channelTotalExposures_);
+	channelData.channel.process(*statsPtr, deviceStatus, imageMetadata, channelTotalExposures_);
 	auto dur = setCurrentChannelIndexGetExposure(imageMetadata, "process: no AGC status found",
 						     channelIndex);
 	if (dur)
diff --git a/src/ipa/rpi/controller/rpi/agc.h b/src/ipa/rpi/controller/rpi/agc.h
index 9089043..5d056f0 100644
--- a/src/ipa/rpi/controller/rpi/agc.h
+++ b/src/ipa/rpi/controller/rpi/agc.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * agc.h - AGC/AEC control algorithm
+ * AGC/AEC control algorithm
  */
 #pragma once
 
@@ -31,20 +31,17 @@
 	unsigned int getConvergenceFrames() const override;
 	std::vector<double> const &getWeights() const override;
 	void setEv(unsigned int channel, double ev) override;
-	void setFlickerPeriod(unsigned int channelIndex,
-			      libcamera::utils::Duration flickerPeriod) override;
+	void setFlickerPeriod(libcamera::utils::Duration flickerPeriod) override;
 	void setMaxShutter(libcamera::utils::Duration maxShutter) override;
 	void setFixedShutter(unsigned int channelIndex,
 			     libcamera::utils::Duration fixedShutter) override;
 	void setFixedAnalogueGain(unsigned int channelIndex,
 				  double fixedAnalogueGain) override;
 	void setMeteringMode(std::string const &meteringModeName) override;
-	void setExposureMode(unsigned int channelIndex,
-			     std::string const &exposureModeName) override;
-	void setConstraintMode(unsigned int channelIndex,
-			       std::string const &contraintModeName) override;
-	void enableAuto(unsigned int channelIndex) override;
-	void disableAuto(unsigned int channelIndex) 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;
diff --git a/src/ipa/rpi/controller/rpi/agc_channel.cpp b/src/ipa/rpi/controller/rpi/agc_channel.cpp
index 3957dbc..cf2565a 100644
--- a/src/ipa/rpi/controller/rpi/agc_channel.cpp
+++ b/src/ipa/rpi/controller/rpi/agc_channel.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2023, Raspberry Pi Ltd
  *
- * agc_channel.cpp - AGC/AEC control algorithm
+ * AGC/AEC control algorithm
  */
 
 #include "agc_channel.h"
@@ -130,7 +130,8 @@
 		return -EINVAL;
 	qHi = *value;
 
-	return yTarget.read(params["y_target"]);
+	yTarget = params["y_target"].get<ipa::Pwl>(ipa::Pwl{});
+	return yTarget.empty() ? -EINVAL : 0;
 }
 
 static std::tuple<int, AgcConstraintMode>
@@ -237,9 +238,9 @@
 			return ret;
 	}
 
-	ret = yTarget.read(params["y_target"]);
-	if (ret)
-		return ret;
+	yTarget = params["y_target"].get<ipa::Pwl>(ipa::Pwl{});
+	if (yTarget.empty())
+		return -EINVAL;
 
 	speed = params["speed"].get<double>(0.2);
 	startupFrames = params["startup_frames"].get<uint16_t>(10);
@@ -251,6 +252,10 @@
 	defaultExposureTime = params["default_exposure_time"].get<double>(1000) * 1us;
 	defaultAnalogueGain = params["default_analogue_gain"].get<double>(1.0);
 
+	stableRegion = params["stable_region"].get<double>(0.02);
+
+	desaturate = params["desaturate"].get<int>(1);
+
 	return 0;
 }
 
@@ -266,7 +271,11 @@
 	  lastTargetExposure_(0s), ev_(1.0), flickerPeriod_(0s),
 	  maxShutter_(0s), fixedShutter_(0s), fixedAnalogueGain_(0.0)
 {
-	memset(&awb_, 0, sizeof(awb_));
+	/* Set AWB default values in case early frames have no updates in metadata. */
+	awb_.gainR = 1.0;
+	awb_.gainG = 1.0;
+	awb_.gainB = 1.0;
+
 	/*
 	 * Setting status_.totalExposureValue_ to zero initially tells us
 	 * it's not been calculated yet (i.e. Process hasn't yet run).
@@ -405,7 +414,6 @@
 	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);
@@ -460,6 +468,9 @@
 	AgcStatus delayedStatus;
 	AgcPrepareStatus prepareStatus;
 
+	/* Fetch the AWB status now because AWB also sets it in the prepare method. */
+	fetchAwbStatus(imageMetadata);
+
 	if (!imageMetadata->get("agc.delayed_status", delayedStatus))
 		totalExposureValue = delayedStatus.totalExposureValue;
 
@@ -503,8 +514,6 @@
 	 * configuration, that kind of thing.
 	 */
 	housekeepConfig();
-	/* Fetch the AWB status immediately, so that we can assume it's there. */
-	fetchAwbStatus(imageMetadata);
 	/* Get the current exposure values for the frame that's just arrived. */
 	fetchCurrentExposure(deviceStatus);
 	/* Compute the total gain we require relative to the current exposure. */
@@ -633,9 +642,6 @@
 
 void AgcChannel::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";
 }
@@ -710,7 +716,7 @@
 static double constraintComputeGain(AgcConstraint &c, const Histogram &h, double lux,
 				    double evGain, double &targetY)
 {
-	targetY = c.yTarget.eval(c.yTarget.domain().clip(lux));
+	targetY = c.yTarget.eval(c.yTarget.domain().clamp(lux));
 	targetY = std::min(EvGainYTargetLimit, targetY * evGain);
 	double iqm = h.interQuantileMean(c.qLo, c.qHi);
 	return (targetY * h.bins()) / iqm;
@@ -729,7 +735,7 @@
 	 * 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 = config_.yTarget.eval(config_.yTarget.domain().clamp(lux.lux));
 	targetY = std::min(EvGainYTargetLimit, targetY * evGain);
 
 	/*
@@ -858,8 +864,10 @@
 	 * quickly (and we then approach the correct value more quickly from
 	 * below).
 	 */
-	bool desaturate = !channelBound &&
-			  targetY > config_.fastReduceThreshold && gain < sqrt(targetY);
+	bool desaturate = false;
+	if (config_.desaturate)
+		desaturate = !channelBound &&
+			     targetY > config_.fastReduceThreshold && gain < sqrt(targetY);
 	if (desaturate)
 		dg /= config_.fastReduceThreshold;
 	LOG(RPiAgc, Debug) << "Digital gain " << dg << " desaturate? " << desaturate;
@@ -871,6 +879,8 @@
 void AgcChannel::filterExposure()
 {
 	double speed = config_.speed;
+	double stableRegion = config_.stableRegion;
+
 	/*
 	 * AGC adapts instantly if both shutter and gain are directly specified
 	 * or we're in the startup phase.
@@ -880,6 +890,9 @@
 		speed = 1.0;
 	if (!filtered_.totalExposure) {
 		filtered_.totalExposure = target_.totalExposure;
+	} else if (filtered_.totalExposure * (1.0 - stableRegion) < target_.totalExposure &&
+		   filtered_.totalExposure * (1.0 + stableRegion) > target_.totalExposure) {
+		/* Total exposure must change by more than this or we leave it alone. */
 	} else {
 		/*
 		 * If close to the result go faster, to save making so many
@@ -965,7 +978,7 @@
 void AgcChannel::writeAndFinish(Metadata *imageMetadata, bool desaturate)
 {
 	status_.totalExposureValue = filtered_.totalExposure;
-	status_.targetExposureValue = desaturate ? 0s : target_.totalExposureNoDG;
+	status_.targetExposureValue = desaturate ? 0s : target_.totalExposure;
 	status_.shutterTime = filtered_.shutter;
 	status_.analogueGain = filtered_.analogueGain;
 	/*
diff --git a/src/ipa/rpi/controller/rpi/agc_channel.h b/src/ipa/rpi/controller/rpi/agc_channel.h
index ae826fa..5836888 100644
--- a/src/ipa/rpi/controller/rpi/agc_channel.h
+++ b/src/ipa/rpi/controller/rpi/agc_channel.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2023, Raspberry Pi Ltd
  *
- * agc_channel.h - AGC/AEC control algorithm
+ * AGC/AEC control algorithm
  */
 #pragma once
 
@@ -12,10 +12,11 @@
 
 #include <libcamera/base/utils.h>
 
+#include <libipa/pwl.h>
+
 #include "../agc_status.h"
 #include "../awb_status.h"
 #include "../controller.h"
-#include "../pwl.h"
 
 /* This is our implementation of AGC. */
 
@@ -40,7 +41,7 @@
 	Bound bound;
 	double qLo;
 	double qHi;
-	Pwl yTarget;
+	libcamera::ipa::Pwl yTarget;
 	int read(const libcamera::YamlObject &params);
 };
 
@@ -61,7 +62,7 @@
 	std::map<std::string, AgcExposureMode> exposureModes;
 	std::map<std::string, AgcConstraintMode> constraintModes;
 	std::vector<AgcChannelConstraint> channelConstraints;
-	Pwl yTarget;
+	libcamera::ipa::Pwl yTarget;
 	double speed;
 	uint16_t startupFrames;
 	unsigned int convergenceFrames;
@@ -75,6 +76,8 @@
 	double baseEv;
 	libcamera::utils::Duration defaultExposureTime;
 	double defaultAnalogueGain;
+	double stableRegion;
+	bool desaturate;
 };
 
 class AgcChannel
diff --git a/src/ipa/rpi/controller/rpi/alsc.cpp b/src/ipa/rpi/controller/rpi/alsc.cpp
index 3a2e8fe..67029fc 100644
--- a/src/ipa/rpi/controller/rpi/alsc.cpp
+++ b/src/ipa/rpi/controller/rpi/alsc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * alsc.cpp - ALSC (auto lens shading correction) control algorithm
+ * ALSC (auto lens shading correction) control algorithm
  */
 
 #include <algorithm>
@@ -338,19 +338,21 @@
 }
 
 static void copyStats(RgbyRegions &regions, StatisticsPtr &stats,
-		      AlscStatus const &status)
+		      std::array<Array2D<double>, 3> &prevSyncResults)
 {
 	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;
+	const std::vector<double> &rTable = prevSyncResults[0].data(); //status.r;
+	const std::vector<double> &gTable = prevSyncResults[1].data(); //status.g;
+	const std::vector<double> &bTable = prevSyncResults[2].data(); //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]);
+		if (stats->colourStatsPos == Statistics::ColourStatsPos::PostLsc) {
+			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);
 	}
 }
@@ -365,17 +367,10 @@
 	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.
+	 * the LSC table that the pipeline applied to them which we get from
+	 * prevSyncResults_.
 	 */
-	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);
+	copyStats(statistics_, stats, prevSyncResults_);
 	framePhase_ = 0;
 	asyncStarted_ = true;
 	{
@@ -414,6 +409,11 @@
 	status.g = prevSyncResults_[1].data();
 	status.b = prevSyncResults_[2].data();
 	imageMetadata->set("alsc.status", status);
+	/*
+	 * Put the results in the global metadata as well. This will be used by
+	 * AWB to factor in the colour shading correction.
+	 */
+	getGlobalMetadata().set("alsc.status", status);
 }
 
 void Alsc::process(StatisticsPtr &stats, Metadata *imageMetadata)
@@ -548,7 +548,9 @@
 	for (unsigned int i = 0; i < cr.size(); i++) {
 		auto s = awbRegion.get(i);
 
-		if (s.counted <= minCount || s.val.gSum / s.counted <= minG) {
+		/* Do not return unreliable, or zero, colour ratio statistics. */
+		if (s.counted <= minCount || s.val.gSum / s.counted <= minG ||
+		    s.val.rSum / s.counted <= minG || s.val.bSum / s.counted <= minG) {
 			cr[i] = cb[i] = InsufficientData;
 			continue;
 		}
diff --git a/src/ipa/rpi/controller/rpi/alsc.h b/src/ipa/rpi/controller/rpi/alsc.h
index 0b6d947..3108798 100644
--- a/src/ipa/rpi/controller/rpi/alsc.h
+++ b/src/ipa/rpi/controller/rpi/alsc.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * alsc.h - ALSC (auto lens shading correction) control algorithm
+ * ALSC (auto lens shading correction) control algorithm
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/rpi/awb.cpp b/src/ipa/rpi/controller/rpi/awb.cpp
index ef3435d..f45525b 100644
--- a/src/ipa/rpi/controller/rpi/awb.cpp
+++ b/src/ipa/rpi/controller/rpi/awb.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * awb.cpp - AWB control algorithm
+ * AWB control algorithm
  */
 
 #include <assert.h>
@@ -12,6 +12,7 @@
 
 #include "../lux_status.h"
 
+#include "alsc_status.h"
 #include "awb.h"
 
 using namespace RPiController;
@@ -48,10 +49,11 @@
 		return -EINVAL;
 	lux = *value;
 
-	return prior.read(params["prior"]);
+	prior = params["prior"].get<ipa::Pwl>(ipa::Pwl{});
+	return prior.empty() ? -EINVAL : 0;
 }
 
-static int readCtCurve(Pwl &ctR, Pwl &ctB, const libcamera::YamlObject &params)
+static int readCtCurve(ipa::Pwl &ctR, ipa::Pwl &ctB, const libcamera::YamlObject &params)
 {
 	if (params.size() % 3) {
 		LOG(RPiAwb, Error) << "AwbConfig: incomplete CT curve entry";
@@ -102,8 +104,8 @@
 		if (ret)
 			return ret;
 		/* We will want the inverse functions of these too. */
-		ctRInverse = ctR.inverse();
-		ctBInverse = ctB.inverse();
+		ctRInverse = ctR.inverse().first;
+		ctBInverse = ctB.inverse().first;
 	}
 
 	if (params.contains("priors")) {
@@ -120,7 +122,7 @@
 		}
 		if (priors.empty()) {
 			LOG(RPiAwb, Error) << "AwbConfig: no AWB priors configured";
-			return ret;
+			return -EINVAL;
 		}
 	}
 	if (params.contains("modes")) {
@@ -206,7 +208,7 @@
 	 * them.
 	 */
 	if (!config_.ctR.empty() && !config_.ctB.empty()) {
-		syncResults_.temperatureK = config_.ctR.domain().clip(4000);
+		syncResults_.temperatureK = config_.ctR.domain().clamp(4000);
 		syncResults_.gainR = 1.0 / config_.ctR.eval(syncResults_.temperatureK);
 		syncResults_.gainG = 1.0;
 		syncResults_.gainB = 1.0 / config_.ctB.eval(syncResults_.temperatureK);
@@ -219,6 +221,12 @@
 	asyncResults_ = syncResults_;
 }
 
+void Awb::initialValues(double &gainR, double &gainB)
+{
+	gainR = syncResults_.gainR;
+	gainB = syncResults_.gainB;
+}
+
 void Awb::disableAuto()
 {
 	/* Freeze the most recent values, and treat them as manual gains */
@@ -266,8 +274,8 @@
 		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_));
+			double ctR = config_.ctRInverse.eval(config_.ctRInverse.domain().clamp(1 / manualR_));
+			double ctB = config_.ctBInverse.eval(config_.ctBInverse.domain().clamp(1 / manualB_));
 			prevSyncResults_.temperatureK = (ctR + ctB) / 2;
 			syncResults_.temperatureK = prevSyncResults_.temperatureK;
 		}
@@ -398,18 +406,28 @@
 }
 
 static void generateStats(std::vector<Awb::RGB> &zones,
-			  RgbyRegions &stats, double minPixels,
-			  double minG)
+			  StatisticsPtr &stats, double minPixels,
+			  double minG, Metadata &globalMetadata)
 {
-	for (auto const &region : stats) {
+	std::scoped_lock<RPiController::Metadata> l(globalMetadata);
+
+	for (unsigned int i = 0; i < stats->awbRegions.numRegions(); i++) {
 		Awb::RGB zone;
+		auto &region = stats->awbRegions.get(i);
 		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);
+			if (zone.G < minG)
+				continue;
+			zone.R = region.val.rSum / region.counted;
+			zone.B = region.val.bSum / region.counted;
+			/* Factor in the ALSC applied colour shading correction if required. */
+			const AlscStatus *alscStatus = globalMetadata.getLocked<AlscStatus>("alsc.status");
+			if (stats->colourStatsPos == Statistics::ColourStatsPos::PreLsc && alscStatus) {
+				zone.R *= alscStatus->r[i];
+				zone.G *= alscStatus->g[i];
+				zone.B *= alscStatus->b[i];
 			}
+			zones.push_back(zone);
 		}
 	}
 }
@@ -421,8 +439,8 @@
 	 * 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);
+	generateStats(zones_, statistics_, config_.minPixels,
+		      config_.minG, getGlobalMetadata());
 	/*
 	 * apply sensitivities, so values appear to come from our "canonical"
 	 * sensor.
@@ -451,7 +469,7 @@
 	return delta2Sum;
 }
 
-Pwl Awb::interpolatePrior()
+ipa::Pwl Awb::interpolatePrior()
 {
 	/*
 	 * Interpolate the prior log likelihood function for our current lux
@@ -468,7 +486,7 @@
 			idx++;
 		double lux0 = config_.priors[idx].lux,
 		       lux1 = config_.priors[idx + 1].lux;
-		return Pwl::combine(config_.priors[idx].prior,
+		return ipa::Pwl::combine(config_.priors[idx].prior,
 				    config_.priors[idx + 1].prior,
 				    [&](double /*x*/, double y0, double y1) {
 					    return y0 + (y1 - y0) *
@@ -477,26 +495,26 @@
 	}
 }
 
-static double interpolateQuadatric(Pwl::Point const &a, Pwl::Point const &b,
-				   Pwl::Point const &c)
+static double interpolateQuadatric(ipa::Pwl::Point const &a, ipa::Pwl::Point const &b,
+				   ipa::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);
+	ipa::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));
+		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);
+	return a.y() < c.y() - eps ? a.x() : (c.y() < a.y() - eps ? c.x() : b.x());
 }
 
-double Awb::coarseSearch(Pwl const &prior)
+double Awb::coarseSearch(ipa::Pwl const &prior)
 {
 	points_.clear(); /* assume doesn't deallocate memory */
 	size_t bestPoint = 0;
@@ -508,22 +526,22 @@
 		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 priorLogLikelihood = prior.eval(prior.domain().clamp(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)
+		points_.push_back(ipa::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;
+	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
@@ -542,7 +560,7 @@
 	return t;
 }
 
-void Awb::fineSearch(double &t, double &r, double &b, Pwl const &prior)
+void Awb::fineSearch(double &t, double &r, double &b, ipa::Pwl const &prior)
 {
 	int spanR = -1, spanB = -1;
 	config_.ctR.eval(t, &spanR);
@@ -553,14 +571,14 @@
 		       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)
+	ipa::Pwl::Point transverse({ bDiff, -rDiff });
+	if (transverse.length2() < 1e-6)
 		return;
 	/*
 	 * unit vector orthogonal to the b vs. r function (pointing outwards
 	 * with r and b increasing)
 	 */
-	transverse = transverse / transverse.len();
+	transverse = transverse / transverse.length();
 	double bestLogLikelihood = 0, bestT = 0, bestR = 0, bestB = 0;
 	double transverseRange = config_.transverseNeg + config_.transversePos;
 	const int maxNumDeltas = 12;
@@ -575,26 +593,26 @@
 	for (int i = -nsteps; i <= nsteps; i++) {
 		double tTest = t + i * step;
 		double priorLogLikelihood =
-			prior.eval(prior.domain().clip(tTest));
+			prior.eval(prior.domain().clamp(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];
+		ipa::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;
+			points[j][0] = -config_.transverseNeg +
+				       (transverseRange * j) / (numDeltas - 1);
+			ipa::Pwl::Point rbTest = ipa::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;
+			points[j][1] = delta2Sum - priorLogLikelihood;
 			LOG(RPiAwb, Debug)
 				<< "At t " << tTest << " r " << rTest << " b "
-				<< bTest << ": " << points[j].y;
-			if (points[j].y < points[bestPoint].y)
+				<< bTest << ": " << points[j].y();
+			if (points[j].y() < points[bestPoint].y())
 				bestPoint = j;
 		}
 		/*
@@ -602,11 +620,11 @@
 		 * 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;
+		ipa::Pwl::Point rbTest = ipa::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;
@@ -636,7 +654,7 @@
 	 * Get the current prior, and scale according to how many zones are
 	 * valid... not entirely sure about this.
 	 */
-	Pwl prior = interpolatePrior();
+	ipa::Pwl prior = interpolatePrior();
 	prior *= zones_.size() / (double)(statistics_->awbRegions.numRegions());
 	prior.map([](double x, double y) {
 		LOG(RPiAwb, Debug) << "(" << x << "," << y << ")";
diff --git a/src/ipa/rpi/controller/rpi/awb.h b/src/ipa/rpi/controller/rpi/awb.h
index e7d49cd..ab30f4f 100644
--- a/src/ipa/rpi/controller/rpi/awb.h
+++ b/src/ipa/rpi/controller/rpi/awb.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * awb.h - AWB control algorithm
+ * AWB control algorithm
  */
 #pragma once
 
@@ -10,11 +10,14 @@
 #include <condition_variable>
 #include <thread>
 
+#include <libcamera/geometry.h>
+
 #include "../awb_algorithm.h"
-#include "../pwl.h"
 #include "../awb_status.h"
 #include "../statistics.h"
 
+#include "libipa/pwl.h"
+
 namespace RPiController {
 
 /* Control algorithm to perform AWB calculations. */
@@ -28,7 +31,7 @@
 struct AwbPrior {
 	int read(const libcamera::YamlObject &params);
 	double lux; /* lux level */
-	Pwl prior; /* maps CT to prior log likelihood for this lux level */
+	libcamera::ipa::Pwl prior; /* maps CT to prior log likelihood for this lux level */
 };
 
 struct AwbConfig {
@@ -41,10 +44,10 @@
 	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 */
+	libcamera::ipa::Pwl ctR; /* function maps CT to r (= R/G) */
+	libcamera::ipa::Pwl ctB; /* function maps CT to b (= B/G) */
+	libcamera::ipa::Pwl ctRInverse; /* inverse of ctR */
+	libcamera::ipa::Pwl ctBInverse; /* inverse of ctB */
 	/* table of illuminant priors at different lux levels */
 	std::vector<AwbPrior> priors;
 	/* AWB "modes" (determines the search range) */
@@ -95,6 +98,7 @@
 	void initialise() override;
 	int read(const libcamera::YamlObject &params) override;
 	unsigned int getConvergenceFrames() const override;
+	void initialValues(double &gainR, double &gainB) override;
 	void setMode(std::string const &name) override;
 	void setManualGains(double manualR, double manualB) override;
 	void enableAuto() override;
@@ -160,11 +164,11 @@
 	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);
+	libcamera::ipa::Pwl interpolatePrior();
+	double coarseSearch(libcamera::ipa::Pwl const &prior);
+	void fineSearch(double &t, double &r, double &b, libcamera::ipa::Pwl const &prior);
 	std::vector<RGB> zones_;
-	std::vector<Pwl::Point> points_;
+	std::vector<libcamera::ipa::Pwl::Point> points_;
 	/* manual r setting */
 	double manualR_;
 	/* manual b setting */
diff --git a/src/ipa/rpi/controller/rpi/black_level.cpp b/src/ipa/rpi/controller/rpi/black_level.cpp
index 85baec3..ea991df 100644
--- a/src/ipa/rpi/controller/rpi/black_level.cpp
+++ b/src/ipa/rpi/controller/rpi/black_level.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * black_level.cpp - black level control algorithm
+ * black level control algorithm
  */
 
 #include <math.h>
@@ -22,7 +22,7 @@
 #define NAME "rpi.black_level"
 
 BlackLevel::BlackLevel(Controller *controller)
-	: Algorithm(controller)
+	: BlackLevelAlgorithm(controller)
 {
 }
 
@@ -45,6 +45,14 @@
 	return 0;
 }
 
+void BlackLevel::initialValues(uint16_t &blackLevelR, uint16_t &blackLevelG,
+			       uint16_t &blackLevelB)
+{
+	blackLevelR = blackLevelR_;
+	blackLevelG = blackLevelG_;
+	blackLevelB = blackLevelB_;
+}
+
 void BlackLevel::prepare(Metadata *imageMetadata)
 {
 	/*
diff --git a/src/ipa/rpi/controller/rpi/black_level.h b/src/ipa/rpi/controller/rpi/black_level.h
index 2403f7f..f50729d 100644
--- a/src/ipa/rpi/controller/rpi/black_level.h
+++ b/src/ipa/rpi/controller/rpi/black_level.h
@@ -2,23 +2,25 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * black_level.h - black level control algorithm
+ * black level control algorithm
  */
 #pragma once
 
-#include "../algorithm.h"
+#include "../black_level_algorithm.h"
 #include "../black_level_status.h"
 
 /* This is our implementation of the "black level algorithm". */
 
 namespace RPiController {
 
-class BlackLevel : public Algorithm
+class BlackLevel : public BlackLevelAlgorithm
 {
 public:
 	BlackLevel(Controller *controller);
 	char const *name() const override;
 	int read(const libcamera::YamlObject &params) override;
+	void initialValues(uint16_t &blackLevelR, uint16_t &blackLevelG,
+			   uint16_t &blackLevelB) override;
 	void prepare(Metadata *imageMetadata) override;
 
 private:
diff --git a/src/ipa/rpi/controller/rpi/cac.cpp b/src/ipa/rpi/controller/rpi/cac.cpp
new file mode 100644
index 0000000..17779ad
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/cac.cpp
@@ -0,0 +1,107 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2023 Raspberry Pi Ltd
+ *
+ * Chromatic Aberration Correction algorithm
+ */
+#include "cac.h"
+
+#include <libcamera/base/log.h>
+
+#include "cac_status.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiCac)
+
+#define NAME "rpi.cac"
+
+Cac::Cac(Controller *controller)
+	: Algorithm(controller)
+{
+}
+
+char const *Cac::name() const
+{
+	return NAME;
+}
+
+static bool arrayToSet(const libcamera::YamlObject &params, std::vector<double> &inputArray, const Size &size)
+{
+	int num = 0;
+	int max_num = (size.width + 1) * (size.height + 1);
+	inputArray.resize(max_num);
+
+	for (const auto &p : params.asList()) {
+		if (num == max_num)
+			return false;
+		inputArray[num++] = p.get<double>(0);
+	}
+
+	return num == max_num;
+}
+
+static void setStrength(std::vector<double> &inputArray, std::vector<double> &outputArray,
+			double strengthFactor)
+{
+	int num = 0;
+	for (const auto &p : inputArray) {
+		outputArray[num++] = p * strengthFactor;
+	}
+}
+
+int Cac::read(const libcamera::YamlObject &params)
+{
+	config_.enabled = params.contains("lut_rx") && params.contains("lut_ry") &&
+			  params.contains("lut_bx") && params.contains("lut_by");
+	if (!config_.enabled)
+		return 0;
+
+	const Size &size = getHardwareConfig().cacRegions;
+
+	if (!arrayToSet(params["lut_rx"], config_.lutRx, size)) {
+		LOG(RPiCac, Error) << "Bad CAC lut_rx table";
+		return -EINVAL;
+	}
+
+	if (!arrayToSet(params["lut_ry"], config_.lutRy, size)) {
+		LOG(RPiCac, Error) << "Bad CAC lut_ry table";
+		return -EINVAL;
+	}
+
+	if (!arrayToSet(params["lut_bx"], config_.lutBx, size)) {
+		LOG(RPiCac, Error) << "Bad CAC lut_bx table";
+		return -EINVAL;
+	}
+
+	if (!arrayToSet(params["lut_by"], config_.lutBy, size)) {
+		LOG(RPiCac, Error) << "Bad CAC lut_by table";
+		return -EINVAL;
+	}
+
+	double strength = params["strength"].get<double>(1);
+	cacStatus_.lutRx = config_.lutRx;
+	cacStatus_.lutRy = config_.lutRy;
+	cacStatus_.lutBx = config_.lutBx;
+	cacStatus_.lutBy = config_.lutBy;
+	setStrength(config_.lutRx, cacStatus_.lutRx, strength);
+	setStrength(config_.lutBx, cacStatus_.lutBx, strength);
+	setStrength(config_.lutRy, cacStatus_.lutRy, strength);
+	setStrength(config_.lutBy, cacStatus_.lutBy, strength);
+
+	return 0;
+}
+
+void Cac::prepare(Metadata *imageMetadata)
+{
+	if (config_.enabled)
+		imageMetadata->set("cac.status", cacStatus_);
+}
+
+// Register algorithm with the system.
+static Algorithm *Create(Controller *controller)
+{
+	return (Algorithm *)new Cac(controller);
+}
+static RegisterAlgorithm reg(NAME, &Create);
diff --git a/src/ipa/rpi/controller/rpi/cac.h b/src/ipa/rpi/controller/rpi/cac.h
new file mode 100644
index 0000000..a7b14c0
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/cac.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2023, Raspberry Pi Ltd
+ *
+ * cac.hpp - CAC control algorithm
+ */
+#pragma once
+
+#include "algorithm.h"
+#include "cac_status.h"
+
+namespace RPiController {
+
+struct CacConfig {
+	bool enabled;
+	std::vector<double> lutRx;
+	std::vector<double> lutRy;
+	std::vector<double> lutBx;
+	std::vector<double> lutBy;
+};
+
+class Cac : public Algorithm
+{
+public:
+	Cac(Controller *controller = NULL);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void prepare(Metadata *imageMetadata) override;
+
+private:
+	CacConfig config_;
+	CacStatus cacStatus_;
+};
+
+} // namespace RPiController
diff --git a/src/ipa/rpi/controller/rpi/ccm.cpp b/src/ipa/rpi/controller/rpi/ccm.cpp
index 2e2e666..aefa580 100644
--- a/src/ipa/rpi/controller/rpi/ccm.cpp
+++ b/src/ipa/rpi/controller/rpi/ccm.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * ccm.cpp - CCM (colour correction matrix) control algorithm
+ * CCM (colour correction matrix) control algorithm
  */
 
 #include <libcamera/base/log.h>
@@ -71,9 +71,9 @@
 	int ret;
 
 	if (params.contains("saturation")) {
-		ret = config_.saturation.read(params["saturation"]);
-		if (ret)
-			return ret;
+		config_.saturation = params["saturation"].get<ipa::Pwl>(ipa::Pwl{});
+		if (config_.saturation.empty())
+			return -EINVAL;
 	}
 
 	for (auto &p : params["ccms"].asList()) {
@@ -113,8 +113,10 @@
 {
 }
 
+namespace {
+
 template<typename T>
-static bool getLocked(Metadata *metadata, std::string const &tag, T &value)
+bool getLocked(Metadata *metadata, std::string const &tag, T &value)
 {
 	T *ptr = metadata->getLocked<T>(tag);
 	if (ptr == nullptr)
@@ -149,6 +151,8 @@
 	return Y2RGB * S * RGB2Y * ccm;
 }
 
+} /* namespace */
+
 void Ccm::prepare(Metadata *imageMetadata)
 {
 	bool awbOk = false, luxOk = false;
@@ -172,7 +176,7 @@
 	ccmStatus.saturation = saturation;
 	if (!config_.saturation.empty())
 		saturation *= config_.saturation.eval(
-			config_.saturation.domain().clip(lux.lux));
+			config_.saturation.domain().clamp(lux.lux));
 	ccm = applySaturation(ccm, saturation);
 	for (int j = 0; j < 3; j++)
 		for (int i = 0; i < 3; i++)
diff --git a/src/ipa/rpi/controller/rpi/ccm.h b/src/ipa/rpi/controller/rpi/ccm.h
index 286d0b3..4e5b33f 100644
--- a/src/ipa/rpi/controller/rpi/ccm.h
+++ b/src/ipa/rpi/controller/rpi/ccm.h
@@ -2,14 +2,15 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * ccm.h - CCM (colour correction matrix) control algorithm
+ * CCM (colour correction matrix) control algorithm
  */
 #pragma once
 
 #include <vector>
 
+#include <libipa/pwl.h>
+
 #include "../ccm_algorithm.h"
-#include "../pwl.h"
 
 namespace RPiController {
 
@@ -54,7 +55,7 @@
 
 struct CcmConfig {
 	std::vector<CtCcm> ccms;
-	Pwl saturation;
+	libcamera::ipa::Pwl saturation;
 };
 
 class Ccm : public CcmAlgorithm
diff --git a/src/ipa/rpi/controller/rpi/contrast.cpp b/src/ipa/rpi/controller/rpi/contrast.cpp
index bee1ead..fe866a5 100644
--- a/src/ipa/rpi/controller/rpi/contrast.cpp
+++ b/src/ipa/rpi/controller/rpi/contrast.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * contrast.cpp - contrast (gamma) control algorithm
+ * contrast (gamma) control algorithm
  */
 #include <stdint.h>
 
@@ -42,6 +42,7 @@
 {
 	// enable adaptive enhancement by default
 	config_.ceEnable = params["ce_enable"].get<int>(1);
+	ceEnable_ = config_.ceEnable;
 	// 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
@@ -52,7 +53,9 @@
 	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"]);
+
+	config_.gammaCurve = params["gamma_curve"].get<ipa::Pwl>(ipa::Pwl{});
+	return config_.gammaCurve.empty() ? -EINVAL : 0;
 }
 
 void Contrast::setBrightness(double brightness)
@@ -65,6 +68,16 @@
 	contrast_ = contrast;
 }
 
+void Contrast::enableCe(bool enable)
+{
+	ceEnable_ = enable;
+}
+
+void Contrast::restoreCe()
+{
+	ceEnable_ = config_.ceEnable;
+}
+
 void Contrast::initialise()
 {
 	/*
@@ -81,10 +94,12 @@
 	imageMetadata->set("contrast.status", status_);
 }
 
-Pwl computeStretchCurve(Histogram const &histogram,
+namespace {
+
+ipa::Pwl computeStretchCurve(Histogram const &histogram,
 			ContrastConfig const &config)
 {
-	Pwl enhance;
+	ipa::Pwl enhance;
 	enhance.append(0, 0);
 	/*
 	 * If the start of the histogram is rather empty, try to pull it down a
@@ -125,10 +140,10 @@
 	return enhance;
 }
 
-Pwl applyManualContrast(Pwl const &gammaCurve, double brightness,
-			double contrast)
+ipa::Pwl applyManualContrast(ipa::Pwl const &gammaCurve, double brightness,
+			     double contrast)
 {
-	Pwl newGammaCurve;
+	ipa::Pwl newGammaCurve;
 	LOG(RPiContrast, Debug)
 		<< "Manual brightness " << brightness << " contrast " << contrast;
 	gammaCurve.map([&](double x, double y) {
@@ -140,6 +155,8 @@
 	return newGammaCurve;
 }
 
+} /* namespace */
+
 void Contrast::process(StatisticsPtr &stats,
 		       [[maybe_unused]] Metadata *imageMetadata)
 {
@@ -149,8 +166,8 @@
 	 * 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) {
+	ipa::Pwl gammaCurve = config_.gammaCurve;
+	if (ceEnable_) {
 		if (config_.loMax != 0 || config_.hiMax != 0)
 			gammaCurve = computeStretchCurve(histogram, config_).compose(gammaCurve);
 		/*
diff --git a/src/ipa/rpi/controller/rpi/contrast.h b/src/ipa/rpi/controller/rpi/contrast.h
index 9c81277..c0f7db9 100644
--- a/src/ipa/rpi/controller/rpi/contrast.h
+++ b/src/ipa/rpi/controller/rpi/contrast.h
@@ -2,14 +2,15 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * contrast.h - contrast (gamma) control algorithm
+ * contrast (gamma) control algorithm
  */
 #pragma once
 
 #include <mutex>
 
+#include <libipa/pwl.h>
+
 #include "../contrast_algorithm.h"
-#include "../pwl.h"
 
 namespace RPiController {
 
@@ -26,7 +27,7 @@
 	double hiHistogram;
 	double hiLevel;
 	double hiMax;
-	Pwl gammaCurve;
+	libcamera::ipa::Pwl gammaCurve;
 };
 
 class Contrast : public ContrastAlgorithm
@@ -37,6 +38,8 @@
 	int read(const libcamera::YamlObject &params) override;
 	void setBrightness(double brightness) override;
 	void setContrast(double contrast) override;
+	void enableCe(bool enable) override;
+	void restoreCe() override;
 	void initialise() override;
 	void prepare(Metadata *imageMetadata) override;
 	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
@@ -46,6 +49,7 @@
 	double brightness_;
 	double contrast_;
 	ContrastStatus status_;
+	double ceEnable_;
 };
 
 } /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/denoise.cpp b/src/ipa/rpi/controller/rpi/denoise.cpp
new file mode 100644
index 0000000..ba85165
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/denoise.cpp
@@ -0,0 +1,198 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022 Raspberry Pi Ltd
+ *
+ * Denoise (spatial, colour, temporal) control algorithm
+ */
+#include "denoise.h"
+
+#include <libcamera/base/log.h>
+
+#include "denoise_status.h"
+#include "noise_status.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiDenoise)
+
+// Calculate settings for the denoise blocks using the noise profile in
+// the image metadata.
+
+#define NAME "rpi.denoise"
+
+int DenoiseConfig::read(const libcamera::YamlObject &params)
+{
+	sdnEnable = params.contains("sdn");
+	if (sdnEnable) {
+		auto &sdnParams = params["sdn"];
+		sdnDeviation = sdnParams["deviation"].get<double>(3.2);
+		sdnStrength = sdnParams["strength"].get<double>(0.25);
+		sdnDeviation2 = sdnParams["deviation2"].get<double>(sdnDeviation);
+		sdnDeviationNoTdn = sdnParams["deviation_no_tdn"].get<double>(sdnDeviation);
+		sdnStrengthNoTdn = sdnParams["strength_no_tdn"].get<double>(sdnStrength);
+		sdnTdnBackoff = sdnParams["backoff"].get<double>(0.75);
+	}
+
+	cdnEnable = params.contains("cdn");
+	if (cdnEnable) {
+		auto &cdnParams = params["cdn"];
+		cdnDeviation = cdnParams["deviation"].get<double>(120);
+		cdnStrength = cdnParams["strength"].get<double>(0.2);
+	}
+
+	tdnEnable = params.contains("tdn");
+	if (tdnEnable) {
+		auto &tdnParams = params["tdn"];
+		tdnDeviation = tdnParams["deviation"].get<double>(0.5);
+		tdnThreshold = tdnParams["threshold"].get<double>(0.75);
+	} else if (sdnEnable) {
+		/*
+		 * If SDN is enabled but TDN isn't, overwrite all the SDN settings
+		 * with the "no TDN" versions. This makes it easier to enable or
+		 * disable TDN in the tuning file without editing all the other
+		 * parameters.
+		 */
+		sdnDeviation = sdnDeviation2 = sdnDeviationNoTdn;
+		sdnStrength = sdnStrengthNoTdn;
+	}
+
+	return 0;
+}
+
+Denoise::Denoise(Controller *controller)
+	: DenoiseAlgorithm(controller), mode_(DenoiseMode::ColourHighQuality)
+{
+}
+
+char const *Denoise::name() const
+{
+	return NAME;
+}
+
+int Denoise::read(const libcamera::YamlObject &params)
+{
+	if (!params.contains("normal")) {
+		configs_["normal"].read(params);
+		currentConfig_ = &configs_["normal"];
+
+		return 0;
+	}
+
+	for (const auto &[key, value] : params.asDict()) {
+		if (configs_[key].read(value)) {
+			LOG(RPiDenoise, Error) << "Failed to read denoise config " << key;
+			return -EINVAL;
+		}
+	}
+
+	auto it = configs_.find("normal");
+	if (it == configs_.end()) {
+		LOG(RPiDenoise, Error) << "No normal denoise settings found";
+		return -EINVAL;
+	}
+	currentConfig_ = &it->second;
+
+	return 0;
+}
+
+void Denoise::initialise()
+{
+}
+
+void Denoise::switchMode([[maybe_unused]] CameraMode const &cameraMode,
+			 [[maybe_unused]] Metadata *metadata)
+{
+	/* A mode switch effectively resets temporal denoise and it has to start over. */
+	currentSdnDeviation_ = currentConfig_->sdnDeviationNoTdn;
+	currentSdnStrength_ = currentConfig_->sdnStrengthNoTdn;
+	currentSdnDeviation2_ = currentConfig_->sdnDeviationNoTdn;
+}
+
+void Denoise::prepare(Metadata *imageMetadata)
+{
+	struct NoiseStatus noiseStatus = {};
+	noiseStatus.noiseSlope = 3.0; // in case no metadata
+	if (imageMetadata->get("noise.status", noiseStatus) != 0)
+		LOG(RPiDenoise, Warning) << "no noise profile found";
+
+	LOG(RPiDenoise, Debug)
+		<< "Noise profile: constant " << noiseStatus.noiseConstant
+		<< " slope " << noiseStatus.noiseSlope;
+
+	if (mode_ == DenoiseMode::Off)
+		return;
+
+	if (currentConfig_->sdnEnable) {
+		struct SdnStatus sdn;
+		sdn.noiseConstant = noiseStatus.noiseConstant * currentSdnDeviation_;
+		sdn.noiseSlope = noiseStatus.noiseSlope * currentSdnDeviation_;
+		sdn.noiseConstant2 = noiseStatus.noiseConstant * currentConfig_->sdnDeviation2;
+		sdn.noiseSlope2 = noiseStatus.noiseSlope * currentSdnDeviation2_;
+		sdn.strength = currentSdnStrength_;
+		imageMetadata->set("sdn.status", sdn);
+		LOG(RPiDenoise, Debug)
+			<< "const " << sdn.noiseConstant
+			<< " slope " << sdn.noiseSlope
+			<< " str " << sdn.strength
+			<< " const2 " << sdn.noiseConstant2
+			<< " slope2 " << sdn.noiseSlope2;
+
+		/* For the next frame, we back off the SDN parameters as TDN ramps up. */
+		double f = currentConfig_->sdnTdnBackoff;
+		currentSdnDeviation_ = f * currentSdnDeviation_ + (1 - f) * currentConfig_->sdnDeviation;
+		currentSdnStrength_ = f * currentSdnStrength_ + (1 - f) * currentConfig_->sdnStrength;
+		currentSdnDeviation2_ = f * currentSdnDeviation2_ + (1 - f) * currentConfig_->sdnDeviation2;
+	}
+
+	if (currentConfig_->tdnEnable) {
+		struct TdnStatus tdn;
+		tdn.noiseConstant = noiseStatus.noiseConstant * currentConfig_->tdnDeviation;
+		tdn.noiseSlope = noiseStatus.noiseSlope * currentConfig_->tdnDeviation;
+		tdn.threshold = currentConfig_->tdnThreshold;
+		imageMetadata->set("tdn.status", tdn);
+		LOG(RPiDenoise, Debug)
+			<< "programmed tdn threshold " << tdn.threshold
+			<< " constant " << tdn.noiseConstant
+			<< " slope " << tdn.noiseSlope;
+	}
+
+	if (currentConfig_->cdnEnable && mode_ != DenoiseMode::ColourOff) {
+		struct CdnStatus cdn;
+		cdn.threshold = currentConfig_->cdnDeviation * noiseStatus.noiseSlope + noiseStatus.noiseConstant;
+		cdn.strength = currentConfig_->cdnStrength;
+		imageMetadata->set("cdn.status", cdn);
+		LOG(RPiDenoise, Debug)
+			<< "programmed cdn threshold " << cdn.threshold
+			<< " strength " << cdn.strength;
+	}
+}
+
+void Denoise::setMode(DenoiseMode mode)
+{
+	// We only distinguish between off and all other modes.
+	mode_ = mode;
+}
+
+void Denoise::setConfig(std::string const &name)
+{
+	auto it = configs_.find(name);
+	if (it == configs_.end()) {
+		/*
+		 * Some platforms may have no need for different denoise settings, so we only issue
+		 * a warning if there clearly are several configurations.
+		 */
+		if (configs_.size() > 1)
+			LOG(RPiDenoise, Warning) << "No denoise config found for " << name;
+		else
+			LOG(RPiDenoise, Debug) << "No denoise config found for " << name;
+	} else
+		currentConfig_ = &it->second;
+}
+
+// Register algorithm with the system.
+static Algorithm *Create(Controller *controller)
+{
+	return (Algorithm *)new Denoise(controller);
+}
+static RegisterAlgorithm reg(NAME, &Create);
diff --git a/src/ipa/rpi/controller/rpi/denoise.h b/src/ipa/rpi/controller/rpi/denoise.h
new file mode 100644
index 0000000..92ff4f9
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/denoise.h
@@ -0,0 +1,59 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022, Raspberry Pi Ltd
+ *
+ * denoise.hpp - Denoise (spatial, colour, temporal) control algorithm
+ */
+#pragma once
+
+#include <map>
+#include <string>
+
+#include "algorithm.h"
+#include "denoise_algorithm.h"
+
+namespace RPiController {
+
+// Algorithm to calculate correct denoise settings.
+
+struct DenoiseConfig {
+	double sdnDeviation;
+	double sdnStrength;
+	double sdnDeviation2;
+	double sdnDeviationNoTdn;
+	double sdnStrengthNoTdn;
+	double sdnTdnBackoff;
+	double cdnDeviation;
+	double cdnStrength;
+	double tdnDeviation;
+	double tdnThreshold;
+	bool tdnEnable;
+	bool sdnEnable;
+	bool cdnEnable;
+	int read(const libcamera::YamlObject &params);
+};
+
+class Denoise : public DenoiseAlgorithm
+{
+public:
+	Denoise(Controller *controller);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void initialise() override;
+	void switchMode(CameraMode const &cameraMode, Metadata *metadata) override;
+	void prepare(Metadata *imageMetadata) override;
+	void setMode(DenoiseMode mode) override;
+	void setConfig(std::string const &name) override;
+
+private:
+	std::map<std::string, DenoiseConfig> configs_;
+	DenoiseConfig *currentConfig_;
+	DenoiseMode mode_;
+
+	/* SDN parameters attenuate over time if TDN is running. */
+	double currentSdnDeviation_;
+	double currentSdnStrength_;
+	double currentSdnDeviation2_;
+};
+
+} // namespace RPiController
diff --git a/src/ipa/rpi/controller/rpi/dpc.cpp b/src/ipa/rpi/controller/rpi/dpc.cpp
index be3871d..8aac03f 100644
--- a/src/ipa/rpi/controller/rpi/dpc.cpp
+++ b/src/ipa/rpi/controller/rpi/dpc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * dpc.cpp - DPC (defective pixel correction) control algorithm
+ * DPC (defective pixel correction) control algorithm
  */
 
 #include <libcamera/base/log.h>
diff --git a/src/ipa/rpi/controller/rpi/dpc.h b/src/ipa/rpi/controller/rpi/dpc.h
index 84a0560..9cefb06 100644
--- a/src/ipa/rpi/controller/rpi/dpc.h
+++ b/src/ipa/rpi/controller/rpi/dpc.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * dpc.h - DPC (defective pixel correction) control algorithm
+ * DPC (defective pixel correction) control algorithm
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/rpi/focus.h b/src/ipa/rpi/controller/rpi/focus.h
index 8556039..ee014be 100644
--- a/src/ipa/rpi/controller/rpi/focus.h
+++ b/src/ipa/rpi/controller/rpi/focus.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * focus.h - focus algorithm
+ * focus algorithm
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/rpi/geq.cpp b/src/ipa/rpi/controller/rpi/geq.cpp
index 510870e..40e7191 100644
--- a/src/ipa/rpi/controller/rpi/geq.cpp
+++ b/src/ipa/rpi/controller/rpi/geq.cpp
@@ -2,14 +2,13 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * geq.cpp - GEQ (green equalisation) control algorithm
+ * GEQ (green equalisation) control algorithm
  */
 
 #include <libcamera/base/log.h>
 
 #include "../device_status.h"
 #include "../lux_status.h"
-#include "../pwl.h"
 
 #include "geq.h"
 
@@ -45,9 +44,9 @@
 	}
 
 	if (params.contains("strength")) {
-		int ret = config_.strength.read(params["strength"]);
-		if (ret)
-			return ret;
+		config_.strength = params["strength"].get<ipa::Pwl>(ipa::Pwl{});
+		if (config_.strength.empty())
+			return -EINVAL;
 	}
 
 	return 0;
@@ -67,7 +66,7 @@
 	GeqStatus geqStatus = {};
 	double strength = config_.strength.empty()
 			? 1.0
-			: config_.strength.eval(config_.strength.domain().clip(luxStatus.lux));
+			: config_.strength.eval(config_.strength.domain().clamp(luxStatus.lux));
 	strength *= deviceStatus.analogueGain;
 	double offset = config_.offset * strength;
 	double slope = config_.slope * strength;
diff --git a/src/ipa/rpi/controller/rpi/geq.h b/src/ipa/rpi/controller/rpi/geq.h
index ee3a52f..e8b9f42 100644
--- a/src/ipa/rpi/controller/rpi/geq.h
+++ b/src/ipa/rpi/controller/rpi/geq.h
@@ -2,10 +2,12 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * geq.h - GEQ (green equalisation) control algorithm
+ * GEQ (green equalisation) control algorithm
  */
 #pragma once
 
+#include <libipa/pwl.h>
+
 #include "../algorithm.h"
 #include "../geq_status.h"
 
@@ -16,7 +18,7 @@
 struct GeqConfig {
 	uint16_t offset;
 	double slope;
-	Pwl strength; /* lux to strength factor */
+	libcamera::ipa::Pwl strength; /* lux to strength factor */
 };
 
 class Geq : public Algorithm
diff --git a/src/ipa/rpi/controller/rpi/hdr.cpp b/src/ipa/rpi/controller/rpi/hdr.cpp
new file mode 100644
index 0000000..f3da829
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/hdr.cpp
@@ -0,0 +1,417 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2023 Raspberry Pi Ltd
+ *
+ * HDR control algorithm
+ */
+
+#include "hdr.h"
+
+#include <cmath>
+
+#include <libcamera/base/log.h>
+
+#include "../agc_status.h"
+#include "../alsc_status.h"
+#include "../stitch_status.h"
+#include "../tonemap_status.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiHdr)
+
+#define NAME "rpi.hdr"
+
+void HdrConfig::read(const libcamera::YamlObject &params, const std::string &modeName)
+{
+	name = modeName;
+
+	if (!params.contains("cadence"))
+		LOG(RPiHdr, Fatal) << "No cadence for HDR mode " << name;
+	cadence = params["cadence"].getList<unsigned int>().value();
+	if (cadence.empty())
+		LOG(RPiHdr, Fatal) << "Empty cadence in HDR mode " << name;
+
+	/*
+	 * In the JSON file it's easier to use the channel name as the key, but
+	 * for us it's convenient to swap them over.
+	 */
+	for (const auto &[k, v] : params["channel_map"].asDict())
+		channelMap[v.get<unsigned int>().value()] = k;
+
+	/* Lens shading related parameters. */
+	if (params.contains("spatial_gain_curve")) {
+		spatialGainCurve = params["spatial_gain_curve"].get<ipa::Pwl>(ipa::Pwl{});
+	} else if (params.contains("spatial_gain")) {
+		double spatialGain = params["spatial_gain"].get<double>(2.0);
+		spatialGainCurve.append(0.0, spatialGain);
+		spatialGainCurve.append(0.01, spatialGain);
+		spatialGainCurve.append(0.06, 1.0); /* maybe make this programmable? */
+		spatialGainCurve.append(1.0, 1.0);
+	}
+
+	diffusion = params["diffusion"].get<unsigned int>(3);
+	/* Clip to an arbitrary limit just to stop typos from killing the system! */
+	const unsigned int MAX_DIFFUSION = 15;
+	if (diffusion > MAX_DIFFUSION) {
+		diffusion = MAX_DIFFUSION;
+		LOG(RPiHdr, Warning) << "Diffusion value clipped to " << MAX_DIFFUSION;
+	}
+
+	/* Read any tonemap parameters. */
+	tonemapEnable = params["tonemap_enable"].get<int>(0);
+	detailConstant = params["detail_constant"].get<uint16_t>(0);
+	detailSlope = params["detail_slope"].get<double>(0.0);
+	iirStrength = params["iir_strength"].get<double>(8.0);
+	strength = params["strength"].get<double>(1.5);
+	if (tonemapEnable)
+		tonemap = params["tonemap"].get<ipa::Pwl>(ipa::Pwl{});
+	speed = params["speed"].get<double>(1.0);
+	if (params.contains("hi_quantile_targets")) {
+		hiQuantileTargets = params["hi_quantile_targets"].getList<double>().value();
+		if (hiQuantileTargets.empty() || hiQuantileTargets.size() % 2)
+			LOG(RPiHdr, Fatal) << "hi_quantile_targets much be even and non-empty";
+	} else
+		hiQuantileTargets = { 0.95, 0.65, 0.5, 0.28, 0.3, 0.25 };
+	hiQuantileMaxGain = params["hi_quantile_max_gain"].get<double>(1.6);
+	if (params.contains("quantile_targets")) {
+		quantileTargets = params["quantile_targets"].getList<double>().value();
+		if (quantileTargets.empty() || quantileTargets.size() % 2)
+			LOG(RPiHdr, Fatal) << "quantile_targets much be even and non-empty";
+	} else
+		quantileTargets = { 0.2, 0.03, 1.0, 0.15 };
+	powerMin = params["power_min"].get<double>(0.65);
+	powerMax = params["power_max"].get<double>(1.0);
+	if (params.contains("contrast_adjustments")) {
+		contrastAdjustments = params["contrast_adjustments"].getList<double>().value();
+	} else
+		contrastAdjustments = { 0.5, 0.75 };
+
+	/* Read any stitch parameters. */
+	stitchEnable = params["stitch_enable"].get<int>(0);
+	thresholdLo = params["threshold_lo"].get<uint16_t>(50000);
+	motionThreshold = params["motion_threshold"].get<double>(0.005);
+	diffPower = params["diff_power"].get<uint8_t>(13);
+	if (diffPower > 15)
+		LOG(RPiHdr, Fatal) << "Bad diff_power value in HDR mode " << name;
+}
+
+Hdr::Hdr(Controller *controller)
+	: HdrAlgorithm(controller)
+{
+	regions_ = controller->getHardwareConfig().awbRegions;
+	numRegions_ = regions_.width * regions_.height;
+	gains_[0].resize(numRegions_, 1.0);
+	gains_[1].resize(numRegions_, 1.0);
+}
+
+char const *Hdr::name() const
+{
+	return NAME;
+}
+
+int Hdr::read(const libcamera::YamlObject &params)
+{
+	/* Make an "HDR off" mode by default so that tuning files don't have to. */
+	HdrConfig &offMode = config_["Off"];
+	offMode.name = "Off";
+	offMode.cadence = { 0 };
+	offMode.channelMap[0] = "None";
+	status_.mode = offMode.name;
+	delayedStatus_.mode = offMode.name;
+
+	/*
+	 * But we still allow the tuning file to override the "Off" mode if it wants.
+	 * For example, maybe an application will make channel 0 be the "short"
+	 * channel, in order to apply other AGC controls to it.
+	 */
+	for (const auto &[key, value] : params.asDict())
+		config_[key].read(value, key);
+
+	return 0;
+}
+
+int Hdr::setMode(std::string const &mode)
+{
+	/* Always validate the mode, so it can be used later without checking. */
+	auto it = config_.find(mode);
+	if (it == config_.end()) {
+		LOG(RPiHdr, Warning) << "No such HDR mode " << mode;
+		return -1;
+	}
+
+	status_.mode = it->second.name;
+
+	return 0;
+}
+
+std::vector<unsigned int> Hdr::getChannels() const
+{
+	return config_.at(status_.mode).cadence;
+}
+
+void Hdr::updateAgcStatus(Metadata *metadata)
+{
+	std::scoped_lock lock(*metadata);
+	AgcStatus *agcStatus = metadata->getLocked<AgcStatus>("agc.status");
+	if (agcStatus) {
+		HdrConfig &hdrConfig = config_[status_.mode];
+		auto it = hdrConfig.channelMap.find(agcStatus->channel);
+		if (it != hdrConfig.channelMap.end()) {
+			status_.channel = it->second;
+			agcStatus->hdr = status_;
+		} else
+			LOG(RPiHdr, Warning) << "Channel " << agcStatus->channel
+					     << " not found in mode " << status_.mode;
+	} else
+		LOG(RPiHdr, Warning) << "No agc.status found";
+}
+
+void Hdr::switchMode([[maybe_unused]] CameraMode const &cameraMode, Metadata *metadata)
+{
+	updateAgcStatus(metadata);
+	delayedStatus_ = status_;
+}
+
+void Hdr::prepare(Metadata *imageMetadata)
+{
+	AgcStatus agcStatus;
+	if (!imageMetadata->get<AgcStatus>("agc.delayed_status", agcStatus))
+		delayedStatus_ = agcStatus.hdr;
+
+	auto it = config_.find(delayedStatus_.mode);
+	if (it == config_.end()) {
+		/* Shouldn't be possible. There would be nothing we could do. */
+		LOG(RPiHdr, Warning) << "Unexpected HDR mode " << delayedStatus_.mode;
+		return;
+	}
+
+	HdrConfig &config = it->second;
+	if (config.spatialGainCurve.empty())
+		return;
+
+	AlscStatus alscStatus{}; /* some compilers seem to require the braces */
+	if (imageMetadata->get<AlscStatus>("alsc.status", alscStatus)) {
+		LOG(RPiHdr, Warning) << "No ALSC status";
+		return;
+	}
+
+	/* The final gains ended up in the odd or even array, according to diffusion. */
+	std::vector<double> &gains = gains_[config.diffusion & 1];
+	for (unsigned int i = 0; i < numRegions_; i++) {
+		alscStatus.r[i] *= gains[i];
+		alscStatus.g[i] *= gains[i];
+		alscStatus.b[i] *= gains[i];
+	}
+	imageMetadata->set("alsc.status", alscStatus);
+}
+
+bool Hdr::updateTonemap([[maybe_unused]] StatisticsPtr &stats, HdrConfig &config)
+{
+	/* When there's a change of HDR mode we start over with a new tonemap curve. */
+	if (delayedStatus_.mode != previousMode_) {
+		previousMode_ = delayedStatus_.mode;
+		tonemap_ = ipa::Pwl();
+	}
+
+	/* No tonemapping. No need to output a tonemap.status. */
+	if (!config.tonemapEnable)
+		return false;
+
+	/* If an explicit tonemap was given, use it. */
+	if (!config.tonemap.empty()) {
+		tonemap_ = config.tonemap;
+		return true;
+	}
+
+	/*
+	 * We wouldn't update the tonemap on short frames when in multi-exposure mode. But
+	 * we still need to output the most recent tonemap. Possibly we should make the
+	 * config indicate the channels for which we should update the tonemap?
+	 */
+	if (delayedStatus_.mode == "MultiExposure" && delayedStatus_.channel != "short")
+		return true;
+
+	/*
+	 * Create a tonemap dynamically. We have three ingredients.
+	 *
+	 * 1. We have a list of "hi quantiles" and "targets". We use these to judge if
+	 * the image does seem to be reasonably saturated. If it isn't, we calculate
+	 * a gain that we will feed as a linear factor into the tonemap generation.
+	 * This prevents unsaturated images from beoming quite so "flat".
+	 *
+	 * 2. We have a list of quantile/target pairs for the bottom of the histogram.
+	 * We use these to calculate how much gain we must apply to the bottom of the
+	 * tonemap. We apply this gain as a power curve so as not to blow out the top
+	 * end.
+	 *
+	 * 3. Finally, when we generate the tonemap, we have some contrast adjustments
+	 * for the bottom because we know that power curves can start quite steeply and
+	 * cause a washed-out look.
+	 */
+
+	/* Compute the linear gain from the headroom for saturation at the top. */
+	double gain = 10; /* arbitrary, but hiQuantileMaxGain will clamp it later */
+	for (unsigned int i = 0; i < config.hiQuantileTargets.size(); i += 2) {
+		double quantile = config.hiQuantileTargets[i];
+		double target = config.hiQuantileTargets[i + 1];
+		double value = stats->yHist.interQuantileMean(quantile, 1.0) / 1024.0;
+		double newGain = target / (value + 0.01);
+		gain = std::min(gain, newGain);
+	}
+	gain = std::clamp(gain, 1.0, config.hiQuantileMaxGain);
+
+	/* Compute the power curve from the amount of gain needed at the bottom. */
+	double min_power = 2; /* arbitrary, but config.powerMax will clamp it later */
+	for (unsigned int i = 0; i < config.quantileTargets.size(); i += 2) {
+		double quantile = config.quantileTargets[i];
+		double target = config.quantileTargets[i + 1];
+		double value = stats->yHist.interQuantileMean(0, quantile) / 1024.0;
+		value = std::min(value * gain, 1.0);
+		double power = log(target + 1e-6) / log(value + 1e-6);
+		min_power = std::min(min_power, power);
+	}
+	double power = std::clamp(min_power, config.powerMin, config.powerMax);
+
+	/* Generate the tonemap, including the contrast adjustment factors. */
+	libcamera::ipa::Pwl tonemap;
+	tonemap.append(0, 0);
+	for (unsigned int i = 0; i <= 6; i++) {
+		double x = 1 << (i + 9); /* x loops from 512 to 32768 inclusive */
+		double y = pow(std::min(x * gain, 65535.0) / 65536.0, power) * 65536;
+		if (i < config.contrastAdjustments.size())
+			y *= config.contrastAdjustments[i];
+		if (!tonemap_.empty())
+			y = y * config.speed + tonemap_.eval(x) * (1 - config.speed);
+		tonemap.append(x, y);
+	}
+	tonemap.append(65535, 65535);
+	tonemap_ = tonemap;
+
+	return true;
+}
+
+static void averageGains(std::vector<double> &src, std::vector<double> &dst, const Size &size)
+{
+#define IDX(y, x) ((y)*size.width + (x))
+	unsigned int lastCol = size.width - 1; /* index of last column */
+	unsigned int preLastCol = lastCol - 1; /* and the column before that */
+	unsigned int lastRow = size.height - 1; /* index of last row */
+	unsigned int preLastRow = lastRow - 1; /* and the row before that */
+
+	/* Corners first. */
+	dst[IDX(0, 0)] = (src[IDX(0, 0)] + src[IDX(0, 1)] + src[IDX(1, 0)]) / 3;
+	dst[IDX(0, lastCol)] = (src[IDX(0, lastCol)] + src[IDX(0, preLastCol)] + src[IDX(1, lastCol)]) / 3;
+	dst[IDX(lastRow, 0)] = (src[IDX(lastRow, 0)] + src[IDX(lastRow, 1)] + src[IDX(preLastRow, 0)]) / 3;
+	dst[IDX(lastRow, lastCol)] = (src[IDX(lastRow, lastCol)] + src[IDX(lastRow, preLastCol)] +
+				      src[IDX(preLastRow, lastCol)]) /
+				     3;
+
+	/* Now the edges. */
+	for (unsigned int i = 1; i < lastCol; i++) {
+		dst[IDX(0, i)] = (src[IDX(0, i - 1)] + src[IDX(0, i)] + src[IDX(0, i + 1)] + src[IDX(1, i)]) / 4;
+		dst[IDX(lastRow, i)] = (src[IDX(lastRow, i - 1)] + src[IDX(lastRow, i)] +
+					src[IDX(lastRow, i + 1)] + src[IDX(preLastRow, i)]) /
+				       4;
+	}
+
+	for (unsigned int i = 1; i < lastRow; i++) {
+		dst[IDX(i, 0)] = (src[IDX(i - 1, 0)] + src[IDX(i, 0)] + src[IDX(i + 1, 0)] + src[IDX(i, 1)]) / 4;
+		dst[IDX(i, 31)] = (src[IDX(i - 1, lastCol)] + src[IDX(i, lastCol)] +
+				   src[IDX(i + 1, lastCol)] + src[IDX(i, preLastCol)]) /
+				  4;
+	}
+
+	/* Finally the interior. */
+	for (unsigned int j = 1; j < lastRow; j++) {
+		for (unsigned int i = 1; i < lastCol; i++) {
+			dst[IDX(j, i)] = (src[IDX(j - 1, i)] + src[IDX(j, i - 1)] + src[IDX(j, i)] +
+					  src[IDX(j, i + 1)] + src[IDX(j + 1, i)]) /
+					 5;
+		}
+	}
+}
+
+void Hdr::updateGains(StatisticsPtr &stats, HdrConfig &config)
+{
+	if (config.spatialGainCurve.empty())
+		return;
+
+	/* When alternating exposures, only compute these gains for the short frame. */
+	if (delayedStatus_.mode == "MultiExposure" && delayedStatus_.channel != "short")
+		return;
+
+	for (unsigned int i = 0; i < numRegions_; i++) {
+		auto &region = stats->awbRegions.get(i);
+		unsigned int counted = region.counted;
+		counted += (counted == 0); /* avoid div by zero */
+		double r = region.val.rSum / counted;
+		double g = region.val.gSum / counted;
+		double b = region.val.bSum / counted;
+		double brightness = std::max({ r, g, b }) / 65535;
+		gains_[0][i] = config.spatialGainCurve.eval(brightness);
+	}
+
+	/* Ping-pong between the two gains_ buffers. */
+	for (unsigned int i = 0; i < config.diffusion; i++)
+		averageGains(gains_[i & 1], gains_[(i & 1) ^ 1], regions_);
+}
+
+void Hdr::process(StatisticsPtr &stats, Metadata *imageMetadata)
+{
+	/* Note what HDR channel this frame will be once it comes back to us. */
+	updateAgcStatus(imageMetadata);
+
+	/*
+	 * Now figure out what HDR channel this frame is. It should be available in the
+	 * agc.delayed_status, unless this is an early frame after a mode switch, in which
+	 * case delayedStatus_ should be right.
+	 */
+	AgcStatus agcStatus;
+	if (!imageMetadata->get<AgcStatus>("agc.delayed_status", agcStatus))
+		delayedStatus_ = agcStatus.hdr;
+
+	auto it = config_.find(delayedStatus_.mode);
+	if (it == config_.end()) {
+		/* Shouldn't be possible. There would be nothing we could do. */
+		LOG(RPiHdr, Warning) << "Unexpected HDR mode " << delayedStatus_.mode;
+		return;
+	}
+
+	HdrConfig &config = it->second;
+
+	/* Update the spatially varying gains. They get written in prepare(). */
+	updateGains(stats, config);
+
+	if (updateTonemap(stats, config)) {
+		/* Add tonemap.status metadata. */
+		TonemapStatus tonemapStatus;
+
+		tonemapStatus.detailConstant = config.detailConstant;
+		tonemapStatus.detailSlope = config.detailSlope;
+		tonemapStatus.iirStrength = config.iirStrength;
+		tonemapStatus.strength = config.strength;
+		tonemapStatus.tonemap = tonemap_;
+
+		imageMetadata->set("tonemap.status", tonemapStatus);
+	}
+
+	if (config.stitchEnable) {
+		/* Add stitch.status metadata. */
+		StitchStatus stitchStatus;
+
+		stitchStatus.diffPower = config.diffPower;
+		stitchStatus.motionThreshold = config.motionThreshold;
+		stitchStatus.thresholdLo = config.thresholdLo;
+
+		imageMetadata->set("stitch.status", stitchStatus);
+	}
+}
+
+/* Register algorithm with the system. */
+static Algorithm *create(Controller *controller)
+{
+	return (Algorithm *)new Hdr(controller);
+}
+static RegisterAlgorithm reg(NAME, &create);
diff --git a/src/ipa/rpi/controller/rpi/hdr.h b/src/ipa/rpi/controller/rpi/hdr.h
new file mode 100644
index 0000000..5c2f398
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/hdr.h
@@ -0,0 +1,85 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2023, Raspberry Pi Ltd
+ *
+ * HDR control algorithm
+ */
+#pragma once
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <libcamera/geometry.h>
+
+#include <libipa/pwl.h>
+
+#include "../hdr_algorithm.h"
+#include "../hdr_status.h"
+
+/* This is our implementation of an HDR algorithm. */
+
+namespace RPiController {
+
+struct HdrConfig {
+	std::string name;
+	std::vector<unsigned int> cadence;
+	std::map<unsigned int, std::string> channelMap;
+
+	/* Lens shading related parameters. */
+	libcamera::ipa::Pwl spatialGainCurve; /* Brightness to gain curve for different image regions. */
+	unsigned int diffusion; /* How much to diffuse the gain spatially. */
+
+	/* Tonemap related parameters. */
+	bool tonemapEnable;
+	uint16_t detailConstant;
+	double detailSlope;
+	double iirStrength;
+	double strength;
+	libcamera::ipa::Pwl tonemap;
+	/* These relate to adaptive tonemap calculation. */
+	double speed;
+	std::vector<double> hiQuantileTargets; /* quantiles to check for unsaturated images */
+	double hiQuantileMaxGain; /* the max gain we'll apply when unsaturated */
+	std::vector<double> quantileTargets; /* target values for histogram quantiles */
+	double powerMin; /* minimum tonemap power */
+	double powerMax; /* maximum tonemap power */
+	std::vector<double> contrastAdjustments; /* any contrast adjustment factors */
+
+	/* Stitch related parameters. */
+	bool stitchEnable;
+	uint16_t thresholdLo;
+	uint8_t diffPower;
+	double motionThreshold;
+
+	void read(const libcamera::YamlObject &params, const std::string &name);
+};
+
+class Hdr : public HdrAlgorithm
+{
+public:
+	Hdr(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;
+	void process(StatisticsPtr &stats, Metadata *imageMetadata) override;
+	int setMode(std::string const &mode) override;
+	std::vector<unsigned int> getChannels() const override;
+
+private:
+	void updateAgcStatus(Metadata *metadata);
+	void updateGains(StatisticsPtr &stats, HdrConfig &config);
+	bool updateTonemap(StatisticsPtr &stats, HdrConfig &config);
+
+	std::map<std::string, HdrConfig> config_;
+	HdrStatus status_; /* track the current HDR mode and channel */
+	HdrStatus delayedStatus_; /* track the delayed HDR mode and channel */
+	std::string previousMode_;
+	libcamera::ipa::Pwl tonemap_;
+	libcamera::Size regions_; /* stats regions */
+	unsigned int numRegions_; /* total number of stats regions */
+	std::vector<double> gains_[2];
+};
+
+} /* namespace RPiController */
diff --git a/src/ipa/rpi/controller/rpi/lux.cpp b/src/ipa/rpi/controller/rpi/lux.cpp
index 06625f3..7b31faa 100644
--- a/src/ipa/rpi/controller/rpi/lux.cpp
+++ b/src/ipa/rpi/controller/rpi/lux.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * lux.cpp - Lux control algorithm
+ * Lux control algorithm
  */
 #include <math.h>
 
diff --git a/src/ipa/rpi/controller/rpi/lux.h b/src/ipa/rpi/controller/rpi/lux.h
index 89411a5..89f441f 100644
--- a/src/ipa/rpi/controller/rpi/lux.h
+++ b/src/ipa/rpi/controller/rpi/lux.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * lux.h - Lux control algorithm
+ * Lux control algorithm
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/rpi/noise.cpp b/src/ipa/rpi/controller/rpi/noise.cpp
index bcd8b9e..3f1c62c 100644
--- a/src/ipa/rpi/controller/rpi/noise.cpp
+++ b/src/ipa/rpi/controller/rpi/noise.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * noise.cpp - Noise control algorithm
+ * Noise control algorithm
  */
 
 #include <math.h>
diff --git a/src/ipa/rpi/controller/rpi/noise.h b/src/ipa/rpi/controller/rpi/noise.h
index 74c31e6..6deae1f 100644
--- a/src/ipa/rpi/controller/rpi/noise.h
+++ b/src/ipa/rpi/controller/rpi/noise.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * noise.h - Noise control algorithm
+ * Noise control algorithm
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/rpi/saturation.cpp b/src/ipa/rpi/controller/rpi/saturation.cpp
new file mode 100644
index 0000000..b83c588
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/saturation.cpp
@@ -0,0 +1,57 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022 Raspberry Pi Ltd
+ *
+ * Saturation control algorithm
+ */
+#include "saturation.h"
+
+#include <libcamera/base/log.h>
+
+#include "saturation_status.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiSaturation)
+
+#define NAME "rpi.saturation"
+
+Saturation::Saturation(Controller *controller)
+	: Algorithm(controller)
+{
+}
+
+char const *Saturation::name() const
+{
+	return NAME;
+}
+
+int Saturation::read(const libcamera::YamlObject &params)
+{
+	config_.shiftR = params["shift_r"].get<uint8_t>(0);
+	config_.shiftG = params["shift_g"].get<uint8_t>(0);
+	config_.shiftB = params["shift_b"].get<uint8_t>(0);
+	return 0;
+}
+
+void Saturation::initialise()
+{
+}
+
+void Saturation::prepare(Metadata *imageMetadata)
+{
+	SaturationStatus saturation;
+
+	saturation.shiftR = config_.shiftR;
+	saturation.shiftG = config_.shiftG;
+	saturation.shiftB = config_.shiftB;
+	imageMetadata->set("saturation.status", saturation);
+}
+
+// Register algorithm with the system.
+static Algorithm *Create(Controller *controller)
+{
+	return (Algorithm *)new Saturation(controller);
+}
+static RegisterAlgorithm reg(NAME, &Create);
diff --git a/src/ipa/rpi/controller/rpi/saturation.h b/src/ipa/rpi/controller/rpi/saturation.h
new file mode 100644
index 0000000..97da412
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/saturation.h
@@ -0,0 +1,32 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022, Raspberry Pi Ltd
+ *
+ * saturation.hpp - Saturation control algorithm
+ */
+#pragma once
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+struct SaturationConfig {
+	uint8_t shiftR;
+	uint8_t shiftG;
+	uint8_t shiftB;
+};
+
+class Saturation : public Algorithm
+{
+public:
+	Saturation(Controller *controller = NULL);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void initialise() override;
+	void prepare(Metadata *imageMetadata) override;
+
+private:
+	SaturationConfig config_;
+};
+
+} // namespace RPiController
diff --git a/src/ipa/rpi/controller/rpi/sdn.cpp b/src/ipa/rpi/controller/rpi/sdn.cpp
index b6b6625..619178a 100644
--- a/src/ipa/rpi/controller/rpi/sdn.cpp
+++ b/src/ipa/rpi/controller/rpi/sdn.cpp
@@ -2,10 +2,11 @@
 /*
  * Copyright (C) 2019-2021, Raspberry Pi Ltd
  *
- * sdn.cpp - SDN (spatial denoise) control algorithm
+ * SDN (spatial denoise) control algorithm
  */
 
 #include <libcamera/base/log.h>
+#include <libcamera/base/utils.h>
 
 #include "../denoise_status.h"
 #include "../noise_status.h"
@@ -36,6 +37,8 @@
 
 int Sdn::read(const libcamera::YamlObject &params)
 {
+	LOG(RPiSdn, Warning)
+		<< "Using legacy SDN tuning - please consider moving SDN inside rpi.denoise";
 	deviation_ = params["deviation"].get<double>(3.2);
 	strength_ = params["strength"].get<double>(0.75);
 	return 0;
@@ -58,7 +61,7 @@
 	status.noiseConstant = noiseStatus.noiseConstant * deviation_;
 	status.noiseSlope = noiseStatus.noiseSlope * deviation_;
 	status.strength = strength_;
-	status.mode = static_cast<std::underlying_type_t<DenoiseMode>>(mode_);
+	status.mode = utils::to_underlying(mode_);
 	imageMetadata->set("denoise.status", status);
 	LOG(RPiSdn, Debug)
 		<< "programmed constant " << status.noiseConstant
diff --git a/src/ipa/rpi/controller/rpi/sdn.h b/src/ipa/rpi/controller/rpi/sdn.h
index 9dd73c3..cb226de 100644
--- a/src/ipa/rpi/controller/rpi/sdn.h
+++ b/src/ipa/rpi/controller/rpi/sdn.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * sdn.h - SDN (spatial denoise) control algorithm
+ * SDN (spatial denoise) control algorithm
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/rpi/sharpen.cpp b/src/ipa/rpi/controller/rpi/sharpen.cpp
index 4f6f020..39537f4 100644
--- a/src/ipa/rpi/controller/rpi/sharpen.cpp
+++ b/src/ipa/rpi/controller/rpi/sharpen.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * sharpen.cpp - sharpening control algorithm
+ * sharpening control algorithm
  */
 
 #include <math.h>
diff --git a/src/ipa/rpi/controller/rpi/sharpen.h b/src/ipa/rpi/controller/rpi/sharpen.h
index 8bb7631..96ccd60 100644
--- a/src/ipa/rpi/controller/rpi/sharpen.h
+++ b/src/ipa/rpi/controller/rpi/sharpen.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * sharpen.h - sharpening control algorithm
+ * sharpening control algorithm
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/rpi/tonemap.cpp b/src/ipa/rpi/controller/rpi/tonemap.cpp
new file mode 100644
index 0000000..3422adf
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/tonemap.cpp
@@ -0,0 +1,61 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022 Raspberry Pi Ltd
+ *
+ * Tonemap control algorithm
+ */
+#include "tonemap.h"
+
+#include <libcamera/base/log.h>
+
+#include "tonemap_status.h"
+
+using namespace RPiController;
+using namespace libcamera;
+
+LOG_DEFINE_CATEGORY(RPiTonemap)
+
+#define NAME "rpi.tonemap"
+
+Tonemap::Tonemap(Controller *controller)
+	: Algorithm(controller)
+{
+}
+
+char const *Tonemap::name() const
+{
+	return NAME;
+}
+
+int Tonemap::read(const libcamera::YamlObject &params)
+{
+	config_.detailConstant = params["detail_constant"].get<uint16_t>(0);
+	config_.detailSlope = params["detail_slope"].get<double>(0.1);
+	config_.iirStrength = params["iir_strength"].get<double>(1.0);
+	config_.strength = params["strength"].get<double>(1.0);
+	config_.tonemap = params["tone_curve"].get<ipa::Pwl>(ipa::Pwl{});
+	return 0;
+}
+
+void Tonemap::initialise()
+{
+}
+
+void Tonemap::prepare(Metadata *imageMetadata)
+{
+	TonemapStatus tonemapStatus;
+
+	tonemapStatus.detailConstant = config_.detailConstant;
+	tonemapStatus.detailSlope = config_.detailSlope;
+	tonemapStatus.iirStrength = config_.iirStrength;
+	tonemapStatus.strength = config_.strength;
+	tonemapStatus.tonemap = config_.tonemap;
+	imageMetadata->set("tonemap.status", tonemapStatus);
+}
+
+// Register algorithm with the system.
+static Algorithm *Create(Controller *controller)
+{
+	return (Algorithm *)new Tonemap(controller);
+}
+static RegisterAlgorithm reg(NAME, &Create);
diff --git a/src/ipa/rpi/controller/rpi/tonemap.h b/src/ipa/rpi/controller/rpi/tonemap.h
new file mode 100644
index 0000000..ba0cf5c
--- /dev/null
+++ b/src/ipa/rpi/controller/rpi/tonemap.h
@@ -0,0 +1,36 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022, Raspberry Pi Ltd
+ *
+ * tonemap.hpp - Tonemap control algorithm
+ */
+#pragma once
+
+#include <libipa/pwl.h>
+
+#include "algorithm.h"
+
+namespace RPiController {
+
+struct TonemapConfig {
+	uint16_t detailConstant;
+	double detailSlope;
+	double iirStrength;
+	double strength;
+	libcamera::ipa::Pwl tonemap;
+};
+
+class Tonemap : public Algorithm
+{
+public:
+	Tonemap(Controller *controller = NULL);
+	char const *name() const override;
+	int read(const libcamera::YamlObject &params) override;
+	void initialise() override;
+	void prepare(Metadata *imageMetadata) override;
+
+private:
+	TonemapConfig config_;
+};
+
+} // namespace RPiController
diff --git a/src/ipa/rpi/controller/saturation_status.h b/src/ipa/rpi/controller/saturation_status.h
new file mode 100644
index 0000000..c7fadc9
--- /dev/null
+++ b/src/ipa/rpi/controller/saturation_status.h
@@ -0,0 +1,13 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022 Raspberry Pi Ltd
+ *
+ * Saturation control algorithm status
+ */
+#pragma once
+
+struct SaturationStatus {
+	uint8_t shiftR;
+	uint8_t shiftG;
+	uint8_t shiftB;
+};
diff --git a/src/ipa/rpi/controller/sharpen_algorithm.h b/src/ipa/rpi/controller/sharpen_algorithm.h
index 3be21c3..abd82cb 100644
--- a/src/ipa/rpi/controller/sharpen_algorithm.h
+++ b/src/ipa/rpi/controller/sharpen_algorithm.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * sharpen_algorithm.h - sharpness control algorithm interface
+ * sharpness control algorithm interface
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/sharpen_status.h b/src/ipa/rpi/controller/sharpen_status.h
index 106166d..7491019 100644
--- a/src/ipa/rpi/controller/sharpen_status.h
+++ b/src/ipa/rpi/controller/sharpen_status.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Raspberry Pi Ltd
  *
- * sharpen_status.h - Sharpen control algorithm status
+ * Sharpen control algorithm status
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/statistics.h b/src/ipa/rpi/controller/statistics.h
index 015d4ef..cbd8116 100644
--- a/src/ipa/rpi/controller/statistics.h
+++ b/src/ipa/rpi/controller/statistics.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Raspberry Pi Ltd
  *
- * statistics.h - Raspberry Pi generic statistics structure
+ * Raspberry Pi generic statistics structure
  */
 #pragma once
 
diff --git a/src/ipa/rpi/controller/stitch_status.h b/src/ipa/rpi/controller/stitch_status.h
new file mode 100644
index 0000000..7812f3e
--- /dev/null
+++ b/src/ipa/rpi/controller/stitch_status.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2023 Raspberry Pi Ltd
+ *
+ * stitch control algorithm status
+ */
+#pragma once
+
+/*
+ * Parameters for the stitch block.
+ */
+
+struct StitchStatus {
+	uint16_t thresholdLo;
+	uint8_t diffPower;
+	double motionThreshold;
+};
diff --git a/src/ipa/rpi/controller/tonemap_status.h b/src/ipa/rpi/controller/tonemap_status.h
new file mode 100644
index 0000000..0364ff6
--- /dev/null
+++ b/src/ipa/rpi/controller/tonemap_status.h
@@ -0,0 +1,17 @@
+/* SPDX-License-Identifier: BSD-2-Clause */
+/*
+ * Copyright (C) 2022 Raspberry Pi Ltd
+ *
+ * Tonemap control algorithm status
+ */
+#pragma once
+
+#include <libipa/pwl.h>
+
+struct TonemapStatus {
+	uint16_t detailConstant;
+	double detailSlope;
+	double iirStrength;
+	double strength;
+	libcamera::ipa::Pwl tonemap;
+};
diff --git a/src/ipa/rpi/vc4/data/imx219.json b/src/ipa/rpi/vc4/data/imx219.json
index e8fce16..a020b12 100644
--- a/src/ipa/rpi/vc4/data/imx219.json
+++ b/src/ipa/rpi/vc4/data/imx219.json
@@ -131,93 +131,306 @@
         {
             "rpi.agc":
             {
-                "metering_modes":
-                {
-                    "centre-weighted":
+                "channels": [
                     {
-                        "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": [
+                        "metering_modes":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.5,
-                                1000, 0.5
-                            ]
-                        }
-                    ],
-                    "highlight": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.5,
-                                1000, 0.5
+                            "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": 0.125,
+                        "metering_modes":
                         {
-                            "bound": "UPPER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.8,
-                                1000, 0.8
-                            ]
-                        }
-                    ],
-                    "shadows": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.0,
-                            "q_hi": 0.5,
-                            "y_target":
-                            [
-                                0, 0.17,
-                                1000, 0.17
+                            "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
+                        },
+                        "y_target":
+                        [
+                            0, 0.16,
+                            1000, 0.165,
+                            10000, 0.17
+                        ]
+                    },
+                    {
+                        "base_ev": 1.5,
+                        "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
+                        ]
+                    }
                 ]
             }
         },
@@ -463,6 +676,20 @@
         },
         {
             "rpi.sharpen": { }
+        },
+        {
+            "rpi.hdr":
+            {
+                "MultiExposureUnmerged":
+                {
+                    "cadence": [ 1, 2 ],
+                    "channel_map":
+                    {
+                        "short": 1,
+                        "long": 2
+                    }
+                }
+            }
         }
     ]
-}
\ 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
index cfedb94..d8bc963 100644
--- a/src/ipa/rpi/vc4/data/imx219_noir.json
+++ b/src/ipa/rpi/vc4/data/imx219_noir.json
@@ -47,93 +47,306 @@
         {
             "rpi.agc":
             {
-                "metering_modes":
-                {
-                    "centre-weighted":
+                "channels": [
                     {
-                        "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": [
+                        "metering_modes":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.5,
-                                1000, 0.5
-                            ]
-                        }
-                    ],
-                    "highlight": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.5,
-                                1000, 0.5
+                            "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": 0.125,
+                        "metering_modes":
                         {
-                            "bound": "UPPER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.8,
-                                1000, 0.8
-                            ]
-                        }
-                    ],
-                    "shadows": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.0,
-                            "q_hi": 0.5,
-                            "y_target":
-                            [
-                                0, 0.17,
-                                1000, 0.17
+                            "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
+                        },
+                        "y_target":
+                        [
+                            0, 0.16,
+                            1000, 0.165,
+                            10000, 0.17
+                        ]
+                    },
+                    {
+                        "base_ev": 1.5,
+                        "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
+                        ]
+                    }
                 ]
             }
         },
@@ -397,6 +610,20 @@
         },
         {
             "rpi.sharpen": { }
+        },
+        {
+            "rpi.hdr":
+            {
+                "MultiExposureUnmerged":
+                {
+                    "cadence": [ 1, 2 ],
+                    "channel_map":
+                    {
+                        "short": 1,
+                        "long": 2
+                    }
+                }
+            }
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/src/ipa/rpi/vc4/data/imx290.json b/src/ipa/rpi/vc4/data/imx290.json
index 8a7cadb..8f41bf5 100644
--- a/src/ipa/rpi/vc4/data/imx290.json
+++ b/src/ipa/rpi/vc4/data/imx290.json
@@ -52,15 +52,24 @@
                 {
                     "matrix":
                     {
-                        "weights": [ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ]
+                        "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 ]
+                        "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 ]
+                        "weights":
+                        [
+                            2, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
+                        ]
                     }
                 },
                 "exposure_modes":
diff --git a/src/ipa/rpi/vc4/data/imx296.json b/src/ipa/rpi/vc4/data/imx296.json
index 7621f75..8f24ce5 100644
--- a/src/ipa/rpi/vc4/data/imx296.json
+++ b/src/ipa/rpi/vc4/data/imx296.json
@@ -135,15 +135,24 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "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 ]
+                        "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 ]
+                        "weights":
+                        [
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
                     }
                 },
                 "exposure_modes":
@@ -431,4 +440,4 @@
             }
         }
     ]
-}
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx296_mono.json b/src/ipa/rpi/vc4/data/imx296_mono.json
index d4140c8..fe33156 100644
--- a/src/ipa/rpi/vc4/data/imx296_mono.json
+++ b/src/ipa/rpi/vc4/data/imx296_mono.json
@@ -38,15 +38,24 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "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 ]
+                        "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 ]
+                        "weights":
+                        [
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
                     }
                 },
                 "exposure_modes":
@@ -228,4 +237,4 @@
             }
         }
     ]
-}
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx378.json b/src/ipa/rpi/vc4/data/imx378.json
index f7b6801..363b47e 100644
--- a/src/ipa/rpi/vc4/data/imx378.json
+++ b/src/ipa/rpi/vc4/data/imx378.json
@@ -133,15 +133,24 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "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 ]
+                        "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 ]
+                        "weights":
+                        [
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
                     }
                 },
                 "exposure_modes":
diff --git a/src/ipa/rpi/vc4/data/imx477.json b/src/ipa/rpi/vc4/data/imx477.json
index 0e39d41..fa25ee8 100644
--- a/src/ipa/rpi/vc4/data/imx477.json
+++ b/src/ipa/rpi/vc4/data/imx477.json
@@ -115,16 +115,16 @@
                 "ct_curve":
                 [
                     2360.0, 0.6009, 0.3093,
-                    2848.0, 0.5071, 0.4000,
+                    2848.0, 0.5071, 0.4,
                     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,
+                    4660.0, 0.3529, 0.68,
+                    5579.0, 0.3227, 0.7,
+                    6125.0, 0.3129, 0.71,
+                    6671.0, 0.3065, 0.72,
+                    7217.0, 0.3014, 0.73,
+                    7763.0, 0.295, 0.74,
                     9505.0, 0.2524, 0.7856
                 ],
                 "sensitivity_r": 1.05,
@@ -136,93 +136,306 @@
         {
             "rpi.agc":
             {
-                "metering_modes":
-                {
-                    "centre-weighted":
+                "channels": [
                     {
-                        "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": [
+                        "metering_modes":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.3,
-                                1000, 0.3
-                            ]
-                        }
-                    ],
-                    "highlight": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.3,
-                                1000, 0.3
+                            "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
+                        ]
+                    },
+                    {
+                        "base_ev": 0.125,
+                        "metering_modes":
                         {
-                            "bound": "UPPER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.8,
-                                1000, 0.8
-                            ]
-                        }
-                    ],
-                    "shadows": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.0,
-                            "q_hi": 0.5,
-                            "y_target":
-                            [
-                                0, 0.17,
-                                1000, 0.17
+                            "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
+                        },
+                        "y_target":
+                        [
+                            0, 0.16,
+                            1000, 0.165,
+                            10000, 0.17
+                        ]
+                    },
+                    {
+                        "base_ev": 1.5,
+                        "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
+                        ]
+                    }
                 ]
             }
         },
@@ -468,6 +681,20 @@
         },
         {
             "rpi.sharpen": { }
+        },
+        {
+            "rpi.hdr":
+            {
+                "MultiExposureUnmerged":
+                {
+                    "cadence": [ 1, 2 ],
+                    "channel_map":
+                    {
+                        "short": 1,
+                        "long": 2
+                    }
+                }
+            }
         }
     ]
 }
\ 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
index 52d7f07..472f33f 100644
--- a/src/ipa/rpi/vc4/data/imx477_noir.json
+++ b/src/ipa/rpi/vc4/data/imx477_noir.json
@@ -47,93 +47,306 @@
         {
             "rpi.agc":
             {
-                "metering_modes":
-                {
-                    "centre-weighted":
+                "channels": [
                     {
-                        "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": [
+                        "metering_modes":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.3,
-                                1000, 0.3
-                            ]
-                        }
-                    ],
-                    "highlight": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.3,
-                                1000, 0.3
+                            "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
+                        ]
+                    },
+                    {
+                        "base_ev": 0.125,
+                        "metering_modes":
                         {
-                            "bound": "UPPER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.8,
-                                1000, 0.8
-                            ]
-                        }
-                    ],
-                    "shadows": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.0,
-                            "q_hi": 0.5,
-                            "y_target":
-                            [
-                                0, 0.17,
-                                1000, 0.17
+                            "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
+                        },
+                        "y_target":
+                        [
+                            0, 0.16,
+                            1000, 0.165,
+                            10000, 0.17
+                        ]
+                    },
+                    {
+                        "base_ev": 1.5,
+                        "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
+                        ]
+                    }
                 ]
             }
         },
@@ -424,6 +637,20 @@
         },
         {
             "rpi.sharpen": { }
+        },
+        {
+            "rpi.hdr":
+            {
+                "MultiExposureUnmerged":
+                {
+                    "cadence": [ 1, 2 ],
+                    "channel_map":
+                    {
+                        "short": 1,
+                        "long": 2
+                    }
+                }
+            }
         }
     ]
-}
\ 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
index 26c692f..9dc32eb 100644
--- a/src/ipa/rpi/vc4/data/imx477_scientific.json
+++ b/src/ipa/rpi/vc4/data/imx477_scientific.json
@@ -148,15 +148,24 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "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 ]
+                        "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 ]
+                        "weights":
+                        [
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
                     }
                 },
                 "exposure_modes":
diff --git a/src/ipa/rpi/vc4/data/imx477_v1.json b/src/ipa/rpi/vc4/data/imx477_v1.json
index d640200..55e4adc 100644
--- a/src/ipa/rpi/vc4/data/imx477_v1.json
+++ b/src/ipa/rpi/vc4/data/imx477_v1.json
@@ -138,15 +138,24 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "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 ]
+                        "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 ]
+                        "weights":
+                        [
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
                     }
                 },
                 "exposure_modes":
diff --git a/src/ipa/rpi/vc4/data/imx519.json b/src/ipa/rpi/vc4/data/imx519.json
index 1b0a774..ce19425 100644
--- a/src/ipa/rpi/vc4/data/imx519.json
+++ b/src/ipa/rpi/vc4/data/imx519.json
@@ -133,15 +133,24 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "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 ]
+                        "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 ]
+                        "weights":
+                        [
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
                     }
                 },
                 "exposure_modes":
diff --git a/src/ipa/rpi/vc4/data/imx708.json b/src/ipa/rpi/vc4/data/imx708.json
index c40a599..4de6f07 100644
--- a/src/ipa/rpi/vc4/data/imx708.json
+++ b/src/ipa/rpi/vc4/data/imx708.json
@@ -139,85 +139,280 @@
         {
             "rpi.agc":
             {
-                "metering_modes":
-                {
-                    "centre-weighted":
+                "channels": [
                     {
-                        "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 ]
-                    },
-                    "long":
-                    {
-                        "shutter": [ 1000, 30000, 60000, 90000, 120000 ],
-                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
-                    }
-                },
-                "constraint_modes":
-                {
-                    "normal": [
+                        "metering_modes":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.2,
-                                1000, 0.2
-                            ]
-                        }
-                    ],
-                    "highlight": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.2,
-                                1000, 0.2
+                            "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                    },
+                    {
+                        "base_ev": 0.125,
+                        "metering_modes":
                         {
-                            "bound": "UPPER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.8,
-                                1000, 0.8
+                            "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                        },
+                        "y_target":
+                        [
+                            0, 0.16,
+                            1000, 0.165,
+                            10000, 0.17
+                        ],
+                        "startup_frames": 5,
+                        "convergence_frames": 6,
+                        "speed": 0.15
+                    },
+                    {
+                        "base_ev": 1.5,
+                        "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                    }
+                ]
             }
         },
         {
@@ -457,6 +652,20 @@
                 "skip_frames": 5,
                 "map": [ 0.0, 445, 15.0, 925 ]
             }
+        },
+        {
+            "rpi.hdr":
+            {
+                "MultiExposureUnmerged":
+                {
+                    "cadence": [ 1, 2 ],
+                    "channel_map":
+                    {
+                        "short": 1,
+                        "long": 2
+                    }
+                }
+            }
         }
     ]
-}
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx708_noir.json b/src/ipa/rpi/vc4/data/imx708_noir.json
index 8d49815..7b7ee87 100644
--- a/src/ipa/rpi/vc4/data/imx708_noir.json
+++ b/src/ipa/rpi/vc4/data/imx708_noir.json
@@ -139,85 +139,280 @@
         {
             "rpi.agc":
             {
-                "metering_modes":
-                {
-                    "centre-weighted":
+                "channels": [
                     {
-                        "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 ]
-                    },
-                    "long":
-                    {
-                        "shutter": [ 1000, 30000, 60000, 90000, 120000 ],
-                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
-                    }
-                },
-                "constraint_modes":
-                {
-                    "normal": [
+                        "metering_modes":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.2,
-                                1000, 0.2
-                            ]
-                        }
-                    ],
-                    "highlight": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.2,
-                                1000, 0.2
+                            "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                    },
+                    {
+                        "base_ev": 0.125,
+                        "metering_modes":
                         {
-                            "bound": "UPPER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.8,
-                                1000, 0.8
+                            "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                        },
+                        "y_target":
+                        [
+                            0, 0.16,
+                            1000, 0.165,
+                            10000, 0.17
+                        ],
+                        "startup_frames": 5,
+                        "convergence_frames": 6,
+                        "speed": 0.15
+                    },
+                    {
+                        "base_ev": 1.5,
+                        "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                    }
+                ]
             }
         },
         {
@@ -556,6 +751,20 @@
                 "skip_frames": 5,
                 "map": [ 0.0, 445, 15.0, 925 ]
             }
+        },
+        {
+            "rpi.hdr":
+            {
+                "MultiExposureUnmerged":
+                {
+                    "cadence": [ 1, 2 ],
+                    "channel_map":
+                    {
+                        "short": 1,
+                        "long": 2
+                    }
+                }
+            }
         }
     ]
-}
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx708_wide.json b/src/ipa/rpi/vc4/data/imx708_wide.json
index 6554362..6f45aaf 100644
--- a/src/ipa/rpi/vc4/data/imx708_wide.json
+++ b/src/ipa/rpi/vc4/data/imx708_wide.json
@@ -129,85 +129,280 @@
         {
             "rpi.agc":
             {
-                "metering_modes":
-                {
-                    "centre-weighted":
+                "channels": [
                     {
-                        "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 ]
-                    },
-                    "long":
-                    {
-                        "shutter": [ 1000, 30000, 60000, 90000, 120000 ],
-                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
-                    }
-                },
-                "constraint_modes":
-                {
-                    "normal": [
+                        "metering_modes":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.2,
-                                1000, 0.2
-                            ]
-                        }
-                    ],
-                    "highlight": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.2,
-                                1000, 0.2
+                            "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                    },
+                    {
+                        "base_ev": 0.125,
+                        "metering_modes":
                         {
-                            "bound": "UPPER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.8,
-                                1000, 0.8
+                            "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                        },
+                        "y_target":
+                        [
+                            0, 0.16,
+                            1000, 0.165,
+                            10000, 0.17
+                        ],
+                        "startup_frames": 5,
+                        "convergence_frames": 6,
+                        "speed": 0.15
+                    },
+                    {
+                        "base_ev": 1.5,
+                        "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                    }
+                ]
             }
         },
         {
@@ -468,6 +663,20 @@
                 "skip_frames": 5,
                 "map": [ 0.0, 420, 35.0, 920 ]
             }
+        },
+        {
+            "rpi.hdr":
+            {
+                "MultiExposureUnmerged":
+                {
+                    "cadence": [ 1, 2 ],
+                    "channel_map":
+                    {
+                        "short": 1,
+                        "long": 2
+                    }
+                }
+            }
         }
     ]
-}
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/imx708_wide_noir.json b/src/ipa/rpi/vc4/data/imx708_wide_noir.json
index 49442c0..b9a5227 100644
--- a/src/ipa/rpi/vc4/data/imx708_wide_noir.json
+++ b/src/ipa/rpi/vc4/data/imx708_wide_noir.json
@@ -129,85 +129,280 @@
         {
             "rpi.agc":
             {
-                "metering_modes":
-                {
-                    "centre-weighted":
+                "channels": [
                     {
-                        "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 ]
-                    },
-                    "long":
-                    {
-                        "shutter": [ 1000, 30000, 60000, 90000, 120000 ],
-                        "gain": [ 1.0, 2.0, 4.0, 6.0, 12.0 ]
-                    }
-                },
-                "constraint_modes":
-                {
-                    "normal": [
+                        "metering_modes":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.2,
-                                1000, 0.2
-                            ]
-                        }
-                    ],
-                    "highlight": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.2,
-                                1000, 0.2
+                            "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                    },
+                    {
+                        "base_ev": 0.125,
+                        "metering_modes":
                         {
-                            "bound": "UPPER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.8,
-                                1000, 0.8
+                            "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                        },
+                        "y_target":
+                        [
+                            0, 0.16,
+                            1000, 0.165,
+                            10000, 0.17
+                        ],
+                        "startup_frames": 5,
+                        "convergence_frames": 6,
+                        "speed": 0.15
+                    },
+                    {
+                        "base_ev": 1.5,
+                        "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 ]
+                            },
+                            "long":
+                            {
+                                "shutter": [ 1000, 30000, 60000, 90000, 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.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
+                    }
+                ]
             }
         },
         {
@@ -459,6 +654,20 @@
                 "skip_frames": 5,
                 "map": [ 0.0, 420, 35.0, 920 ]
             }
+        },
+        {
+            "rpi.hdr":
+            {
+                "MultiExposureUnmerged":
+                {
+                    "cadence": [ 1, 2 ],
+                    "channel_map":
+                    {
+                        "short": 1,
+                        "long": 2
+                    }
+                }
+            }
         }
     ]
-}
+}
\ No newline at end of file
diff --git a/src/ipa/rpi/vc4/data/meson.build b/src/ipa/rpi/vc4/data/meson.build
index bcf5658..afbf875 100644
--- a/src/ipa/rpi/vc4/data/meson.build
+++ b/src/ipa/rpi/vc4/data/meson.build
@@ -17,10 +17,12 @@
     'imx708_wide_noir.json',
     'ov5647.json',
     'ov5647_noir.json',
+    'ov64a40.json',
     'ov9281_mono.json',
     'se327m12.json',
     'uncalibrated.json',
 ])
 
 install_data(conf_files,
-             install_dir : ipa_data_dir / 'rpi' / 'vc4')
+             install_dir : ipa_data_dir / 'rpi' / 'vc4',
+             install_tag : 'runtime')
diff --git a/src/ipa/rpi/vc4/data/ov5647.json b/src/ipa/rpi/vc4/data/ov5647.json
index a1b42a1..40c6059 100644
--- a/src/ipa/rpi/vc4/data/ov5647.json
+++ b/src/ipa/rpi/vc4/data/ov5647.json
@@ -131,95 +131,308 @@
         {
             "rpi.agc":
             {
-                "metering_modes":
-                {
-                    "centre-weighted":
+                "channels": [
                     {
-                        "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": [
+                        "metering_modes":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.5,
-                                1000, 0.5
-                            ]
-                        }
-                    ],
-                    "highlight": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.5,
-                                1000, 0.5
+                            "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
+                    },
+                    {
+                        "base_ev": 1.25,
+                        "metering_modes":
                         {
-                            "bound": "UPPER",
-                            "q_lo": 0.98,
-                            "q_hi": 1.0,
-                            "y_target":
-                            [
-                                0, 0.8,
-                                1000, 0.8
-                            ]
-                        }
-                    ],
-                    "shadows": [
+                            "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":
                         {
-                            "bound": "LOWER",
-                            "q_lo": 0.0,
-                            "q_hi": 0.5,
-                            "y_target":
-                            [
-                                0, 0.17,
-                                1000, 0.17
+                            "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
+                        },
+                        "y_target":
+                        [
+                            0, 0.16,
+                            1000, 0.165,
+                            10000, 0.17
+                        ]
+                    },
+                    {
+                        "base_ev": 1.25,
+                        "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
+                        ]
+                    }
+                ]
             }
         },
         {
@@ -464,6 +677,20 @@
         },
         {
             "rpi.sharpen": { }
+        },
+        {
+            "rpi.hdr":
+            {
+                "MultiExposureUnmerged":
+                {
+                    "cadence": [ 1, 2 ],
+                    "channel_map":
+                    {
+                        "short": 1,
+                        "long": 2
+                    }
+                }
+            }
         }
     ]
-}
\ 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
index a6c6722..488b711 100644
--- a/src/ipa/rpi/vc4/data/ov5647_noir.json
+++ b/src/ipa/rpi/vc4/data/ov5647_noir.json
@@ -51,15 +51,24 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "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 ]
+                        "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 ]
+                        "weights":
+                        [
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
                     }
                 },
                 "exposure_modes":
diff --git a/src/ipa/rpi/vc4/data/ov64a40.json b/src/ipa/rpi/vc4/data/ov64a40.json
new file mode 100644
index 0000000..096f0b1
--- /dev/null
+++ b/src/ipa/rpi/vc4/data/ov64a40.json
@@ -0,0 +1,422 @@
+{
+    "version": 2.0,
+    "target": "bcm2835",
+    "algorithms": [
+        {
+            "rpi.black_level":
+            {
+                "black_level": 4096
+            }
+        },
+        {
+            "rpi.dpc": { }
+        },
+        {
+            "rpi.lux":
+            {
+                "reference_shutter_speed": 17861,
+                "reference_gain": 2.0,
+                "reference_aperture": 1.0,
+                "reference_lux": 1073,
+                "reference_Y": 9022
+            }
+        },
+        {
+            "rpi.noise":
+            {
+                "reference_constant": 0,
+                "reference_slope": 2.984
+            }
+        },
+        {
+            "rpi.geq":
+            {
+                "offset": 215,
+                "slope": 0.01121
+            }
+        },
+        {
+            "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
+                    },
+                },
+                "bayes": 1,
+                "ct_curve":
+                [
+                    2300.0, 1.0522, 0.4091,
+                    2700.0, 0.7884, 0.4327,
+                    3000.0, 0.7597, 0.4421,
+                    4000.0, 0.5972, 0.5404,
+                    4150.0, 0.5598, 0.5779,
+                    6500.0, 0.4388, 0.7582
+                ],
+                "sensitivity_r": 1.0,
+                "sensitivity_b": 1.0,
+                "transverse_pos": 0.0558,
+                "transverse_neg": 0.04278
+            }
+        },
+        {
+            "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": 100,
+                "luminance_strength": 0.8,
+                "calibrations_Cr": [
+                    {
+                        "ct": 6500,
+                        "table":
+                        [
+                            2.437, 2.415, 2.392, 2.378, 2.369, 2.353, 2.344, 2.336, 2.329, 2.325, 2.325, 2.325, 2.333, 2.344, 2.366, 2.381,
+                            2.434, 2.405, 2.386, 2.369, 2.361, 2.334, 2.314, 2.302, 2.295, 2.289, 2.289, 2.303, 2.327, 2.334, 2.356, 2.378,
+                            2.434, 2.405, 2.385, 2.363, 2.334, 2.314, 2.289, 2.277, 2.269, 2.262, 2.262, 2.283, 2.303, 2.328, 2.352, 2.375,
+                            2.434, 2.405, 2.385, 2.348, 2.315, 2.289, 2.277, 2.258, 2.251, 2.242, 2.249, 2.258, 2.283, 2.321, 2.352, 2.375,
+                            2.434, 2.413, 2.385, 2.343, 2.311, 2.282, 2.258, 2.251, 2.229, 2.233, 2.242, 2.251, 2.281, 2.321, 2.356, 2.375,
+                            2.437, 2.418, 2.388, 2.343, 2.311, 2.282, 2.251, 2.229, 2.221, 2.226, 2.233, 2.251, 2.281, 2.322, 2.361, 2.381,
+                            2.444, 2.422, 2.393, 2.351, 2.314, 2.284, 2.251, 2.227, 2.221, 2.227, 2.234, 2.256, 2.287, 2.326, 2.366, 2.389,
+                            2.445, 2.424, 2.395, 2.353, 2.316, 2.287, 2.266, 2.251, 2.228, 2.234, 2.251, 2.259, 2.289, 2.331, 2.371, 2.395,
+                            2.445, 2.424, 2.399, 2.364, 2.329, 2.308, 2.287, 2.266, 2.259, 2.254, 2.259, 2.283, 2.304, 2.343, 2.375, 2.395,
+                            2.445, 2.425, 2.407, 2.385, 2.364, 2.329, 2.308, 2.299, 2.291, 2.284, 2.284, 2.304, 2.335, 2.354, 2.381, 2.399,
+                            2.449, 2.427, 2.418, 2.407, 2.385, 2.364, 2.349, 2.338, 2.333, 2.326, 2.326, 2.335, 2.354, 2.374, 2.389, 2.408,
+                            2.458, 2.441, 2.427, 2.411, 2.403, 2.395, 2.391, 2.383, 2.375, 2.369, 2.369, 2.369, 2.369, 2.385, 2.408, 2.418
+                        ]
+                    }
+                ],
+                "calibrations_Cb": [
+                    {
+                        "ct": 6500,
+                        "table":
+                        [
+                            1.297, 1.297, 1.289, 1.289, 1.289, 1.291, 1.293, 1.294, 1.294, 1.294, 1.294, 1.296, 1.298, 1.304, 1.312, 1.313,
+                            1.297, 1.289, 1.286, 1.286, 1.287, 1.289, 1.292, 1.294, 1.294, 1.294, 1.294, 1.294, 1.296, 1.298, 1.306, 1.312,
+                            1.289, 1.286, 1.283, 1.283, 1.285, 1.287, 1.291, 1.294, 1.294, 1.292, 1.291, 1.289, 1.293, 1.294, 1.298, 1.304,
+                            1.283, 1.282, 1.279, 1.281, 1.282, 1.285, 1.287, 1.294, 1.294, 1.291, 1.289, 1.289, 1.289, 1.293, 1.294, 1.298,
+                            1.281, 1.279, 1.279, 1.279, 1.281, 1.283, 1.287, 1.292, 1.292, 1.291, 1.291, 1.289, 1.289, 1.291, 1.294, 1.297,
+                            1.279, 1.277, 1.277, 1.279, 1.281, 1.282, 1.286, 1.289, 1.291, 1.291, 1.291, 1.291, 1.289, 1.291, 1.293, 1.297,
+                            1.277, 1.275, 1.275, 1.278, 1.279, 1.281, 1.284, 1.287, 1.289, 1.291, 1.291, 1.291, 1.289, 1.289, 1.292, 1.297,
+                            1.277, 1.275, 1.274, 1.275, 1.277, 1.278, 1.279, 1.284, 1.285, 1.285, 1.286, 1.288, 1.289, 1.289, 1.292, 1.297,
+                            1.277, 1.272, 1.272, 1.274, 1.274, 1.277, 1.279, 1.282, 1.284, 1.284, 1.285, 1.286, 1.288, 1.289, 1.292, 1.297,
+                            1.277, 1.272, 1.272, 1.273, 1.274, 1.276, 1.279, 1.282, 1.284, 1.284, 1.286, 1.286, 1.288, 1.289, 1.293, 1.297,
+                            1.279, 1.272, 1.271, 1.272, 1.274, 1.276, 1.279, 1.283, 1.284, 1.284, 1.285, 1.286, 1.288, 1.291, 1.294, 1.299,
+                            1.281, 1.274, 1.271, 1.271, 1.273, 1.276, 1.278, 1.282, 1.284, 1.284, 1.285, 1.286, 1.286, 1.291, 1.295, 1.302
+                        ]
+                    }
+                ],
+                "luminance_lut":
+                [
+                    3.811, 3.611, 3.038, 2.632, 2.291, 2.044, 1.967, 1.957, 1.957, 1.957, 2.009, 2.222, 2.541, 2.926, 3.455, 3.652,
+                    3.611, 3.135, 2.636, 2.343, 2.044, 1.846, 1.703, 1.626, 1.626, 1.671, 1.796, 1.983, 2.266, 2.549, 3.007, 3.455,
+                    3.135, 2.781, 2.343, 2.044, 1.831, 1.554, 1.411, 1.337, 1.337, 1.379, 1.502, 1.749, 1.983, 2.266, 2.671, 3.007,
+                    2.903, 2.538, 2.149, 1.831, 1.554, 1.401, 1.208, 1.145, 1.145, 1.183, 1.339, 1.502, 1.749, 2.072, 2.446, 2.801,
+                    2.812, 2.389, 2.018, 1.684, 1.401, 1.208, 1.139, 1.028, 1.028, 1.109, 1.183, 1.339, 1.604, 1.939, 2.309, 2.723,
+                    2.799, 2.317, 1.948, 1.606, 1.327, 1.139, 1.028, 1.019, 1.001, 1.021, 1.109, 1.272, 1.531, 1.869, 2.246, 2.717,
+                    2.799, 2.317, 1.948, 1.606, 1.327, 1.139, 1.027, 1.006, 1.001, 1.007, 1.109, 1.272, 1.531, 1.869, 2.246, 2.717,
+                    2.799, 2.372, 1.997, 1.661, 1.378, 1.184, 1.118, 1.019, 1.012, 1.103, 1.158, 1.326, 1.589, 1.926, 2.302, 2.717,
+                    2.884, 2.507, 2.116, 1.795, 1.511, 1.361, 1.184, 1.118, 1.118, 1.158, 1.326, 1.461, 1.726, 2.056, 2.434, 2.799,
+                    3.083, 2.738, 2.303, 1.989, 1.783, 1.511, 1.361, 1.291, 1.291, 1.337, 1.461, 1.726, 1.942, 2.251, 2.657, 2.999,
+                    3.578, 3.083, 2.589, 2.303, 1.989, 1.783, 1.637, 1.563, 1.563, 1.613, 1.743, 1.942, 2.251, 2.537, 2.999, 3.492,
+                    3.764, 3.578, 2.999, 2.583, 2.237, 1.986, 1.913, 1.905, 1.905, 1.905, 1.962, 2.196, 2.525, 2.932, 3.492, 3.659
+                ],
+                "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.ccm":
+            {
+                "ccms": [
+                    {
+                        "ct": 2300,
+                        "ccm":
+                        [
+                            1.77644, -0.14825, -0.62819,
+                            -0.25816, 1.66348, -0.40532,
+                            -0.21633, -1.95132, 3.16765
+                        ]
+                    },
+                    {
+                        "ct": 2700,
+                        "ccm":
+                        [
+                            1.53605, 0.03047, -0.56652,
+                            -0.27159, 1.78525, -0.51366,
+                            -0.13581, -1.22128, 2.35709
+                        ]
+                    },
+                    {
+                        "ct": 3000,
+                        "ccm":
+                        [
+                            1.72928, -0.18819, -0.54108,
+                            -0.44398, 2.04756, -0.60358,
+                            -0.13203, -0.94711, 2.07913
+                        ]
+                    },
+                    {
+                        "ct": 4000,
+                        "ccm":
+                        [
+                            1.69895, -0.23055, -0.46841,
+                            -0.33934, 1.80391, -0.46456,
+                            -0.13902, -0.75385, 1.89287
+                        ]
+                    },
+                    {
+                        "ct": 4150,
+                        "ccm":
+                        [
+                            2.08494, -0.68698, -0.39796,
+                            -0.37928, 1.78795, -0.40867,
+                            -0.11537, -0.74686, 1.86223
+                        ]
+                    },
+                    {
+                        "ct": 6500,
+                        "ccm":
+                        [
+                            1.69813, -0.27304, -0.42509,
+                            -0.23364, 1.87586, -0.64221,
+                            -0.07587, -0.62348, 1.69935
+                        ]
+                    }
+                ]
+            }
+        },
+        {
+            "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": 0,
+                        "dropout_frames": 0,
+                        "step_frames": 4
+                    }
+                },
+                "conf_epsilon": 8,
+                "conf_thresh": 16,
+                "conf_clip": 512,
+                "skip_frames": 5,
+                "map": [ 0.0, 0, 15.0, 1023 ]
+            }
+        }
+    ]
+}
\ 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
index 2b7292e..a9d05a0 100644
--- a/src/ipa/rpi/vc4/data/ov9281_mono.json
+++ b/src/ipa/rpi/vc4/data/ov9281_mono.json
@@ -35,7 +35,10 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 4, 4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "weights":
+                        [
+                            4, 4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0
+                        ]
                     }
                 },
                 "exposure_modes":
diff --git a/src/ipa/rpi/vc4/data/se327m12.json b/src/ipa/rpi/vc4/data/se327m12.json
index 8552ed9..948169d 100644
--- a/src/ipa/rpi/vc4/data/se327m12.json
+++ b/src/ipa/rpi/vc4/data/se327m12.json
@@ -133,15 +133,24 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 3, 3, 3, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "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 ]
+                        "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 ]
+                        "weights":
+                        [
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
                     }
                 },
                 "exposure_modes":
diff --git a/src/ipa/rpi/vc4/data/uncalibrated.json b/src/ipa/rpi/vc4/data/uncalibrated.json
index 7654def..cdc56b3 100644
--- a/src/ipa/rpi/vc4/data/uncalibrated.json
+++ b/src/ipa/rpi/vc4/data/uncalibrated.json
@@ -22,7 +22,10 @@
                 {
                     "centre-weighted":
                     {
-                        "weights": [ 4, 4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0 ]
+                        "weights":
+                        [
+                            4, 4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, 0
+                        ]
                     }
                 },
                 "exposure_modes":
diff --git a/src/ipa/rpi/vc4/meson.build b/src/ipa/rpi/vc4/meson.build
index 590e919..63fc592 100644
--- a/src/ipa/rpi/vc4/meson.build
+++ b/src/ipa/rpi/vc4/meson.build
@@ -15,7 +15,6 @@
 
 vc4_ipa_includes = [
     ipa_includes,
-    libipa_includes,
 ]
 
 vc4_ipa_sources = files([
@@ -28,8 +27,7 @@
                     [vc4_ipa_sources, libcamera_generated_ipa_headers],
                     name_prefix : '',
                     include_directories : vc4_ipa_includes,
-                    dependencies : vc4_ipa_deps,
-                    link_with : libipa,
+                    dependencies : [vc4_ipa_deps, libipa_dep],
                     link_whole : vc4_ipa_libs,
                     install : true,
                     install_dir : ipa_install_dir)
diff --git a/src/ipa/rpi/vc4/vc4.cpp b/src/ipa/rpi/vc4/vc4.cpp
index 1de0d3c..ba43e47 100644
--- a/src/ipa/rpi/vc4/vc4.cpp
+++ b/src/ipa/rpi/vc4/vc4.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2021, Raspberry Pi Ltd
  *
- * rpi.cpp - Raspberry Pi VC4/BCM2835 ISP IPA.
+ * Raspberry Pi VC4/BCM2835 ISP IPA.
  */
 
 #include <string.h>
@@ -11,6 +11,8 @@
 #include <linux/bcm2835-isp.h>
 
 #include <libcamera/base/log.h>
+#include <libcamera/base/span.h>
+#include <libcamera/control_ids.h>
 #include <libcamera/ipa/ipa_module_info.h>
 
 #include "common/ipa_base.h"
@@ -51,6 +53,7 @@
 
 private:
 	int32_t platformInit(const InitParams &params, InitResult *result) override;
+	int32_t platformStart(const ControlList &controls, StartResult *result) override;
 	int32_t platformConfigure(const ConfigParams &params, ConfigResult *result) override;
 
 	void platformPrepareIsp(const PrepareParams &params, RPiController::Metadata &rpiMetadata) override;
@@ -94,6 +97,12 @@
 	return 0;
 }
 
+int32_t IpaVc4::platformStart([[maybe_unused]] const ControlList &controls,
+			      [[maybe_unused]] StartResult *result)
+{
+	return 0;
+}
+
 int32_t IpaVc4::platformConfigure(const ConfigParams &params, [[maybe_unused]] ConfigResult *result)
 {
 	ispCtrls_ = params.ispControls;
@@ -237,12 +246,48 @@
 						  stats->focus_stats[i].contrast_val_num[1][1],
 						  stats->focus_stats[i].contrast_val_num[1][0] });
 
+	if (statsMetadataOutput_) {
+		Span<const uint8_t> statsSpan(reinterpret_cast<const uint8_t *>(stats),
+					      sizeof(bcm2835_isp_stats));
+		libcameraMetadata_.set(controls::rpi::Bcm2835StatsOutput, statsSpan);
+	}
+
 	return statistics;
 }
 
-void IpaVc4::handleControls([[maybe_unused]] const ControlList &controls)
+void IpaVc4::handleControls(const ControlList &controls)
 {
-	/* No controls require any special updates to the hardware configuration. */
+	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 },
+	};
+
+	for (auto const &ctrl : controls) {
+		switch (ctrl.first) {
+		case controls::draft::NOISE_REDUCTION_MODE: {
+			RPiController::DenoiseAlgorithm *sdn = dynamic_cast<RPiController::DenoiseAlgorithm *>(
+				controller_.getAlgorithm("SDN"));
+			/* Some platforms may have a combined "denoise" algorithm instead. */
+			if (!sdn)
+				sdn = dynamic_cast<RPiController::DenoiseAlgorithm *>(
+					controller_.getAlgorithm("denoise"));
+			if (!sdn) {
+				LOG(IPARPI, Warning)
+					<< "Could not set NOISE_REDUCTION_MODE - no SDN algorithm";
+				return;
+			}
+
+			int32_t idx = ctrl.second.get<int32_t>();
+			auto mode = DenoiseModeTable.find(idx);
+			if (mode != DenoiseModeTable.end())
+				sdn->setMode(mode->second);
+			break;
+		}
+		}
+	}
 }
 
 bool IpaVc4::validateIspControls()
@@ -538,7 +583,7 @@
 const struct IPAModuleInfo ipaModuleInfo = {
 	IPA_MODULE_API_VERSION,
 	1,
-	"PipelineHandlerVc4",
+	"rpi/vc4",
 	"rpi/vc4",
 };
 
diff --git a/src/ipa/simple/black_level.cpp b/src/ipa/simple/black_level.cpp
new file mode 100644
index 0000000..cc490eb
--- /dev/null
+++ b/src/ipa/simple/black_level.cpp
@@ -0,0 +1,88 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * black level handling
+ */
+
+#include "black_level.h"
+
+#include <numeric>
+
+#include <libcamera/base/log.h>
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(IPASoftBL)
+
+/**
+ * \class BlackLevel
+ * \brief Object providing black point level for software ISP
+ *
+ * Black level can be provided in hardware tuning files or, if no tuning file is
+ * available for the given hardware, guessed automatically, with less accuracy.
+ * As tuning files are not yet implemented for software ISP, BlackLevel
+ * currently provides only guessed black levels.
+ *
+ * This class serves for tracking black level as a property of the underlying
+ * hardware, not as means of enhancing a particular scene or image.
+ *
+ * The class is supposed to be instantiated for the given camera stream.
+ * The black level can be retrieved using BlackLevel::get() method. It is
+ * initially 0 and may change when updated using BlackLevel::update() method.
+ */
+
+BlackLevel::BlackLevel()
+	: blackLevel_(255), blackLevelSet_(false)
+{
+}
+
+/**
+ * \brief Return the current black level
+ *
+ * \return The black level, in the range from 0 (minimum) to 255 (maximum).
+ * If the black level couldn't be determined yet, return 0.
+ */
+uint8_t BlackLevel::get() const
+{
+	return blackLevelSet_ ? blackLevel_ : 0;
+}
+
+/**
+ * \brief Update black level from the provided histogram
+ * \param[in] yHistogram The histogram to be used for updating black level
+ *
+ * The black level is property of the given hardware, not image. It is updated
+ * only if it has not been yet set or if it is lower than the lowest value seen
+ * so far.
+ */
+void BlackLevel::update(SwIspStats::Histogram &yHistogram)
+{
+	/*
+	 * The constant is selected to be "good enough", not overly conservative or
+	 * aggressive. There is no magic about the given value.
+	 */
+	constexpr float ignoredPercentage_ = 0.02;
+	const unsigned int total =
+		std::accumulate(begin(yHistogram), end(yHistogram), 0);
+	const unsigned int pixelThreshold = ignoredPercentage_ * total;
+	const unsigned int histogramRatio = 256 / SwIspStats::kYHistogramSize;
+	const unsigned int currentBlackIdx = blackLevel_ / histogramRatio;
+
+	for (unsigned int i = 0, seen = 0;
+	     i < currentBlackIdx && i < SwIspStats::kYHistogramSize;
+	     i++) {
+		seen += yHistogram[i];
+		if (seen >= pixelThreshold) {
+			blackLevel_ = i * histogramRatio;
+			blackLevelSet_ = true;
+			LOG(IPASoftBL, Debug)
+				<< "Auto-set black level: "
+				<< i << "/" << SwIspStats::kYHistogramSize
+				<< " (" << 100 * (seen - yHistogram[i]) / total << "% below, "
+				<< 100 * seen / total << "% at or below)";
+			break;
+		}
+	};
+}
+} /* namespace libcamera */
diff --git a/src/ipa/simple/black_level.h b/src/ipa/simple/black_level.h
new file mode 100644
index 0000000..5e032f9
--- /dev/null
+++ b/src/ipa/simple/black_level.h
@@ -0,0 +1,29 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ *
+ * black level handling
+ */
+
+#pragma once
+
+#include <array>
+#include <stdint.h>
+
+#include "libcamera/internal/software_isp/swisp_stats.h"
+
+namespace libcamera {
+
+class BlackLevel
+{
+public:
+	BlackLevel();
+	uint8_t get() const;
+	void update(SwIspStats::Histogram &yHistogram);
+
+private:
+	uint8_t blackLevel_;
+	bool blackLevelSet_;
+};
+
+} /* namespace libcamera */
diff --git a/src/ipa/simple/data/meson.build b/src/ipa/simple/data/meson.build
new file mode 100644
index 0000000..92795ee
--- /dev/null
+++ b/src/ipa/simple/data/meson.build
@@ -0,0 +1,10 @@
+# SPDX-License-Identifier: CC0-1.0
+
+conf_files = files([
+    'uncalibrated.yaml',
+])
+
+# The install_dir must match the name from the IPAModuleInfo
+install_data(conf_files,
+             install_dir : ipa_data_dir / 'simple',
+             install_tag : 'runtime')
diff --git a/src/ipa/simple/data/uncalibrated.yaml b/src/ipa/simple/data/uncalibrated.yaml
new file mode 100644
index 0000000..ff981a1
--- /dev/null
+++ b/src/ipa/simple/data/uncalibrated.yaml
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: CC0-1.0
+%YAML 1.1
+---
+version: 1
+...
diff --git a/src/ipa/simple/meson.build b/src/ipa/simple/meson.build
new file mode 100644
index 0000000..33d1c96
--- /dev/null
+++ b/src/ipa/simple/meson.build
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: CC0-1.0
+
+ipa_name = 'ipa_soft_simple'
+
+soft_simple_sources = files([
+    'soft_simple.cpp',
+    'black_level.cpp',
+])
+
+mod = shared_module(ipa_name,
+                    [soft_simple_sources, libcamera_generated_ipa_headers],
+                    name_prefix : '',
+                    include_directories : [ipa_includes],
+                    dependencies : [libcamera_private, libipa_dep],
+                    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/simple/soft_simple.cpp b/src/ipa/simple/soft_simple.cpp
new file mode 100644
index 0000000..b7746ce
--- /dev/null
+++ b/src/ipa/simple/soft_simple.cpp
@@ -0,0 +1,444 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ *
+ * Simple Software Image Processing Algorithm module
+ */
+
+#include <cmath>
+#include <numeric>
+#include <stdint.h>
+#include <sys/mman.h>
+
+#include <linux/v4l2-controls.h>
+
+#include <libcamera/base/file.h>
+#include <libcamera/base/log.h>
+#include <libcamera/base/shared_fd.h>
+
+#include <libcamera/control_ids.h>
+#include <libcamera/controls.h>
+
+#include <libcamera/ipa/ipa_interface.h>
+#include <libcamera/ipa/ipa_module_info.h>
+#include <libcamera/ipa/soft_ipa_interface.h>
+
+#include "libcamera/internal/software_isp/debayer_params.h"
+#include "libcamera/internal/software_isp/swisp_stats.h"
+#include "libcamera/internal/yaml_parser.h"
+
+#include "libipa/camera_sensor_helper.h"
+
+#include "black_level.h"
+
+namespace libcamera {
+LOG_DEFINE_CATEGORY(IPASoft)
+
+namespace ipa::soft {
+
+/*
+ * The number of bins to use for the optimal exposure calculations.
+ */
+static constexpr unsigned int kExposureBinsCount = 5;
+
+/*
+ * The exposure is optimal when the mean sample value of the histogram is
+ * in the middle of the range.
+ */
+static constexpr float kExposureOptimal = kExposureBinsCount / 2.0;
+
+/*
+ * The below value implements the hysteresis for the exposure adjustment.
+ * It is small enough to have the exposure close to the optimal, and is big
+ * enough to prevent the exposure from wobbling around the optimal value.
+ */
+static constexpr float kExposureSatisfactory = 0.2;
+
+class IPASoftSimple : public ipa::soft::IPASoftInterface
+{
+public:
+	IPASoftSimple()
+		: params_(nullptr), stats_(nullptr), blackLevel_(BlackLevel()),
+		  ignoreUpdates_(0)
+	{
+	}
+
+	~IPASoftSimple();
+
+	int init(const IPASettings &settings,
+		 const SharedFD &fdStats,
+		 const SharedFD &fdParams,
+		 const ControlInfoMap &sensorInfoMap) override;
+	int configure(const ControlInfoMap &sensorInfoMap) override;
+
+	int start() override;
+	void stop() override;
+
+	void processStats(const ControlList &sensorControls) override;
+
+private:
+	void updateExposure(double exposureMSV);
+
+	DebayerParams *params_;
+	SwIspStats *stats_;
+	std::unique_ptr<CameraSensorHelper> camHelper_;
+	ControlInfoMap sensorInfoMap_;
+	BlackLevel blackLevel_;
+
+	static constexpr unsigned int kGammaLookupSize = 1024;
+	std::array<uint8_t, kGammaLookupSize> gammaTable_;
+	int lastBlackLevel_ = -1;
+
+	int32_t exposureMin_, exposureMax_;
+	int32_t exposure_;
+	double againMin_, againMax_, againMinStep_;
+	double again_;
+	unsigned int ignoreUpdates_;
+};
+
+IPASoftSimple::~IPASoftSimple()
+{
+	if (stats_)
+		munmap(stats_, sizeof(SwIspStats));
+	if (params_)
+		munmap(params_, sizeof(DebayerParams));
+}
+
+int IPASoftSimple::init(const IPASettings &settings,
+			const SharedFD &fdStats,
+			const SharedFD &fdParams,
+			const ControlInfoMap &sensorInfoMap)
+{
+	camHelper_ = CameraSensorHelperFactoryBase::create(settings.sensorModel);
+	if (!camHelper_) {
+		LOG(IPASoft, Warning)
+			<< "Failed to create camera sensor helper for "
+			<< settings.sensorModel;
+	}
+
+	/* Load the tuning data file */
+	File file(settings.configurationFile);
+	if (!file.open(File::OpenModeFlag::ReadOnly)) {
+		int ret = file.error();
+		LOG(IPASoft, Error)
+			<< "Failed to open configuration file "
+			<< settings.configurationFile << ": " << strerror(-ret);
+		return ret;
+	}
+
+	std::unique_ptr<libcamera::YamlObject> data = YamlParser::parse(file);
+	if (!data)
+		return -EINVAL;
+
+	/* \todo Use the IPA configuration file for real. */
+	unsigned int version = (*data)["version"].get<uint32_t>(0);
+	LOG(IPASoft, Debug) << "Tuning file version " << version;
+
+	params_ = nullptr;
+	stats_ = nullptr;
+
+	if (!fdStats.isValid()) {
+		LOG(IPASoft, Error) << "Invalid Statistics handle";
+		return -ENODEV;
+	}
+
+	if (!fdParams.isValid()) {
+		LOG(IPASoft, Error) << "Invalid Parameters handle";
+		return -ENODEV;
+	}
+
+	{
+		void *mem = mmap(nullptr, sizeof(DebayerParams), PROT_WRITE,
+				 MAP_SHARED, fdParams.get(), 0);
+		if (mem == MAP_FAILED) {
+			LOG(IPASoft, Error) << "Unable to map Parameters";
+			return -errno;
+		}
+
+		params_ = static_cast<DebayerParams *>(mem);
+	}
+
+	{
+		void *mem = mmap(nullptr, sizeof(SwIspStats), PROT_READ,
+				 MAP_SHARED, fdStats.get(), 0);
+		if (mem == MAP_FAILED) {
+			LOG(IPASoft, Error) << "Unable to map Statistics";
+			return -errno;
+		}
+
+		stats_ = static_cast<SwIspStats *>(mem);
+	}
+
+	/*
+	 * Check if the sensor driver supports the controls required by the
+	 * Soft IPA.
+	 * Don't save the min and max control values yet, as e.g. the limits
+	 * for V4L2_CID_EXPOSURE depend on the configured sensor resolution.
+	 */
+	if (sensorInfoMap.find(V4L2_CID_EXPOSURE) == sensorInfoMap.end()) {
+		LOG(IPASoft, Error) << "Don't have exposure control";
+		return -EINVAL;
+	}
+
+	if (sensorInfoMap.find(V4L2_CID_ANALOGUE_GAIN) == sensorInfoMap.end()) {
+		LOG(IPASoft, Error) << "Don't have gain control";
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+int IPASoftSimple::configure(const ControlInfoMap &sensorInfoMap)
+{
+	sensorInfoMap_ = sensorInfoMap;
+
+	const ControlInfo &exposureInfo = sensorInfoMap_.find(V4L2_CID_EXPOSURE)->second;
+	const ControlInfo &gainInfo = sensorInfoMap_.find(V4L2_CID_ANALOGUE_GAIN)->second;
+
+	exposureMin_ = exposureInfo.min().get<int32_t>();
+	exposureMax_ = exposureInfo.max().get<int32_t>();
+	if (!exposureMin_) {
+		LOG(IPASoft, Warning) << "Minimum exposure is zero, that can't be linear";
+		exposureMin_ = 1;
+	}
+
+	int32_t againMin = gainInfo.min().get<int32_t>();
+	int32_t againMax = gainInfo.max().get<int32_t>();
+
+	if (camHelper_) {
+		againMin_ = camHelper_->gain(againMin);
+		againMax_ = camHelper_->gain(againMax);
+		againMinStep_ = (againMax_ - againMin_) / 100.0;
+	} else {
+		/*
+		 * The camera sensor gain (g) is usually not equal to the value written
+		 * into the gain register (x). But the way how the AGC algorithm changes
+		 * the gain value to make the total exposure closer to the optimum
+		 * assumes that g(x) is not too far from linear function. If the minimal
+		 * gain is 0, the g(x) is likely to be far from the linear, like
+		 * g(x) = a / (b * x + c). To avoid unexpected changes to the gain by
+		 * the AGC algorithm (abrupt near one edge, and very small near the
+		 * other) we limit the range of the gain values used.
+		 */
+		againMax_ = againMax;
+		if (!againMin) {
+			LOG(IPASoft, Warning)
+				<< "Minimum gain is zero, that can't be linear";
+			againMin_ = std::min(100, againMin / 2 + againMax / 2);
+		}
+		againMinStep_ = 1.0;
+	}
+
+	LOG(IPASoft, Info) << "Exposure " << exposureMin_ << "-" << exposureMax_
+			   << ", gain " << againMin_ << "-" << againMax_
+			   << " (" << againMinStep_ << ")";
+
+	return 0;
+}
+
+int IPASoftSimple::start()
+{
+	return 0;
+}
+
+void IPASoftSimple::stop()
+{
+}
+
+void IPASoftSimple::processStats(const ControlList &sensorControls)
+{
+	SwIspStats::Histogram histogram = stats_->yHistogram;
+	if (ignoreUpdates_ > 0)
+		blackLevel_.update(histogram);
+	const uint8_t blackLevel = blackLevel_.get();
+
+	/*
+	 * Black level must be subtracted to get the correct AWB ratios, they
+	 * would be off if they were computed from the whole brightness range
+	 * rather than from the sensor range.
+	 */
+	const uint64_t nPixels = std::accumulate(
+		histogram.begin(), histogram.end(), 0);
+	const uint64_t offset = blackLevel * nPixels;
+	const uint64_t sumR = stats_->sumR_ - offset / 4;
+	const uint64_t sumG = stats_->sumG_ - offset / 2;
+	const uint64_t sumB = stats_->sumB_ - offset / 4;
+
+	/*
+	 * Calculate red and blue gains for AWB.
+	 * Clamp max gain at 4.0, this also avoids 0 division.
+	 * Gain: 128 = 0.5, 256 = 1.0, 512 = 2.0, etc.
+	 */
+	const unsigned int gainR = sumR <= sumG / 4 ? 1024 : 256 * sumG / sumR;
+	const unsigned int gainB = sumB <= sumG / 4 ? 1024 : 256 * sumG / sumB;
+	/* Green gain and gamma values are fixed */
+	constexpr unsigned int gainG = 256;
+
+	/* Update the gamma table if needed */
+	if (blackLevel != lastBlackLevel_) {
+		constexpr float gamma = 0.5;
+		const unsigned int blackIndex = blackLevel * kGammaLookupSize / 256;
+		std::fill(gammaTable_.begin(), gammaTable_.begin() + blackIndex, 0);
+		const float divisor = kGammaLookupSize - blackIndex - 1.0;
+		for (unsigned int i = blackIndex; i < kGammaLookupSize; i++)
+			gammaTable_[i] = UINT8_MAX *
+					 std::pow((i - blackIndex) / divisor, gamma);
+
+		lastBlackLevel_ = blackLevel;
+	}
+
+	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
+		constexpr unsigned int div =
+			DebayerParams::kRGBLookupSize * 256 / kGammaLookupSize;
+		unsigned int idx;
+
+		/* Apply gamma after gain! */
+		idx = std::min({ i * gainR / div, (kGammaLookupSize - 1) });
+		params_->red[i] = gammaTable_[idx];
+
+		idx = std::min({ i * gainG / div, (kGammaLookupSize - 1) });
+		params_->green[i] = gammaTable_[idx];
+
+		idx = std::min({ i * gainB / div, (kGammaLookupSize - 1) });
+		params_->blue[i] = gammaTable_[idx];
+	}
+
+	setIspParams.emit();
+
+	/* \todo Switch to the libipa/algorithm.h API someday. */
+
+	/*
+	 * AE / AGC, use 2 frames delay to make sure that the exposure and
+	 * the gain set have applied to the camera sensor.
+	 * \todo This could be handled better with DelayedControls.
+	 */
+	if (ignoreUpdates_ > 0) {
+		--ignoreUpdates_;
+		return;
+	}
+
+	/*
+	 * Calculate Mean Sample Value (MSV) according to formula from:
+	 * https://www.araa.asn.au/acra/acra2007/papers/paper84final.pdf
+	 */
+	const unsigned int blackLevelHistIdx =
+		blackLevel / (256 / SwIspStats::kYHistogramSize);
+	const unsigned int histogramSize =
+		SwIspStats::kYHistogramSize - blackLevelHistIdx;
+	const unsigned int yHistValsPerBin = histogramSize / kExposureBinsCount;
+	const unsigned int yHistValsPerBinMod =
+		histogramSize / (histogramSize % kExposureBinsCount + 1);
+	int exposureBins[kExposureBinsCount] = {};
+	unsigned int denom = 0;
+	unsigned int num = 0;
+
+	for (unsigned int i = 0; i < histogramSize; i++) {
+		unsigned int idx = (i - (i / yHistValsPerBinMod)) / yHistValsPerBin;
+		exposureBins[idx] += stats_->yHistogram[blackLevelHistIdx + i];
+	}
+
+	for (unsigned int i = 0; i < kExposureBinsCount; i++) {
+		LOG(IPASoft, Debug) << i << ": " << exposureBins[i];
+		denom += exposureBins[i];
+		num += exposureBins[i] * (i + 1);
+	}
+
+	float exposureMSV = static_cast<float>(num) / denom;
+
+	/* Sanity check */
+	if (!sensorControls.contains(V4L2_CID_EXPOSURE) ||
+	    !sensorControls.contains(V4L2_CID_ANALOGUE_GAIN)) {
+		LOG(IPASoft, Error) << "Control(s) missing";
+		return;
+	}
+
+	exposure_ = sensorControls.get(V4L2_CID_EXPOSURE).get<int32_t>();
+	int32_t again = sensorControls.get(V4L2_CID_ANALOGUE_GAIN).get<int32_t>();
+	again_ = camHelper_ ? camHelper_->gain(again) : again;
+
+	updateExposure(exposureMSV);
+
+	ControlList ctrls(sensorInfoMap_);
+
+	ctrls.set(V4L2_CID_EXPOSURE, exposure_);
+	ctrls.set(V4L2_CID_ANALOGUE_GAIN,
+		  static_cast<int32_t>(camHelper_ ? camHelper_->gainCode(again_) : again_));
+
+	ignoreUpdates_ = 2;
+
+	setSensorControls.emit(ctrls);
+
+	LOG(IPASoft, Debug) << "exposureMSV " << exposureMSV
+			    << " exp " << exposure_ << " again " << again_
+			    << " gain R/B " << gainR << "/" << gainB
+			    << " black level " << static_cast<unsigned int>(blackLevel);
+}
+
+void IPASoftSimple::updateExposure(double exposureMSV)
+{
+	/*
+	 * kExpDenominator of 10 gives ~10% increment/decrement;
+	 * kExpDenominator of 5 - about ~20%
+	 */
+	static constexpr uint8_t kExpDenominator = 10;
+	static constexpr uint8_t kExpNumeratorUp = kExpDenominator + 1;
+	static constexpr uint8_t kExpNumeratorDown = kExpDenominator - 1;
+
+	double next;
+
+	if (exposureMSV < kExposureOptimal - kExposureSatisfactory) {
+		next = exposure_ * kExpNumeratorUp / kExpDenominator;
+		if (next - exposure_ < 1)
+			exposure_ += 1;
+		else
+			exposure_ = next;
+		if (exposure_ >= exposureMax_) {
+			next = again_ * kExpNumeratorUp / kExpDenominator;
+			if (next - again_ < againMinStep_)
+				again_ += againMinStep_;
+			else
+				again_ = next;
+		}
+	}
+
+	if (exposureMSV > kExposureOptimal + kExposureSatisfactory) {
+		if (exposure_ == exposureMax_ && again_ > againMin_) {
+			next = again_ * kExpNumeratorDown / kExpDenominator;
+			if (again_ - next < againMinStep_)
+				again_ -= againMinStep_;
+			else
+				again_ = next;
+		} else {
+			next = exposure_ * kExpNumeratorDown / kExpDenominator;
+			if (exposure_ - next < 1)
+				exposure_ -= 1;
+			else
+				exposure_ = next;
+		}
+	}
+
+	exposure_ = std::clamp(exposure_, exposureMin_, exposureMax_);
+	again_ = std::clamp(again_, againMin_, againMax_);
+}
+
+} /* namespace ipa::soft */
+
+/*
+ * External IPA module interface
+ */
+extern "C" {
+const struct IPAModuleInfo ipaModuleInfo = {
+	IPA_MODULE_API_VERSION,
+	0,
+	"simple",
+	"simple",
+};
+
+IPAInterface *ipaCreate()
+{
+	return new ipa::soft::IPASoftSimple();
+}
+
+} /* extern "C" */
+
+} /* namespace libcamera */
diff --git a/src/ipa/vimc/data/meson.build b/src/ipa/vimc/data/meson.build
index 42ec651..628d6a2 100644
--- a/src/ipa/vimc/data/meson.build
+++ b/src/ipa/vimc/data/meson.build
@@ -5,4 +5,5 @@
 ])
 
 install_data(conf_files,
-             install_dir : ipa_data_dir / 'vimc')
+             install_dir : ipa_data_dir / 'vimc',
+             install_tag : 'runtime')
diff --git a/src/ipa/vimc/meson.build b/src/ipa/vimc/meson.build
index 264a2d9..d0b63ed 100644
--- a/src/ipa/vimc/meson.build
+++ b/src/ipa/vimc/meson.build
@@ -5,9 +5,8 @@
 mod = shared_module(ipa_name,
                     ['vimc.cpp', libcamera_generated_ipa_headers],
                     name_prefix : '',
-                    include_directories : [ipa_includes, libipa_includes],
-                    dependencies : libcamera_private,
-                    link_with : libipa,
+                    include_directories : [ipa_includes],
+                    dependencies : [libcamera_private, libipa_dep],
                     install : true,
                     install_dir : ipa_install_dir)
 
diff --git a/src/ipa/vimc/vimc.cpp b/src/ipa/vimc/vimc.cpp
index 2c25577..ebd63fa 100644
--- a/src/ipa/vimc/vimc.cpp
+++ b/src/ipa/vimc/vimc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * vimc.cpp - Vimc Image Processing Algorithm module
+ * Vimc Image Processing Algorithm module
  */
 #include <libcamera/ipa/vimc_ipa_interface.h>
 
@@ -200,7 +200,7 @@
 const struct IPAModuleInfo ipaModuleInfo = {
 	IPA_MODULE_API_VERSION,
 	0,
-	"PipelineHandlerVimc",
+	"vimc",
 	"vimc",
 };
 
diff --git a/src/libcamera/base/backtrace.cpp b/src/libcamera/base/backtrace.cpp
index be30589..0b04629 100644
--- a/src/libcamera/base/backtrace.cpp
+++ b/src/libcamera/base/backtrace.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Ideas on Board Oy
  *
- * backtrace.h - Call stack backtraces
+ * Call stack backtraces
  */
 
 #include <libcamera/base/backtrace.h>
diff --git a/src/libcamera/base/bound_method.cpp b/src/libcamera/base/bound_method.cpp
index 3ecec51..322029a 100644
--- a/src/libcamera/base/bound_method.cpp
+++ b/src/libcamera/base/bound_method.cpp
@@ -2,11 +2,12 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * bound_method.cpp - Method bind and invocation
+ * Method bind and invocation
  */
 
 #include <libcamera/base/bound_method.h>
 #include <libcamera/base/message.h>
+#include <libcamera/base/object.h>
 #include <libcamera/base/semaphore.h>
 #include <libcamera/base/thread.h>
 
diff --git a/src/libcamera/base/class.cpp b/src/libcamera/base/class.cpp
index 9c2d9f2..6199839 100644
--- a/src/libcamera/base/class.cpp
+++ b/src/libcamera/base/class.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * class.cpp - Utilities and helpers for classes
+ * Utilities and helpers for classes
  */
 
 #include <libcamera/base/class.h>
diff --git a/src/libcamera/base/event_dispatcher.cpp b/src/libcamera/base/event_dispatcher.cpp
index 4be89e8..5f4a5cb 100644
--- a/src/libcamera/base/event_dispatcher.cpp
+++ b/src/libcamera/base/event_dispatcher.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event_dispatcher.cpp - Event dispatcher
+ * Event dispatcher
  */
 
 #include <libcamera/base/event_dispatcher.h>
diff --git a/src/libcamera/base/event_dispatcher_poll.cpp b/src/libcamera/base/event_dispatcher_poll.cpp
index 7238a31..b737ca7 100644
--- a/src/libcamera/base/event_dispatcher_poll.cpp
+++ b/src/libcamera/base/event_dispatcher_poll.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event_dispatcher_poll.cpp - Poll-based event dispatcher
+ * Poll-based event dispatcher
  */
 
 #include <libcamera/base/event_dispatcher_poll.h>
diff --git a/src/libcamera/base/event_notifier.cpp b/src/libcamera/base/event_notifier.cpp
index fd93c08..495c281 100644
--- a/src/libcamera/base/event_notifier.cpp
+++ b/src/libcamera/base/event_notifier.cpp
@@ -2,12 +2,13 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event_notifier.cpp - File descriptor event notifier
+ * File descriptor event notifier
  */
 
 #include <libcamera/base/event_notifier.h>
 
 #include <libcamera/base/event_dispatcher.h>
+#include <libcamera/base/log.h>
 #include <libcamera/base/message.h>
 #include <libcamera/base/thread.h>
 
@@ -20,6 +21,8 @@
 
 namespace libcamera {
 
+LOG_DECLARE_CATEGORY(Event)
+
 /**
  * \class EventNotifier
  * \brief Notify of activity on a file descriptor
@@ -104,6 +107,9 @@
  */
 void EventNotifier::setEnabled(bool enable)
 {
+	if (!assertThreadBound("EventNotifier can't be enabled from another thread"))
+		return;
+
 	if (enabled_ == enable)
 		return;
 
diff --git a/src/libcamera/base/file.cpp b/src/libcamera/base/file.cpp
index d1ab1aa..2b83a51 100644
--- a/src/libcamera/base/file.cpp
+++ b/src/libcamera/base/file.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * file.cpp - File I/O operations
+ * File I/O operations
  */
 
 #include <libcamera/base/file.h>
diff --git a/src/libcamera/base/flags.cpp b/src/libcamera/base/flags.cpp
index 3e4320a..9981f2e 100644
--- a/src/libcamera/base/flags.cpp
+++ b/src/libcamera/base/flags.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * flags.cpp - Type-safe enum-based bitfields
+ * Type-safe enum-based bitfields
  */
 
 #include <libcamera/base/flags.h>
diff --git a/src/libcamera/base/log.cpp b/src/libcamera/base/log.cpp
index c8045ef..3a656b8 100644
--- a/src/libcamera/base/log.cpp
+++ b/src/libcamera/base/log.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * log.cpp - Logging infrastructure
+ * Logging infrastructure
  */
 
 #include <libcamera/base/log.h>
diff --git a/src/libcamera/base/message.cpp b/src/libcamera/base/message.cpp
index 2da2a7e..098faac 100644
--- a/src/libcamera/base/message.cpp
+++ b/src/libcamera/base/message.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * message.cpp - Message queue support
+ * Message queue support
  */
 
 #include <libcamera/base/message.h>
diff --git a/src/libcamera/base/mutex.cpp b/src/libcamera/base/mutex.cpp
index e34e861..2a4542c 100644
--- a/src/libcamera/base/mutex.cpp
+++ b/src/libcamera/base/mutex.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * mutex.cpp - Mutex classes with clang thread safety annotation
+ * Mutex classes with clang thread safety annotation
  */
 
 #include <libcamera/base/mutex.h>
diff --git a/src/libcamera/base/object.cpp b/src/libcamera/base/object.cpp
index 92cecd2..745d256 100644
--- a/src/libcamera/base/object.cpp
+++ b/src/libcamera/base/object.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * object.cpp - Base object
+ * Base object
  */
 
 #include <libcamera/base/object.h>
@@ -40,8 +40,9 @@
  * Object class.
  *
  * Deleting an object from a thread other than the one the object is bound to is
- * unsafe, unless the caller ensures that the object isn't processing any
- * message concurrently.
+ * unsafe, unless the caller ensures that the object's thread is stopped and no
+ * parent or child of the object gets deleted concurrently. See
+ * Object::~Object() for more information.
  *
  * Object slots connected to signals will also run in the context of the
  * object's thread, regardless of whether the signal is emitted in the same or
@@ -84,9 +85,20 @@
  * Object instances shall be destroyed from the thread they are bound to,
  * otherwise undefined behaviour may occur. If deletion of an Object needs to
  * be scheduled from a different thread, deleteLater() shall be used.
+ *
+ * As an exception to this rule, Object instances may be deleted from a
+ * different thread if the thread the instance is bound to is stopped through
+ * the whole duration of the object's destruction, *and* the parent and children
+ * of the object do not get deleted concurrently. The caller is responsible for
+ * fulfilling those requirements.
+ *
+ * In all cases Object instances shall be deleted before the Thread they are
+ * bound to.
  */
 Object::~Object()
 {
+	ASSERT(Thread::current() == thread_ || !thread_->isRunning());
+
 	/*
 	 * Move signals to a private list to avoid concurrent iteration and
 	 * deletion of items from Signal::disconnect().
@@ -116,8 +128,9 @@
  * event loop that the object belongs to. This ensures the object is destroyed
  * from the right context, as required by the libcamera threading model.
  *
- * If this function is called before the thread's event loop is started, the
- * object will be deleted when the event loop starts.
+ * If this function is called before the thread's event loop is started or after
+ * it has stopped, the object will be deleted when the event loop (re)starts. If
+ * this never occurs, the object will be leaked.
  *
  * Deferred deletion can be used to control the destruction context with shared
  * pointers. An object managed with shared pointers is deleted when the last
@@ -213,6 +226,35 @@
 }
 
 /**
+ * \fn Object::assertThreadBound()
+ * \brief Check if the caller complies with thread-bound constraints
+ * \param[in] message The message to be printed on error
+ *
+ * This function verifies the calling constraints required by the \threadbound
+ * definition. It shall be called at the beginning of member functions of an
+ * Object subclass that are explicitly marked as thread-bound in their
+ * documentation.
+ *
+ * If the thread-bound constraints are not met, the function prints \a message
+ * as an error message. For debug builds, it additionally causes an assertion
+ * error.
+ *
+ * \todo Verify the thread-bound requirements for functions marked as
+ * thread-bound at the class level.
+ *
+ * \return True if the call is thread-bound compliant, false otherwise
+ */
+bool Object::assertThreadBound(const char *message)
+{
+	if (Thread::current() == thread_)
+		return true;
+
+	LOG(Object, Error) << message;
+	ASSERT(false);
+	return false;
+}
+
+/**
  * \fn R Object::invokeMethod()
  * \brief Invoke a method asynchronously on an Object instance
  * \param[in] func The object method to invoke
@@ -259,11 +301,12 @@
  * Moving an object that has a parent is not allowed, and causes undefined
  * behaviour.
  *
- * \context This function is thread-bound.
+ * \context This function is \threadbound.
  */
 void Object::moveToThread(Thread *thread)
 {
-	ASSERT(Thread::current() == thread_);
+	if (!assertThreadBound("Object can't be moved from another thread"))
+		return;
 
 	if (thread_ == thread)
 		return;
diff --git a/src/libcamera/base/semaphore.cpp b/src/libcamera/base/semaphore.cpp
index 6217e38..862f3b3 100644
--- a/src/libcamera/base/semaphore.cpp
+++ b/src/libcamera/base/semaphore.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * semaphore.cpp - General-purpose counting semaphore
+ * General-purpose counting semaphore
  */
 
 #include <libcamera/base/semaphore.h>
diff --git a/src/libcamera/base/shared_fd.cpp b/src/libcamera/base/shared_fd.cpp
index c711cf5..7afc8ca 100644
--- a/src/libcamera/base/shared_fd.cpp
+++ b/src/libcamera/base/shared_fd.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * shared_fd.cpp - File descriptor wrapper with shared ownership
+ * File descriptor wrapper with shared ownership
  */
 
 #include <libcamera/base/shared_fd.h>
diff --git a/src/libcamera/base/signal.cpp b/src/libcamera/base/signal.cpp
index a46386a..b782e05 100644
--- a/src/libcamera/base/signal.cpp
+++ b/src/libcamera/base/signal.cpp
@@ -2,12 +2,13 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * signal.cpp - Signal & slot implementation
+ * Signal & slot implementation
  */
 
 #include <libcamera/base/signal.h>
 
 #include <libcamera/base/mutex.h>
+#include <libcamera/base/object.h>
 
 /**
  * \file base/signal.h
@@ -74,7 +75,7 @@
  *
  * Signals and slots are a language construct aimed at communication between
  * objects through the observer pattern without the need for boilerplate code.
- * See http://doc.qt.io/qt-5/signalsandslots.html for more information.
+ * See http://doc.qt.io/qt-6/signalsandslots.html for more information.
  *
  * Signals model events that can be observed from objects unrelated to the event
  * source. Slots are functions that are called in response to a signal. Signals
diff --git a/src/libcamera/base/thread.cpp b/src/libcamera/base/thread.cpp
index b96951a..7273343 100644
--- a/src/libcamera/base/thread.cpp
+++ b/src/libcamera/base/thread.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * thread.cpp - Thread support
+ * Thread support
  */
 
 #include <libcamera/base/thread.h>
@@ -18,6 +18,7 @@
 #include <libcamera/base/log.h>
 #include <libcamera/base/message.h>
 #include <libcamera/base/mutex.h>
+#include <libcamera/base/object.h>
 
 /**
  * \page thread Thread Support
@@ -370,6 +371,12 @@
 
 void Thread::finishThread()
 {
+	/*
+	 * Objects may have been scheduled for deletion right before the thread
+	 * exited. Ensure they get deleted now, before the thread stops.
+	 */
+	dispatchMessages(Message::Type::DeferredDelete);
+
 	data_->mutex_.lock();
 	data_->running_ = false;
 	data_->mutex_.unlock();
diff --git a/src/libcamera/base/timer.cpp b/src/libcamera/base/timer.cpp
index 74b060a..7b0f372 100644
--- a/src/libcamera/base/timer.cpp
+++ b/src/libcamera/base/timer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * timer.cpp - Generic timer
+ * Generic timer
  */
 
 #include <libcamera/base/timer.h>
@@ -85,10 +85,8 @@
  */
 void Timer::start(std::chrono::steady_clock::time_point deadline)
 {
-	if (Thread::current() != thread()) {
-		LOG(Timer, Error) << "Timer " << this << " << can't be started from another thread";
+	if (!assertThreadBound("Timer can't be started from another thread"))
 		return;
-	}
 
 	deadline_ = deadline;
 
@@ -114,13 +112,11 @@
  */
 void Timer::stop()
 {
-	if (!isRunning())
+	if (!assertThreadBound("Timer can't be stopped from another thread"))
 		return;
 
-	if (Thread::current() != thread()) {
-		LOG(Timer, Error) << "Timer " << this << " can't be stopped from another thread";
+	if (!isRunning())
 		return;
-	}
 
 	unregisterTimer();
 }
diff --git a/src/libcamera/base/unique_fd.cpp b/src/libcamera/base/unique_fd.cpp
index 83d6919..d0649e4 100644
--- a/src/libcamera/base/unique_fd.cpp
+++ b/src/libcamera/base/unique_fd.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * unique_fd.cpp - File descriptor wrapper that owns a file descriptor
+ * File descriptor wrapper that owns a file descriptor
  */
 
 #include <libcamera/base/unique_fd.h>
diff --git a/src/libcamera/base/utils.cpp b/src/libcamera/base/utils.cpp
index 3b73b44..ccb3106 100644
--- a/src/libcamera/base/utils.cpp
+++ b/src/libcamera/base/utils.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * utils.cpp - Miscellaneous utility functions
+ * Miscellaneous utility functions
  */
 
 #include <libcamera/base/utils.h>
@@ -517,10 +517,20 @@
 	 * If the libc implementation doesn't provide locale object support,
 	 * assume that strtod() is locale-independent.
 	 */
-	return strtod(nptr, endptr);
+	return ::strtod(nptr, endptr);
 #endif
 }
 
+/**
+ * \fn to_underlying(Enum e)
+ * \brief Convert an enumeration to its underlygin type
+ * \param[in] e Enumeration value to convert
+ *
+ * This function is equivalent to the C++23 std::to_underlying().
+ *
+ * \return The value of e converted to its underlying type
+ */
+
 } /* namespace utils */
 
 #ifndef __DOXYGEN__
diff --git a/src/libcamera/bayer_format.cpp b/src/libcamera/bayer_format.cpp
index 3bf15fb..014f716 100644
--- a/src/libcamera/bayer_format.cpp
+++ b/src/libcamera/bayer_format.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * bayer_format.cpp - Class to represent Bayer formats
+ * Class to represent Bayer formats
  */
 
 #include "libcamera/internal/bayer_format.h"
@@ -61,6 +61,10 @@
  * \brief Format uses MIPI CSI-2 style packing
  * \var BayerFormat::Packing::IPU3
  * \brief Format uses IPU3 style packing
+ * \var BayerFormat::Packing::PISP1
+ * \brief Format uses PISP mode 1 compression
+ * \var BayerFormat::Packing::PISP2
+ * \brief Format uses PISP mode 2 compression
  */
 
 namespace {
@@ -164,12 +168,26 @@
 		{ formats::SGRBG16, V4L2PixelFormat(V4L2_PIX_FMT_SGRBG16) } },
 	{ { BayerFormat::RGGB, 16, BayerFormat::Packing::None },
 		{ formats::SRGGB16, V4L2PixelFormat(V4L2_PIX_FMT_SRGGB16) } },
+	{ { BayerFormat::BGGR, 16, BayerFormat::Packing::PISP1 },
+		{ formats::BGGR_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_BGGR) } },
+	{ { BayerFormat::GBRG, 16, BayerFormat::Packing::PISP1 },
+		{ formats::GBRG_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GBRG) } },
+	{ { BayerFormat::GRBG, 16, BayerFormat::Packing::PISP1 },
+		{ formats::GRBG_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GRBG) } },
+	{ { BayerFormat::RGGB, 16, BayerFormat::Packing::PISP1 },
+		{ formats::RGGB_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_RGGB) } },
 	{ { BayerFormat::MONO, 8, BayerFormat::Packing::None },
 		{ formats::R8, V4L2PixelFormat(V4L2_PIX_FMT_GREY) } },
 	{ { BayerFormat::MONO, 10, BayerFormat::Packing::None },
 		{ formats::R10, V4L2PixelFormat(V4L2_PIX_FMT_Y10) } },
 	{ { BayerFormat::MONO, 10, BayerFormat::Packing::CSI2 },
 		{ formats::R10_CSI2P, V4L2PixelFormat(V4L2_PIX_FMT_Y10P) } },
+	{ { BayerFormat::MONO, 12, BayerFormat::Packing::None },
+		{ formats::R12, V4L2PixelFormat(V4L2_PIX_FMT_Y12) } },
+	{ { BayerFormat::MONO, 16, BayerFormat::Packing::None },
+		{ formats::R16, V4L2PixelFormat(V4L2_PIX_FMT_Y16) } },
+	{ { BayerFormat::MONO, 16, BayerFormat::Packing::PISP1 },
+		{ formats::MONO_PISP_COMP1, V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_MONO) } },
 };
 
 const std::unordered_map<unsigned int, BayerFormat> mbusCodeToBayer{
@@ -208,6 +226,7 @@
 	{ MEDIA_BUS_FMT_Y8_1X8, { BayerFormat::MONO, 8, BayerFormat::Packing::None } },
 	{ MEDIA_BUS_FMT_Y10_1X10, { BayerFormat::MONO, 10, BayerFormat::Packing::None } },
 	{ MEDIA_BUS_FMT_Y12_1X12, { BayerFormat::MONO, 12, BayerFormat::Packing::None } },
+	{ MEDIA_BUS_FMT_Y16_1X16, { BayerFormat::MONO, 16, BayerFormat::Packing::None } },
 };
 
 } /* namespace */
@@ -298,6 +317,10 @@
 		out << "-CSI2P";
 	else if (f.packing == BayerFormat::Packing::IPU3)
 		out << "-IPU3P";
+	else if (f.packing == BayerFormat::Packing::PISP1)
+		out << "-PISP1";
+	else if (f.packing == BayerFormat::Packing::PISP2)
+		out << "-PISP2";
 
 	return out;
 }
diff --git a/src/libcamera/byte_stream_buffer.cpp b/src/libcamera/byte_stream_buffer.cpp
index 881cd37..fba9a6f 100644
--- a/src/libcamera/byte_stream_buffer.cpp
+++ b/src/libcamera/byte_stream_buffer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * byte_stream_buffer.cpp - Byte stream buffer
+ * Byte stream buffer
  */
 
 #include "libcamera/internal/byte_stream_buffer.h"
diff --git a/src/libcamera/camera.cpp b/src/libcamera/camera.cpp
index 0eecee7..67f3490 100644
--- a/src/libcamera/camera.cpp
+++ b/src/libcamera/camera.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * camera.cpp - Camera device
+ * Camera device
  */
 
 #include <libcamera/camera.h>
@@ -97,6 +97,16 @@
  * implemented in the above order at the hardware level. The libcamera pipeline
  * handlers translate the pipeline model to the real hardware configuration.
  *
+ * \subsection camera-sensor-model Camera Sensor Model
+ *
+ * By default, libcamera configures the camera sensor automatically based on the
+ * configuration of the streams. Applications may instead specify a manual
+ * configuration for the camera sensor. This allows precise control of the frame
+ * geometry and frame rate delivered by the sensor.
+ *
+ * More details about the camera sensor model implemented by libcamera are
+ * available in the libcamera camera-sensor-model documentation page.
+ *
  * \subsection digital-zoom Digital Zoom
  *
  * Digital zoom is implemented as a combination of the cropping and scaling
@@ -112,6 +122,127 @@
 LOG_DECLARE_CATEGORY(Camera)
 
 /**
+ * \class SensorConfiguration
+ * \brief Camera sensor configuration
+ *
+ * The SensorConfiguration class collects parameters to control the operations
+ * of the camera sensor, according to the abstract camera sensor model
+ * implemented by libcamera.
+ *
+ * \todo Applications shall fully populate all fields of the
+ * CameraConfiguration::sensorConfig class members before validating the
+ * CameraConfiguration. If the SensorConfiguration is not fully populated, or if
+ * any of its parameters cannot be applied to the sensor in use, the
+ * CameraConfiguration validation process will fail and return
+ * CameraConfiguration::Status::Invalid.
+ *
+ * Applications that populate the SensorConfiguration class members are
+ * expected to be highly-specialized applications that know what sensor
+ * they are operating with and what parameters are valid for the sensor in use.
+ *
+ * A detailed description of the abstract camera sensor model implemented by
+ * libcamera and the description of its configuration parameters is available
+ * in the libcamera documentation camera-sensor-model file.
+ */
+
+/**
+ * \var SensorConfiguration::bitDepth
+ * \brief The sensor image format bit depth
+ *
+ * The number of bits (resolution) used to represent a pixel sample.
+ */
+
+/**
+ * \var SensorConfiguration::analogCrop
+ * \brief The analog crop rectangle
+ *
+ * The selected portion of the active pixel array used to produce the image
+ * frame.
+ */
+
+/**
+ * \var SensorConfiguration::binning
+ * \brief Sensor binning configuration
+ *
+ * Refer to the camera-sensor-model documentation for an accurate description
+ * of the binning operations. Disabled by default.
+ */
+
+/**
+ * \var SensorConfiguration::binX
+ * \brief Horizontal binning factor
+ *
+ * The horizontal binning factor. Default to 1.
+ */
+
+/**
+ * \var SensorConfiguration::binY
+ * \brief Vertical binning factor
+ *
+ * The vertical binning factor. Default to 1.
+ */
+
+/**
+ * \var SensorConfiguration::skipping
+ * \brief The sensor skipping configuration
+ *
+ * Refer to the camera-sensor-model documentation for an accurate description
+ * of the skipping operations.
+ *
+ * If no skipping is performed, all the structure fields should be
+ * set to 1. Disabled by default.
+ */
+
+/**
+ * \var SensorConfiguration::xOddInc
+ * \brief Horizontal increment for odd rows. Default to 1.
+ */
+
+/**
+ * \var SensorConfiguration::xEvenInc
+ * \brief Horizontal increment for even rows. Default to 1.
+ */
+
+/**
+ * \var SensorConfiguration::yOddInc
+ * \brief Vertical increment for odd columns. Default to 1.
+ */
+
+/**
+ * \var SensorConfiguration::yEvenInc
+ * \brief Vertical increment for even columns. Default to 1.
+ */
+
+/**
+ * \var SensorConfiguration::outputSize
+ * \brief The frame output (visible) size
+ *
+ * The size of the data frame as received by the host processor.
+ */
+
+/**
+ * \brief Check if the sensor configuration is valid
+ *
+ * A sensor configuration is valid if it's fully populated.
+ *
+ * \todo For now allow applications to populate the bitDepth and the outputSize
+ * only as skipping and binnings factors are initialized to 1 and the analog
+ * crop is ignored.
+ *
+ * \return True if the sensor configuration is valid, false otherwise
+ */
+bool SensorConfiguration::isValid() const
+{
+	if (bitDepth && binning.binX && binning.binY &&
+	    skipping.xOddInc && skipping.yOddInc &&
+	    skipping.xEvenInc && skipping.yEvenInc &&
+	    !outputSize.isNull())
+		return true;
+
+	return false;
+}
+
+/**
  * \class CameraConfiguration
  * \brief Hold configuration for streams of the camera
 
@@ -160,7 +291,7 @@
  * \brief Create an empty camera configuration
  */
 CameraConfiguration::CameraConfiguration()
-	: transform(Transform::Identity), config_({})
+	: orientation(Orientation::Rotate0), config_({})
 {
 }
 
@@ -363,7 +494,7 @@
 	std::optional<ColorSpace> colorSpace;
 	Size size;
 
-	for (auto [i, cfg] : utils::enumerate(config_)) {
+	for (StreamConfiguration &cfg : config_) {
 		if (!cfg.colorSpace)
 			continue;
 
@@ -392,16 +523,35 @@
 }
 
 /**
- * \var CameraConfiguration::transform
- * \brief User-specified transform to be applied to the image
+ * \var CameraConfiguration::sensorConfig
+ * \brief The camera sensor configuration
  *
- * The transform is a user-specified 2D plane transform that will be applied
- * to the camera images by the processing pipeline before being handed to
- * the application.
+ * The sensorConfig member allows manual control of the configuration of the
+ * camera sensor. By default, if sensorConfig is not set, the camera will
+ * configure the sensor automatically based on the configuration of the streams.
+ * Applications can override this by manually specifying the full sensor
+ * configuration.
  *
- * The usual 2D plane transforms are allowed here (horizontal/vertical
- * flips, multiple of 90-degree rotations etc.), but the validate() function
- * may adjust this field at its discretion if the selection is not supported.
+ * Refer to the camera-sensor-model documentation and to the SensorConfiguration
+ * class documentation for details about the sensor configuration process.
+ *
+ * The camera sensor configuration applies to all streams produced by a camera
+ * from the same image source.
+ */
+
+/**
+ * \var CameraConfiguration::orientation
+ * \brief The desired orientation of the images produced by the camera
+ *
+ * The orientation field is a user-specified 2D plane transformation that
+ * specifies how the application wants the camera images to be rotated in
+ * the memory buffers.
+ *
+ * If the orientation requested by the application cannot be obtained, the
+ * camera will not rotate or flip the images, and the validate() function will
+ * Adjust this value to the native image orientation produced by the camera.
+ *
+ * By default the orientation field is set to Orientation::Rotate0.
  */
 
 /**
diff --git a/src/libcamera/camera_controls.cpp b/src/libcamera/camera_controls.cpp
index cabdcf7..b672c7c 100644
--- a/src/libcamera/camera_controls.cpp
+++ b/src/libcamera/camera_controls.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_controls.cpp - Camera controls
+ * Camera controls
  */
 
 #include "libcamera/internal/camera_controls.h"
diff --git a/src/libcamera/camera_lens.cpp b/src/libcamera/camera_lens.cpp
index b3d4819..ccc2a6a 100644
--- a/src/libcamera/camera_lens.cpp
+++ b/src/libcamera/camera_lens.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_lens.cpp - A camera lens
+ * A camera lens
  */
 
 #include "libcamera/internal/camera_lens.h"
diff --git a/src/libcamera/camera_manager.cpp b/src/libcamera/camera_manager.cpp
index 355f3ad..95a9e32 100644
--- a/src/libcamera/camera_manager.cpp
+++ b/src/libcamera/camera_manager.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * camera_manager.h - Camera management
+ * Camera management
  */
 
 #include "libcamera/internal/camera_manager.h"
@@ -99,16 +99,37 @@
 
 void CameraManager::Private::createPipelineHandlers()
 {
-	CameraManager *const o = LIBCAMERA_O_PTR();
-
 	/*
 	 * \todo Try to read handlers and order from configuration
-	 * file and only fallback on all handlers if there is no
-	 * configuration file.
+	 * file and only fallback on environment variable or all handlers, if
+	 * there is no configuration file.
 	 */
+	const char *pipesList =
+		utils::secure_getenv("LIBCAMERA_PIPELINES_MATCH_LIST");
+	if (pipesList) {
+		/*
+		 * When a list of preferred pipelines is defined, iterate
+		 * through the ordered list to match the enumerated devices.
+		 */
+		for (const auto &pipeName : utils::split(pipesList, ",")) {
+			const PipelineHandlerFactoryBase *factory;
+			factory = PipelineHandlerFactoryBase::getFactoryByName(pipeName);
+			if (!factory)
+				continue;
+
+			LOG(Camera, Debug)
+				<< "Found listed pipeline handler '"
+				<< pipeName << "'";
+			pipelineFactoryMatch(factory);
+		}
+
+		return;
+	}
+
 	const std::vector<PipelineHandlerFactoryBase *> &factories =
 		PipelineHandlerFactoryBase::factories();
 
+	/* Match all the registered pipeline handlers. */
 	for (const PipelineHandlerFactoryBase *factory : factories) {
 		LOG(Camera, Debug)
 			<< "Found registered pipeline handler '"
@@ -117,15 +138,23 @@
 		 * Try each pipeline handler until it exhaust
 		 * all pipelines it can provide.
 		 */
-		while (1) {
-			std::shared_ptr<PipelineHandler> pipe = factory->create(o);
-			if (!pipe->match(enumerator_.get()))
-				break;
+		pipelineFactoryMatch(factory);
+	}
+}
 
-			LOG(Camera, Debug)
-				<< "Pipeline handler \"" << factory->name()
-				<< "\" matched";
-		}
+void CameraManager::Private::pipelineFactoryMatch(const PipelineHandlerFactoryBase *factory)
+{
+	CameraManager *const o = LIBCAMERA_O_PTR();
+
+	/* Provide as many matching pipelines as possible. */
+	while (1) {
+		std::shared_ptr<PipelineHandler> pipe = factory->create(o);
+		if (!pipe->match(enumerator_.get()))
+			break;
+
+		LOG(Camera, Debug)
+			<< "Pipeline handler \"" << factory->name()
+			<< "\" matched";
 	}
 }
 
diff --git a/src/libcamera/color_space.cpp b/src/libcamera/color_space.cpp
index 7356bf7..3d1c456 100644
--- a/src/libcamera/color_space.cpp
+++ b/src/libcamera/color_space.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Raspberry Pi Ltd
  *
- * color_space.cpp - color spaces.
+ * color spaces.
  */
 
 #include <libcamera/color_space.h>
diff --git a/src/libcamera/control_ids.cpp.in b/src/libcamera/control_ids.cpp.in
index 5fb1c2c..d283c1c 100644
--- a/src/libcamera/control_ids.cpp.in
+++ b/src/libcamera/control_ids.cpp.in
@@ -24,14 +24,7 @@
 
 ${controls_doc}
 
-/**
- * \brief Namespace for libcamera draft controls
- */
-namespace draft {
-
-${draft_controls_doc}
-
-} /* namespace draft */
+${vendor_controls_doc}
 
 #ifndef __DOXYGEN__
 /*
@@ -40,11 +33,8 @@
  */
 ${controls_def}
 
-namespace draft {
+${vendor_controls_def}
 
-${draft_controls_def}
-
-} /* namespace draft */
 #endif
 
 /**
diff --git a/src/libcamera/control_ids.yaml b/src/libcamera/control_ids_core.yaml
similarity index 78%
rename from src/libcamera/control_ids.yaml
rename to src/libcamera/control_ids_core.yaml
index f2e542f..9d413a9 100644
--- a/src/libcamera/control_ids.yaml
+++ b/src/libcamera/control_ids_core.yaml
@@ -6,6 +6,7 @@
 ---
 # Unless otherwise stated, all controls are bi-directional, i.e. they can be
 # set through Request::controls() and returned out through Request::metadata().
+vendor: libcamera
 controls:
   - AeEnable:
       type: bool
@@ -448,7 +449,7 @@
         range of reported temperatures is device dependent.
 
         The SensorTemperature control will only be returned in metadata if a
-        themal sensor is present.
+        thermal sensor is present.
 
   - SensorTimestamp:
       type: int64_t
@@ -774,236 +775,101 @@
             Continuous AF is paused. No further state changes or lens movements
             will occur until the AfPauseResume control is sent.
 
-  # ----------------------------------------------------------------------------
-  # Draft controls section
-
-  - AePrecaptureTrigger:
+  - HdrMode:
       type: int32_t
-      draft: true
       description: |
-        Control for AE metering trigger. Currently identical to
-        ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER.
+        Control to set the mode to be used for High Dynamic Range (HDR)
+        imaging. HDR techniques typically include multiple exposure, image
+        fusion and tone mapping techniques to improve the dynamic range of the
+        resulting images.
 
-        Whether the camera device will trigger a precapture metering sequence
-        when it processes this request.
+        When using an HDR mode, images are captured with different sets of AGC
+        settings called HDR channels. Channels indicate in particular the type
+        of exposure (short, medium or long) used to capture the raw image,
+        before fusion. Each HDR image is tagged with the corresponding channel
+        using the HdrChannel control.
+
+        \sa HdrChannel
+
       enum:
-        - name: AePrecaptureTriggerIdle
+        - name: HdrModeOff
           value: 0
-          description: The trigger is idle.
-        - name: AePrecaptureTriggerStart
-          value: 1
-          description: The pre-capture AE metering is started by the camera.
-        - name: AePrecaptureTriggerCancel
-          value: 2
           description: |
-            The camera will cancel any active or completed metering sequence.
-            The AE algorithm is reset to its initial state.
-
-  - NoiseReductionMode:
-      type: int32_t
-      draft: true
-      description: |
-       Control to select the noise reduction algorithm mode. Currently
-       identical to ANDROID_NOISE_REDUCTION_MODE.
-
-        Mode of operation for the noise reduction algorithm.
-      enum:
-        - name: NoiseReductionModeOff
-          value: 0
-          description: No noise reduction is applied
-        - name: NoiseReductionModeFast
+            HDR is disabled. Metadata for this frame will not include the
+            HdrChannel control.
+        - name: HdrModeMultiExposureUnmerged
           value: 1
           description: |
-            Noise reduction is applied without reducing the frame rate.
-        - name: NoiseReductionModeHighQuality
+            Multiple exposures will be generated in an alternating fashion.
+            However, they will not be merged together and will be returned to
+            the application as they are. Each image will be tagged with the
+            correct HDR channel, indicating what kind of exposure it is. The
+            tag should be the same as in the HdrModeMultiExposure case.
+
+            The expectation is that an application using this mode would merge
+            the frames to create HDR images for itself if it requires them.
+        - name: HdrModeMultiExposure
           value: 2
           description: |
-            High quality noise reduction at the expense of frame rate.
-        - name: NoiseReductionModeMinimal
+            Multiple exposures will be generated and merged to create HDR
+            images. Each image will be tagged with the HDR channel (long, medium
+            or short) that arrived and which caused this image to be output.
+
+            Systems that use two channels for HDR will return images tagged
+            alternately as the short and long channel. Systems that use three
+            channels for HDR will cycle through the short, medium and long
+            channel before repeating.
+        - name: HdrModeSingleExposure
           value: 3
           description: |
-            Minimal noise reduction is applied without reducing the frame rate.
-        - name: NoiseReductionModeZSL
+            Multiple frames all at a single exposure will be used to create HDR
+            images. These images should be reported as all corresponding to the
+            HDR short channel.
+        - name: HdrModeNight
           value: 4
           description: |
-            Noise reduction is applied at different levels to different streams.
+            Multiple frames will be combined to produce "night mode" images. It
+            is up to the implementation exactly which HDR channels it uses, and
+            the images will all be tagged accordingly with the correct HDR
+            channel information.
 
-  - ColorCorrectionAberrationMode:
+  - HdrChannel:
       type: int32_t
-      draft: true
       description: |
-       Control to select the color correction aberration mode. Currently
-       identical to ANDROID_COLOR_CORRECTION_ABERRATION_MODE.
+        This value is reported back to the application so that it can discover
+        whether this capture corresponds to the short or long exposure image (or
+        any other image used by the HDR procedure). An application can monitor
+        the HDR channel to discover when the differently exposed images have
+        arrived.
 
-        Mode of operation for the chromatic aberration correction algorithm.
+        This metadata is only available when an HDR mode has been enabled.
+
+        \sa HdrMode
+
       enum:
-        - name: ColorCorrectionAberrationOff
+        - name: HdrChannelNone
           value: 0
-          description: No aberration correction is applied.
-        - name: ColorCorrectionAberrationFast
+          description: |
+            This image does not correspond to any of the captures used to create
+            an HDR image.
+        - name: HdrChannelShort
           value: 1
-          description: Aberration correction will not slow down the frame rate.
-        - name: ColorCorrectionAberrationHighQuality
+          description: |
+            This is a short exposure image.
+        - name: HdrChannelMedium
           value: 2
           description: |
-            High quality aberration correction which might reduce the frame
-            rate.
-
-  - AeState:
-      type: int32_t
-      draft: true
-      description: |
-       Control to report the current AE algorithm state. Currently identical to
-       ANDROID_CONTROL_AE_STATE.
-
-        Current state of the AE algorithm.
-      enum:
-        - name: AeStateInactive
-          value: 0
-          description: The AE algorithm is inactive.
-        - name: AeStateSearching
-          value: 1
-          description: The AE algorithm has not converged yet.
-        - name: AeStateConverged
-          value: 2
-          description: The AE algorithm has converged.
-        - name: AeStateLocked
-          value: 3
-          description: The AE algorithm is locked.
-        - name: AeStateFlashRequired
-          value: 4
-          description: The AE algorithm would need a flash for good results
-        - name: AeStatePrecapture
-          value: 5
-          description: |
-            The AE algorithm has started a pre-capture metering session.
-            \sa AePrecaptureTrigger
-
-  - AwbState:
-      type: int32_t
-      draft: true
-      description: |
-       Control to report the current AWB algorithm state. Currently identical
-       to ANDROID_CONTROL_AWB_STATE.
-
-        Current state of the AWB algorithm.
-      enum:
-        - name: AwbStateInactive
-          value: 0
-          description: The AWB algorithm is inactive.
-        - name: AwbStateSearching
-          value: 1
-          description: The AWB algorithm has not converged yet.
-        - name: AwbConverged
-          value: 2
-          description: The AWB algorithm has converged.
-        - name: AwbLocked
-          value: 3
-          description: The AWB algorithm is locked.
-
-  - SensorRollingShutterSkew:
-      type: int64_t
-      draft: true
-      description: |
-       Control to report the time between the start of exposure of the first
-       row and the start of exposure of the last row. Currently identical to
-       ANDROID_SENSOR_ROLLING_SHUTTER_SKEW
-
-  - LensShadingMapMode:
-      type: int32_t
-      draft: true
-      description: |
-       Control to report if the lens shading map is available. Currently
-       identical to ANDROID_STATISTICS_LENS_SHADING_MAP_MODE.
-      enum:
-        - name: LensShadingMapModeOff
-          value: 0
-          description: No lens shading map mode is available.
-        - name: LensShadingMapModeOn
-          value: 1
-          description: The lens shading map mode is available.
-
-  - PipelineDepth:
-      type: int32_t
-      draft: true
-      description: |
-        Specifies the number of pipeline stages the frame went through from when
-        it was exposed to when the final completed result was available to the
-        framework. Always less than or equal to PipelineMaxDepth. Currently
-        identical to ANDROID_REQUEST_PIPELINE_DEPTH.
-
-        The typical value for this control is 3 as a frame is first exposed,
-        captured and then processed in a single pass through the ISP. Any
-        additional processing step performed after the ISP pass (in example face
-        detection, additional format conversions etc) count as an additional
-        pipeline stage.
-
-  - MaxLatency:
-      type: int32_t
-      draft: true
-      description: |
-        The maximum number of frames that can occur after a request (different
-        than the previous) has been submitted, and before the result's state
-        becomes synchronized. A value of -1 indicates unknown latency, and 0
-        indicates per-frame control. Currently identical to
-        ANDROID_SYNC_MAX_LATENCY.
-
-  - TestPatternMode:
-      type: int32_t
-      draft: true
-      description: |
-        Control to select the test pattern mode. Currently identical to
-        ANDROID_SENSOR_TEST_PATTERN_MODE.
-      enum:
-        - name: TestPatternModeOff
-          value: 0
-          description: |
-            No test pattern mode is used. The camera device returns frames from
-            the image sensor.
-        - name: TestPatternModeSolidColor
-          value: 1
-          description: |
-            Each pixel in [R, G_even, G_odd, B] is replaced by its respective
-            color channel provided in test pattern data.
-            \todo Add control for test pattern data.
-        - name: TestPatternModeColorBars
-          value: 2
-          description: |
-            All pixel data is replaced with an 8-bar color pattern. The vertical
-            bars (left-to-right) are as follows; white, yellow, cyan, green,
-            magenta, red, blue and black. Each bar should take up 1/8 of the
-            sensor pixel array width. When this is not possible, the bar size
-            should be rounded down to the nearest integer and the pattern can
-            repeat on the right side. Each bar's height must always take up the
-            full sensor pixel array height.
-        - name: TestPatternModeColorBarsFadeToGray
+            This is a medium exposure image.
+        - name: HdrChannelLong
           value: 3
           description: |
-            The test pattern is similar to TestPatternModeColorBars,
-            except that each bar should start at its specified color at the top
-            and fade to gray at the bottom. Furthermore each bar is further
-            subdevided into a left and right half. The left half should have a
-            smooth gradient, and the right half should have a quantized
-            gradient. In particular, the right half's should consist of blocks
-            of the same color for 1/16th active sensor pixel array width. The
-            least significant bits in the quantized gradient should be copied
-            from the most significant bits of the smooth gradient. The height of
-            each bar should always be a multiple of 128. When this is not the
-            case, the pattern should repeat at the bottom of the image.
-        - name: TestPatternModePn9
-          value: 4
-          description: |
-            All pixel data is replaced by a pseudo-random sequence generated
-            from a PN9 512-bit sequence (typically implemented in hardware with
-            a linear feedback shift register). The generator should be reset at
-            the beginning of each frame, and thus each subsequent raw frame with
-            this test pattern should be exactly the same as the last.
-        - name: TestPatternModeCustom1
-          value: 256
-          description: |
-            The first custom test pattern. All custom patterns that are
-            available only on this camera device are at least this numeric
-            value. All of the custom test patterns will be static (that is the
-            raw image must not vary from frame to frame).
+            This is a long exposure image.
+
+  - Gamma:
+      type: float
+      description:  |
+        Specify a fixed gamma value. Default must be 2.2 which closely mimics 
+        sRGB gamma. Note that this is camera gamma, so it is applied as 
+        1.0/gamma.
 
 ...
diff --git a/src/libcamera/control_ids_draft.yaml b/src/libcamera/control_ids_draft.yaml
new file mode 100644
index 0000000..9bef5bf
--- /dev/null
+++ b/src/libcamera/control_ids_draft.yaml
@@ -0,0 +1,230 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# Copyright (C) 2019, Google Inc.
+#
+%YAML 1.1
+---
+# Unless otherwise stated, all controls are bi-directional, i.e. they can be
+# set through Request::controls() and returned out through Request::metadata().
+vendor: draft
+controls:
+  - AePrecaptureTrigger:
+      type: int32_t
+      description: |
+        Control for AE metering trigger. Currently identical to
+        ANDROID_CONTROL_AE_PRECAPTURE_TRIGGER.
+
+        Whether the camera device will trigger a precapture metering sequence
+        when it processes this request.
+      enum:
+        - name: AePrecaptureTriggerIdle
+          value: 0
+          description: The trigger is idle.
+        - name: AePrecaptureTriggerStart
+          value: 1
+          description: The pre-capture AE metering is started by the camera.
+        - name: AePrecaptureTriggerCancel
+          value: 2
+          description: |
+            The camera will cancel any active or completed metering sequence.
+            The AE algorithm is reset to its initial state.
+
+  - NoiseReductionMode:
+      type: int32_t
+      description: |
+       Control to select the noise reduction algorithm mode. Currently
+       identical to ANDROID_NOISE_REDUCTION_MODE.
+
+        Mode of operation for the noise reduction algorithm.
+      enum:
+        - name: NoiseReductionModeOff
+          value: 0
+          description: No noise reduction is applied
+        - name: NoiseReductionModeFast
+          value: 1
+          description: |
+            Noise reduction is applied without reducing the frame rate.
+        - name: NoiseReductionModeHighQuality
+          value: 2
+          description: |
+            High quality noise reduction at the expense of frame rate.
+        - name: NoiseReductionModeMinimal
+          value: 3
+          description: |
+            Minimal noise reduction is applied without reducing the frame rate.
+        - name: NoiseReductionModeZSL
+          value: 4
+          description: |
+            Noise reduction is applied at different levels to different streams.
+
+  - ColorCorrectionAberrationMode:
+      type: int32_t
+      description: |
+       Control to select the color correction aberration mode. Currently
+       identical to ANDROID_COLOR_CORRECTION_ABERRATION_MODE.
+
+        Mode of operation for the chromatic aberration correction algorithm.
+      enum:
+        - name: ColorCorrectionAberrationOff
+          value: 0
+          description: No aberration correction is applied.
+        - name: ColorCorrectionAberrationFast
+          value: 1
+          description: Aberration correction will not slow down the frame rate.
+        - name: ColorCorrectionAberrationHighQuality
+          value: 2
+          description: |
+            High quality aberration correction which might reduce the frame
+            rate.
+
+  - AeState:
+      type: int32_t
+      description: |
+       Control to report the current AE algorithm state. Currently identical to
+       ANDROID_CONTROL_AE_STATE.
+
+        Current state of the AE algorithm.
+      enum:
+        - name: AeStateInactive
+          value: 0
+          description: The AE algorithm is inactive.
+        - name: AeStateSearching
+          value: 1
+          description: The AE algorithm has not converged yet.
+        - name: AeStateConverged
+          value: 2
+          description: The AE algorithm has converged.
+        - name: AeStateLocked
+          value: 3
+          description: The AE algorithm is locked.
+        - name: AeStateFlashRequired
+          value: 4
+          description: The AE algorithm would need a flash for good results
+        - name: AeStatePrecapture
+          value: 5
+          description: |
+            The AE algorithm has started a pre-capture metering session.
+            \sa AePrecaptureTrigger
+
+  - AwbState:
+      type: int32_t
+      description: |
+       Control to report the current AWB algorithm state. Currently identical
+       to ANDROID_CONTROL_AWB_STATE.
+
+        Current state of the AWB algorithm.
+      enum:
+        - name: AwbStateInactive
+          value: 0
+          description: The AWB algorithm is inactive.
+        - name: AwbStateSearching
+          value: 1
+          description: The AWB algorithm has not converged yet.
+        - name: AwbConverged
+          value: 2
+          description: The AWB algorithm has converged.
+        - name: AwbLocked
+          value: 3
+          description: The AWB algorithm is locked.
+
+  - SensorRollingShutterSkew:
+      type: int64_t
+      description: |
+       Control to report the time between the start of exposure of the first
+       row and the start of exposure of the last row. Currently identical to
+       ANDROID_SENSOR_ROLLING_SHUTTER_SKEW
+
+  - LensShadingMapMode:
+      type: int32_t
+      description: |
+       Control to report if the lens shading map is available. Currently
+       identical to ANDROID_STATISTICS_LENS_SHADING_MAP_MODE.
+      enum:
+        - name: LensShadingMapModeOff
+          value: 0
+          description: No lens shading map mode is available.
+        - name: LensShadingMapModeOn
+          value: 1
+          description: The lens shading map mode is available.
+
+  - PipelineDepth:
+      type: int32_t
+      description: |
+        Specifies the number of pipeline stages the frame went through from when
+        it was exposed to when the final completed result was available to the
+        framework. Always less than or equal to PipelineMaxDepth. Currently
+        identical to ANDROID_REQUEST_PIPELINE_DEPTH.
+
+        The typical value for this control is 3 as a frame is first exposed,
+        captured and then processed in a single pass through the ISP. Any
+        additional processing step performed after the ISP pass (in example face
+        detection, additional format conversions etc) count as an additional
+        pipeline stage.
+
+  - MaxLatency:
+      type: int32_t
+      description: |
+        The maximum number of frames that can occur after a request (different
+        than the previous) has been submitted, and before the result's state
+        becomes synchronized. A value of -1 indicates unknown latency, and 0
+        indicates per-frame control. Currently identical to
+        ANDROID_SYNC_MAX_LATENCY.
+
+  - TestPatternMode:
+      type: int32_t
+      description: |
+        Control to select the test pattern mode. Currently identical to
+        ANDROID_SENSOR_TEST_PATTERN_MODE.
+      enum:
+        - name: TestPatternModeOff
+          value: 0
+          description: |
+            No test pattern mode is used. The camera device returns frames from
+            the image sensor.
+        - name: TestPatternModeSolidColor
+          value: 1
+          description: |
+            Each pixel in [R, G_even, G_odd, B] is replaced by its respective
+            color channel provided in test pattern data.
+            \todo Add control for test pattern data.
+        - name: TestPatternModeColorBars
+          value: 2
+          description: |
+            All pixel data is replaced with an 8-bar color pattern. The vertical
+            bars (left-to-right) are as follows; white, yellow, cyan, green,
+            magenta, red, blue and black. Each bar should take up 1/8 of the
+            sensor pixel array width. When this is not possible, the bar size
+            should be rounded down to the nearest integer and the pattern can
+            repeat on the right side. Each bar's height must always take up the
+            full sensor pixel array height.
+        - name: TestPatternModeColorBarsFadeToGray
+          value: 3
+          description: |
+            The test pattern is similar to TestPatternModeColorBars,
+            except that each bar should start at its specified color at the top
+            and fade to gray at the bottom. Furthermore each bar is further
+            subdevided into a left and right half. The left half should have a
+            smooth gradient, and the right half should have a quantized
+            gradient. In particular, the right half's should consist of blocks
+            of the same color for 1/16th active sensor pixel array width. The
+            least significant bits in the quantized gradient should be copied
+            from the most significant bits of the smooth gradient. The height of
+            each bar should always be a multiple of 128. When this is not the
+            case, the pattern should repeat at the bottom of the image.
+        - name: TestPatternModePn9
+          value: 4
+          description: |
+            All pixel data is replaced by a pseudo-random sequence generated
+            from a PN9 512-bit sequence (typically implemented in hardware with
+            a linear feedback shift register). The generator should be reset at
+            the beginning of each frame, and thus each subsequent raw frame with
+            this test pattern should be exactly the same as the last.
+        - name: TestPatternModeCustom1
+          value: 256
+          description: |
+            The first custom test pattern. All custom patterns that are
+            available only on this camera device are at least this numeric
+            value. All of the custom test patterns will be static (that is the
+            raw image must not vary from frame to frame).
+
+...
diff --git a/src/libcamera/control_ids_rpi.yaml b/src/libcamera/control_ids_rpi.yaml
new file mode 100644
index 0000000..cb097f8
--- /dev/null
+++ b/src/libcamera/control_ids_rpi.yaml
@@ -0,0 +1,29 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# Copyright (C) 2023, Raspberry Pi Ltd
+#
+%YAML 1.1
+---
+# Raspberry Pi (VC4 and PiSP) specific vendor controls
+vendor: rpi
+controls:
+  - StatsOutputEnable:
+      type: bool
+      description: |
+        Toggles the Raspberry Pi IPA to output a binary dump of the hardware
+        generated statistics through the Request metadata in the Bcm2835StatsOutput
+        control.
+
+        \sa Bcm2835StatsOutput
+
+  - Bcm2835StatsOutput:
+      type: uint8_t
+      size: [n]
+      description: |
+        Span of the BCM2835 ISP generated statistics for the current frame. This
+        is sent in the Request metadata if the StatsOutputEnable is set to true.
+        The statistics struct definition can be found in include/linux/bcm2835-isp.h.
+
+        \sa StatsOutputEnable
+
+...
diff --git a/src/libcamera/control_ranges.yaml b/src/libcamera/control_ranges.yaml
new file mode 100644
index 0000000..d42447d
--- /dev/null
+++ b/src/libcamera/control_ranges.yaml
@@ -0,0 +1,18 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# Copyright (C) 2023, Raspberry Pi Ltd
+#
+%YAML 1.1
+---
+# Specifies the control id ranges/offsets for core/draft libcamera and vendor
+# controls and properties.
+ranges:
+  # Core libcamera controls
+  libcamera: 0
+  # Draft designated libcamera controls
+  draft: 10000
+  # Raspberry Pi vendor controls
+  rpi: 20000
+  # Next range starts at 30000
+
+...
diff --git a/src/libcamera/control_serializer.cpp b/src/libcamera/control_serializer.cpp
index 0cf719b..52fd714 100644
--- a/src/libcamera/control_serializer.cpp
+++ b/src/libcamera/control_serializer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_serializer.cpp - Control (de)serializer
+ * Control (de)serializer
  */
 
 #include "libcamera/internal/control_serializer.h"
diff --git a/src/libcamera/control_validator.cpp b/src/libcamera/control_validator.cpp
index cf08b34..93982cf 100644
--- a/src/libcamera/control_validator.cpp
+++ b/src/libcamera/control_validator.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_validator.cpp - Control validator
+ * Control validator
  */
 
 #include "libcamera/internal/control_validator.h"
diff --git a/src/libcamera/controls.cpp b/src/libcamera/controls.cpp
index b808116..11d3532 100644
--- a/src/libcamera/controls.cpp
+++ b/src/libcamera/controls.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * controls.cpp - Control handling
+ * Control handling
  */
 
 #include <libcamera/controls.h>
@@ -908,12 +908,26 @@
  */
 
 /**
+ * \enum ControlList::MergePolicy
+ * \brief The policy used by the merge function
+ *
+ * \var ControlList::MergePolicy::KeepExisting
+ * \brief Existing controls in the target list are kept
+ *
+ * \var ControlList::MergePolicy::OverwriteExisting
+ * \brief Existing controls in the target list are updated
+ */
+
+/**
  * \brief Merge the \a source into the ControlList
  * \param[in] source The ControlList to merge into this object
+ * \param[in] policy Controls if existing elements in *this shall be
+ * overwritten
  *
  * Merging two control lists copies elements from the \a source and inserts
  * them in *this. If the \a source contains elements whose key is already
- * present in *this, then those elements are not overwritten.
+ * present in *this, then those elements are only overwritten if
+ * \a policy is MergePolicy::OverwriteExisting.
  *
  * Only control lists created from the same ControlIdMap or ControlInfoMap may
  * be merged. Attempting to do otherwise results in undefined behaviour.
@@ -921,7 +935,7 @@
  * \todo Reimplement or implement an overloaded version which internally uses
  * std::unordered_map::merge() and accepts a non-const argument.
  */
-void ControlList::merge(const ControlList &source)
+void ControlList::merge(const ControlList &source, MergePolicy policy)
 {
 	/**
 	 * \todo ASSERT that the current and source ControlList are derived
@@ -936,7 +950,7 @@
 	 */
 
 	for (const auto &ctrl : source) {
-		if (contains(ctrl.first)) {
+		if (policy == MergePolicy::KeepExisting && contains(ctrl.first)) {
 			const ControlId *id = idmap_->at(ctrl.first);
 			LOG(Controls, Warning)
 				<< "Control " << id->name() << " not overwritten";
diff --git a/src/libcamera/converter.cpp b/src/libcamera/converter.cpp
index fa0f1ec..d3d38c1 100644
--- a/src/libcamera/converter.cpp
+++ b/src/libcamera/converter.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright 2022 NXP
  *
- * converter.cpp - Generic format converter interface
+ * Generic format converter interface
  */
 
 #include "libcamera/internal/converter.h"
@@ -13,8 +13,6 @@
 
 #include "libcamera/internal/media_device.h"
 
-#include "linux/media.h"
-
 /**
  * \file internal/converter.h
  * \brief Abstract converter
@@ -199,16 +197,18 @@
 
 /**
  * \fn ConverterFactoryBase::compatibles()
- * \return The names compatibles
+ * \return The list of compatible name aliases of the converter
  */
 
 /**
- * \brief Create an instance of the converter corresponding to a named factory
- * \param[in] media Name of the factory
+ * \brief Create an instance of the converter corresponding to the media device
+ * \param[in] media The media device to create the converter for
  *
- * \return A unique pointer to a new instance of the converter subclass
- * corresponding to the named factory or one of its alias. Otherwise a null
- * pointer if no such factory exists
+ * The converter is created by matching the factory name or any of its
+ * compatible aliases with the media device driver name.
+ *
+ * \return A new instance of the converter subclass corresponding to the media
+ * device, or null if the media device driver name doesn't match anything
  */
 std::unique_ptr<Converter> ConverterFactoryBase::create(MediaDevice *media)
 {
@@ -236,10 +236,11 @@
 }
 
 /**
- * \brief Add a converter class to the registry
+ * \brief Add a converter factory to the registry
  * \param[in] factory Factory to use to construct the converter class
  *
- * The caller is responsible to guarantee the uniqueness of the converter name.
+ * The caller is responsible to guarantee the uniqueness of the converter
+ * factory name.
  */
 void ConverterFactoryBase::registerType(ConverterFactoryBase *factory)
 {
diff --git a/src/libcamera/converter/converter_v4l2_m2m.cpp b/src/libcamera/converter/converter_v4l2_m2m.cpp
index 2a4d1d9..d8929fc 100644
--- a/src/libcamera/converter/converter_v4l2_m2m.cpp
+++ b/src/libcamera/converter/converter_v4l2_m2m.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Laurent Pinchart
  * Copyright 2022 NXP
  *
- * converter_v4l2_m2m.cpp - V4L2 M2M Format converter
+ * V4L2 M2M Format converter
  */
 
 #include "libcamera/internal/converter/converter_v4l2_m2m.h"
@@ -445,6 +445,7 @@
 }
 
 static std::initializer_list<std::string> compatibles = {
+	"mtk-mdp",
 	"pxp",
 };
 
diff --git a/src/libcamera/delayed_controls.cpp b/src/libcamera/delayed_controls.cpp
index 777441e..94d0a57 100644
--- a/src/libcamera/delayed_controls.cpp
+++ b/src/libcamera/delayed_controls.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * delayed_controls.h - Helper to deal with controls that take effect with a delay
+ * Helper to deal with controls that take effect with a delay
  */
 
 #include "libcamera/internal/delayed_controls.h"
diff --git a/src/libcamera/device_enumerator.cpp b/src/libcamera/device_enumerator.cpp
index f2e055d..ae17862 100644
--- a/src/libcamera/device_enumerator.cpp
+++ b/src/libcamera/device_enumerator.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * device_enumerator.cpp - Enumeration and matching
+ * Enumeration and matching
  */
 
 #include "libcamera/internal/device_enumerator.h"
@@ -56,7 +56,7 @@
  * names can be added as match criteria.
  *
  * Pipeline handlers are recommended to add entities to DeviceMatch as
- * appropriare to ensure that the media device they need can be uniquely
+ * appropriate to ensure that the media device they need can be uniquely
  * identified. This is useful when the corresponding kernel driver can produce
  * different graphs, for instance as a result of different driver versions or
  * hardware configurations, and not all those graphs are suitable for a pipeline
@@ -101,8 +101,14 @@
 
 		for (const MediaEntity *entity : device->entities()) {
 			if (name == entity->name()) {
-				found = true;
-				break;
+				if (!entity->deviceNode().empty()) {
+					found = true;
+					break;
+				} else {
+					LOG(DeviceEnumerator, Debug)
+						<< "Skip " << entity->name()
+						<< ": no device node";
+				}
 			}
 		}
 
diff --git a/src/libcamera/device_enumerator_sysfs.cpp b/src/libcamera/device_enumerator_sysfs.cpp
index 686bb80..7866885 100644
--- a/src/libcamera/device_enumerator_sysfs.cpp
+++ b/src/libcamera/device_enumerator_sysfs.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * device_enumerator_sysfs.cpp - sysfs-based device enumerator
+ * sysfs-based device enumerator
  */
 
 #include "libcamera/internal/device_enumerator_sysfs.h"
@@ -33,7 +33,7 @@
 int DeviceEnumeratorSysfs::enumerate()
 {
 	struct dirent *ent;
-	DIR *dir;
+	DIR *dir = nullptr;
 
 	static const char * const sysfs_dirs[] = {
 		"/sys/subsystem/media/devices",
diff --git a/src/libcamera/device_enumerator_udev.cpp b/src/libcamera/device_enumerator_udev.cpp
index 0abc124..01c70b6 100644
--- a/src/libcamera/device_enumerator_udev.cpp
+++ b/src/libcamera/device_enumerator_udev.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018-2019, Google Inc.
  *
- * device_enumerator_udev.cpp - udev-based device enumerator
+ * udev-based device enumerator
  */
 
 #include "libcamera/internal/device_enumerator_udev.h"
diff --git a/src/libcamera/dma_buf_allocator.cpp b/src/libcamera/dma_buf_allocator.cpp
new file mode 100644
index 0000000..c06eca7
--- /dev/null
+++ b/src/libcamera/dma_buf_allocator.cpp
@@ -0,0 +1,246 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Red Hat Inc.
+ * Copyright (C) 2020, Raspberry Pi Ltd
+ *
+ * Helper class for dma-buf allocations.
+ */
+
+#include "libcamera/internal/dma_buf_allocator.h"
+
+#include <array>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <linux/dma-buf.h>
+#include <linux/dma-heap.h>
+#include <linux/udmabuf.h>
+
+#include <libcamera/base/log.h>
+
+/**
+ * \file dma_buf_allocator.cpp
+ * \brief dma-buf allocator
+ */
+
+namespace libcamera {
+
+#ifndef __DOXYGEN__
+struct DmaBufAllocatorInfo {
+	DmaBufAllocator::DmaBufAllocatorFlag type;
+	const char *deviceNodeName;
+};
+#endif
+
+static constexpr std::array<DmaBufAllocatorInfo, 4> providerInfos = { {
+	/*
+	 * /dev/dma_heap/linux,cma is the CMA dma-heap. When the cma heap size is
+	 * specified on the kernel command line, this gets renamed to "reserved".
+	 */
+	{ DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap, "/dev/dma_heap/linux,cma" },
+	{ DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap, "/dev/dma_heap/reserved" },
+	{ DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap, "/dev/dma_heap/system" },
+	{ DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf, "/dev/udmabuf" },
+} };
+
+LOG_DEFINE_CATEGORY(DmaBufAllocator)
+
+/**
+ * \class DmaBufAllocator
+ * \brief Helper class for dma-buf allocations
+ *
+ * This class wraps a userspace dma-buf provider selected at construction time,
+ * and exposes functions to allocate dma-buffers from this provider.
+ *
+ * Different providers may provide dma-buffers with different properties for
+ * the underlying memory. Which providers are acceptable is specified through
+ * the type argument passed to the DmaBufAllocator() constructor.
+ */
+
+/**
+ * \enum DmaBufAllocator::DmaBufAllocatorFlag
+ * \brief Type of the dma-buf provider
+ * \var DmaBufAllocator::CmaHeap
+ * \brief Allocate from a CMA dma-heap, providing physically-contiguous memory
+ * \var DmaBufAllocator::SystemHeap
+ * \brief Allocate from the system dma-heap, using the page allocator
+ * \var DmaBufAllocator::UDmaBuf
+ * \brief Allocate using a memfd + /dev/udmabuf
+ */
+
+/**
+ * \typedef DmaBufAllocator::DmaBufAllocatorFlags
+ * \brief A bitwise combination of DmaBufAllocator::DmaBufAllocatorFlag values
+ */
+
+/**
+ * \brief Construct a DmaBufAllocator of a given type
+ * \param[in] type The type(s) of the dma-buf providers to allocate from
+ *
+ * The dma-buf provider type is selected with the \a type parameter, which
+ * defaults to the CMA heap. If no provider of the given type can be accessed,
+ * the constructed DmaBufAllocator instance is invalid as indicated by
+ * the isValid() function.
+ *
+ * Multiple types can be selected by combining type flags, in which case
+ * the constructed DmaBufAllocator will match one of the types. If multiple
+ * requested types can work on the system, which provider is used is undefined.
+ */
+DmaBufAllocator::DmaBufAllocator(DmaBufAllocatorFlags type)
+{
+	for (const auto &info : providerInfos) {
+		if (!(type & info.type))
+			continue;
+
+		int ret = ::open(info.deviceNodeName, O_RDWR | O_CLOEXEC, 0);
+		if (ret < 0) {
+			ret = errno;
+			LOG(DmaBufAllocator, Debug)
+				<< "Failed to open " << info.deviceNodeName << ": "
+				<< strerror(ret);
+			continue;
+		}
+
+		LOG(DmaBufAllocator, Debug) << "Using " << info.deviceNodeName;
+		providerHandle_ = UniqueFD(ret);
+		type_ = info.type;
+		break;
+	}
+
+	if (!providerHandle_.isValid())
+		LOG(DmaBufAllocator, Error) << "Could not open any dma-buf provider";
+}
+
+/**
+ * \brief Destroy the DmaBufAllocator instance
+ */
+DmaBufAllocator::~DmaBufAllocator() = default;
+
+/**
+ * \fn DmaBufAllocator::isValid()
+ * \brief Check if the DmaBufAllocator instance is valid
+ * \return True if the DmaBufAllocator is valid, false otherwise
+ */
+
+/* uClibc doesn't provide the file sealing API. */
+#ifndef __DOXYGEN__
+#if not HAVE_FILE_SEALS
+#define F_ADD_SEALS		1033
+#define F_SEAL_SHRINK		0x0002
+#endif
+#endif
+
+UniqueFD DmaBufAllocator::allocFromUDmaBuf(const char *name, std::size_t size)
+{
+	/* Size must be a multiple of the page size. Round it up. */
+	std::size_t pageMask = sysconf(_SC_PAGESIZE) - 1;
+	size = (size + pageMask) & ~pageMask;
+
+#if HAVE_MEMFD_CREATE
+	int ret = memfd_create(name, MFD_ALLOW_SEALING | MFD_CLOEXEC);
+#else
+	int ret = syscall(SYS_memfd_create, name, MFD_ALLOW_SEALING | MFD_CLOEXEC);
+#endif
+	if (ret < 0) {
+		ret = errno;
+		LOG(DmaBufAllocator, Error)
+			<< "Failed to allocate memfd storage for " << name
+			<< ": " << strerror(ret);
+		return {};
+	}
+
+	UniqueFD memfd(ret);
+
+	ret = ftruncate(memfd.get(), size);
+	if (ret < 0) {
+		ret = errno;
+		LOG(DmaBufAllocator, Error)
+			<< "Failed to set memfd size for " << name
+			<< ": " << strerror(ret);
+		return {};
+	}
+
+	/* udmabuf dma-buffers *must* have the F_SEAL_SHRINK seal. */
+	ret = fcntl(memfd.get(), F_ADD_SEALS, F_SEAL_SHRINK);
+	if (ret < 0) {
+		ret = errno;
+		LOG(DmaBufAllocator, Error)
+			<< "Failed to seal the memfd for " << name
+			<< ": " << strerror(ret);
+		return {};
+	}
+
+	struct udmabuf_create create;
+
+	create.memfd = memfd.get();
+	create.flags = UDMABUF_FLAGS_CLOEXEC;
+	create.offset = 0;
+	create.size = size;
+
+	ret = ::ioctl(providerHandle_.get(), UDMABUF_CREATE, &create);
+	if (ret < 0) {
+		ret = errno;
+		LOG(DmaBufAllocator, Error)
+			<< "Failed to create dma buf for " << name
+			<< ": " << strerror(ret);
+		return {};
+	}
+
+	/* The underlying memfd is kept as as a reference in the kernel. */
+	return UniqueFD(ret);
+}
+
+UniqueFD DmaBufAllocator::allocFromHeap(const char *name, std::size_t size)
+{
+	struct dma_heap_allocation_data alloc = {};
+	int ret;
+
+	alloc.len = size;
+	alloc.fd_flags = O_CLOEXEC | O_RDWR;
+
+	ret = ::ioctl(providerHandle_.get(), DMA_HEAP_IOCTL_ALLOC, &alloc);
+	if (ret < 0) {
+		LOG(DmaBufAllocator, Error)
+			<< "dma-heap allocation failure for " << name;
+		return {};
+	}
+
+	UniqueFD allocFd(alloc.fd);
+	ret = ::ioctl(allocFd.get(), DMA_BUF_SET_NAME, name);
+	if (ret < 0) {
+		LOG(DmaBufAllocator, Error)
+			<< "dma-heap naming failure for " << name;
+		return {};
+	}
+
+	return allocFd;
+}
+
+/**
+ * \brief Allocate a dma-buf from the DmaBufAllocator
+ * \param [in] name The name to set for the allocated buffer
+ * \param [in] size The size of the buffer to allocate
+ *
+ * Allocates a dma-buf with read/write access.
+ *
+ * If the allocation fails, return an invalid UniqueFD.
+ *
+ * \return The UniqueFD of the allocated buffer
+ */
+UniqueFD DmaBufAllocator::alloc(const char *name, std::size_t size)
+{
+	if (!name)
+		return {};
+
+	if (type_ == DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf)
+		return allocFromUDmaBuf(name, size);
+	else
+		return allocFromHeap(name, size);
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/fence.cpp b/src/libcamera/fence.cpp
index 7b78477..634c74f 100644
--- a/src/libcamera/fence.cpp
+++ b/src/libcamera/fence.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * fence.cpp - Synchronization fence
+ * Synchronization fence
  */
 
 #include "libcamera/fence.h"
diff --git a/src/libcamera/formats.cpp b/src/libcamera/formats.cpp
index 447e623..cf41f2c 100644
--- a/src/libcamera/formats.cpp
+++ b/src/libcamera/formats.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * formats.cpp - libcamera image formats
+ * libcamera image formats
  */
 
 #include "libcamera/internal/formats.h"
@@ -33,7 +33,7 @@
  * used in pipeline handlers.
  *
  * \var PixelFormatInfo::name
- * \brief The format name as a human-readable string, used as the test
+ * \brief The format name as a human-readable string, used as the text
  * representation of the PixelFormat
  *
  * \var PixelFormatInfo::format
@@ -49,9 +49,9 @@
  * \var PixelFormatInfo::bitsPerPixel
  * \brief The average number of bits per pixel
  *
- * The number per pixel averages the total number of bits for all colour
- * components over the whole image, excluding any padding bits or padding
- * pixels.
+ * The number of bits per pixel averages the total number of bits for all
+ * colour components over the whole image, excluding any padding bits or
+ * padding pixels.
  *
  * For formats that store pixels with bit padding within words, only the
  * effective bits are taken into account. For instance, 12-bit Bayer data
@@ -270,6 +270,26 @@
 		.pixelsPerGroup = 1,
 		.planes = {{ { 4, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
+	{ formats::BGR161616, {
+		.name = "BGR161616",
+		.format = formats::BGR161616,
+		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_RGB48), },
+		.bitsPerPixel = 48,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 3, 1 }, { 0, 0 }, { 0, 0 } }},
+	} },
+	{ formats::RGB161616, {
+		.name = "RGB161616",
+		.format = formats::RGB161616,
+		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_BGR48), },
+		.bitsPerPixel = 48,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 3, 1 }, { 0, 0 }, { 0, 0 } }},
+	} },
 
 	/* YUV packed formats. */
 	{ formats::YUYV, {
@@ -497,6 +517,16 @@
 		.pixelsPerGroup = 1,
 		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
+	{ formats::R10_CSI2P, {
+		.name = "R10_CSI2P",
+		.format = formats::R10_CSI2P,
+		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_Y10P), },
+		.bitsPerPixel = 10,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+		.packed = true,
+		.pixelsPerGroup = 4,
+		.planes = {{ { 5, 1 }, { 0, 0 }, { 0, 0 } }},
+	} },
 	{ formats::R12, {
 		.name = "R12",
 		.format = formats::R12,
@@ -507,15 +537,25 @@
 		.pixelsPerGroup = 1,
 		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
-	{ formats::R10_CSI2P, {
-		.name = "R10_CSI2P",
-		.format = formats::R10_CSI2P,
-		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_Y10P), },
-		.bitsPerPixel = 10,
+	{ formats::R16, {
+		.name = "R16",
+		.format = formats::R16,
+		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_Y16), },
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+		.packed = false,
+		.pixelsPerGroup = 1,
+		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
+	} },
+	{ formats::MONO_PISP_COMP1, {
+		.name = "MONO_PISP_COMP1",
+		.format = formats::MONO_PISP_COMP1,
+		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_MONO), },
+		.bitsPerPixel = 8,
 		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
 		.packed = true,
-		.pixelsPerGroup = 4,
-		.planes = {{ { 5, 1 }, { 0, 0 }, { 0, 0 } }},
+		.pixelsPerGroup = 1,
+		.planes = {{ { 1, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
 
 	/* Bayer formats. */
@@ -880,7 +920,46 @@
 		.pixelsPerGroup = 25,
 		.planes = {{ { 32, 1 }, { 0, 0 }, { 0, 0 } }},
 	} },
-
+	{ formats::BGGR_PISP_COMP1, {
+		.name = "BGGR_PISP_COMP1",
+		.format = formats::BGGR_PISP_COMP1,
+		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_BGGR), },
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+		.packed = true,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
+	} },
+	{ formats::GBRG_PISP_COMP1, {
+		.name = "GBRG_PISP_COMP1",
+		.format = formats::GBRG_PISP_COMP1,
+		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GBRG), },
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+		.packed = true,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
+	} },
+	{ formats::GRBG_PISP_COMP1, {
+		.name = "GRBG_PISP_COMP1",
+		.format = formats::GRBG_PISP_COMP1,
+		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GRBG), },
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+		.packed = true,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
+	} },
+	{ formats::RGGB_PISP_COMP1, {
+		.name = "RGGB_PISP_COMP1",
+		.format = formats::RGGB_PISP_COMP1,
+		.v4l2Formats = { V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_RGGB), },
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+		.packed = true,
+		.pixelsPerGroup = 2,
+		.planes = {{ { 2, 1 }, { 0, 0 }, { 0, 0 } }},
+	} },
 	/* Compressed formats. */
 	{ formats::MJPEG, {
 		.name = "MJPEG",
@@ -987,7 +1066,7 @@
 		return 0;
 	}
 
-	if (plane > planes.size() || !planes[plane].bytesPerGroup) {
+	if (plane >= planes.size() || !planes[plane].bytesPerGroup) {
 		LOG(Formats, Warning) << "Invalid plane index, stride is zero";
 		return 0;
 	}
diff --git a/src/libcamera/formats.yaml b/src/libcamera/formats.yaml
index 539ac0b..fe027a7 100644
--- a/src/libcamera/formats.yaml
+++ b/src/libcamera/formats.yaml
@@ -11,6 +11,8 @@
       fourcc: DRM_FORMAT_R10
   - R12:
       fourcc: DRM_FORMAT_R12
+  - R16:
+      fourcc: DRM_FORMAT_R16
 
   - RGB565:
       fourcc: DRM_FORMAT_RGB565
@@ -41,6 +43,11 @@
   - BGRA8888:
       fourcc: DRM_FORMAT_BGRA8888
 
+  - RGB161616:
+      fourcc: DRM_FORMAT_RGB161616
+  - BGR161616:
+      fourcc: DRM_FORMAT_BGR161616
+
   - YUYV:
       fourcc: DRM_FORMAT_YUYV
   - YVYU:
@@ -183,4 +190,20 @@
   - SBGGR10_IPU3:
       fourcc: DRM_FORMAT_SBGGR10
       mod: IPU3_FORMAT_MOD_PACKED
+
+  - RGGB_PISP_COMP1:
+      fourcc: DRM_FORMAT_SRGGB16
+      mod: PISP_FORMAT_MOD_COMPRESS_MODE1
+  - GRBG_PISP_COMP1:
+      fourcc: DRM_FORMAT_SGRBG16
+      mod: PISP_FORMAT_MOD_COMPRESS_MODE1
+  - GBRG_PISP_COMP1:
+      fourcc: DRM_FORMAT_SGBRG16
+      mod: PISP_FORMAT_MOD_COMPRESS_MODE1
+  - BGGR_PISP_COMP1:
+      fourcc: DRM_FORMAT_SBGGR16
+      mod: PISP_FORMAT_MOD_COMPRESS_MODE1
+  - MONO_PISP_COMP1:
+      fourcc: DRM_FORMAT_R16
+      mod: PISP_FORMAT_MOD_COMPRESS_MODE1
 ...
diff --git a/src/libcamera/framebuffer.cpp b/src/libcamera/framebuffer.cpp
index 5a7f3c0..63d679c 100644
--- a/src/libcamera/framebuffer.cpp
+++ b/src/libcamera/framebuffer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * framebuffer.cpp - Frame buffer handling
+ * Frame buffer handling
  */
 
 #include <libcamera/framebuffer.h>
diff --git a/src/libcamera/framebuffer_allocator.cpp b/src/libcamera/framebuffer_allocator.cpp
index dabd921..3d53bde 100644
--- a/src/libcamera/framebuffer_allocator.cpp
+++ b/src/libcamera/framebuffer_allocator.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * framebuffer_allocator.cpp - FrameBuffer allocator
+ * FrameBuffer allocator
  */
 
 #include <libcamera/framebuffer_allocator.h>
@@ -59,14 +59,11 @@
  * \param[in] camera The camera
  */
 FrameBufferAllocator::FrameBufferAllocator(std::shared_ptr<Camera> camera)
-	: camera_(camera)
+	: camera_(std::move(camera))
 {
 }
 
-FrameBufferAllocator::~FrameBufferAllocator()
-{
-	buffers_.clear();
-}
+FrameBufferAllocator::~FrameBufferAllocator() = default;
 
 /**
  * \brief Allocate buffers for a configured stream
@@ -100,6 +97,10 @@
 		LOG(Allocator, Error)
 			<< "Stream is not part of " << camera_->id()
 			<< " active configuration";
+
+	if (ret < 0)
+		buffers_.erase(it);
+
 	return ret;
 }
 
@@ -121,8 +122,6 @@
 	if (iter == buffers_.end())
 		return -EINVAL;
 
-	std::vector<std::unique_ptr<FrameBuffer>> &buffers = iter->second;
-	buffers.clear();
 	buffers_.erase(iter);
 
 	return 0;
diff --git a/src/libcamera/geometry.cpp b/src/libcamera/geometry.cpp
index e50b46c..0001513 100644
--- a/src/libcamera/geometry.cpp
+++ b/src/libcamera/geometry.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * geometry.cpp - Geometry-related structures
+ * Geometry-related structures
  */
 
 #include <libcamera/geometry.h>
@@ -95,10 +95,10 @@
 }
 
 /**
- * \struct Size
+ * \class Size
  * \brief Describe a two-dimensional size
  *
- * The Size structure defines a two-dimensional size with integer precision.
+ * The Size class defines a two-dimensional size with integer precision.
  */
 
 /**
@@ -455,7 +455,7 @@
 }
 
 /**
- * \struct SizeRange
+ * \class SizeRange
  * \brief Describe a range of sizes
  *
  * A SizeRange describes a range of sizes included in the [min, max] interval
@@ -589,7 +589,7 @@
 }
 
 /**
- * \struct Rectangle
+ * \class Rectangle
  * \brief Describe a rectangle's position and dimensions
  *
  * Rectangles are used to identify an area of an image. They are specified by
diff --git a/src/libcamera/ipa/meson.build b/src/libcamera/ipa/meson.build
index 4469524..ef73b3f 100644
--- a/src/libcamera/ipa/meson.build
+++ b/src/libcamera/ipa/meson.build
@@ -3,13 +3,10 @@
 libcamera_ipa_interfaces = []
 
 foreach file : ipa_mojom_files
-    name = '@0@'.format(file).split('/')[-1].split('.')[0]
-
     # {pipeline}_ipa_interface.cpp
     libcamera_ipa_interfaces += \
-        custom_target(name + '_ipa_interface_cpp',
-                      input : file,
-                      output : name + '_ipa_interface.cpp',
+        custom_target(input : file,
+                      output : '@BASENAME@_ipa_interface.cpp',
                       command : [
                           mojom_docs_extractor,
                           '-o', '@OUTPUT@', '@INPUT@'
diff --git a/src/libcamera/ipa_controls.cpp b/src/libcamera/ipa_controls.cpp
index 870a443..9420c88 100644
--- a/src/libcamera/ipa_controls.cpp
+++ b/src/libcamera/ipa_controls.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_controls.cpp - IPA control handling
+ * IPA control handling
  */
 
 #include <libcamera/ipa/ipa_controls.h>
diff --git a/src/libcamera/ipa_data_serializer.cpp b/src/libcamera/ipa_data_serializer.cpp
index 0a25930..3e9bef0 100644
--- a/src/libcamera/ipa_data_serializer.cpp
+++ b/src/libcamera/ipa_data_serializer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * ipa_data_serializer.cpp - Image Processing Algorithm data serializer
+ * Image Processing Algorithm data serializer
  */
 
 #include "libcamera/internal/ipa_data_serializer.h"
diff --git a/src/libcamera/ipa_interface.cpp b/src/libcamera/ipa_interface.cpp
index 8ea6cbe..a9dc54a 100644
--- a/src/libcamera/ipa_interface.cpp
+++ b/src/libcamera/ipa_interface.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_interface.cpp - Image Processing Algorithm interface
+ * Image Processing Algorithm interface
  */
 
 #include <libcamera/ipa/ipa_interface.h>
diff --git a/src/libcamera/ipa_manager.cpp b/src/libcamera/ipa_manager.cpp
index 7a4515d..f4e0b63 100644
--- a/src/libcamera/ipa_manager.cpp
+++ b/src/libcamera/ipa_manager.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_manager.cpp - Image Processing Algorithm module manager
+ * Image Processing Algorithm module manager
  */
 
 #include "libcamera/internal/ipa_manager.h"
diff --git a/src/libcamera/ipa_module.cpp b/src/libcamera/ipa_module.cpp
index f2dd87e..0756b69 100644
--- a/src/libcamera/ipa_module.cpp
+++ b/src/libcamera/ipa_module.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_module.cpp - Image Processing Algorithm module
+ * Image Processing Algorithm module
  */
 
 #include "libcamera/internal/ipa_module.h"
diff --git a/src/libcamera/ipa_proxy.cpp b/src/libcamera/ipa_proxy.cpp
index 3f2cc6b..6c17c45 100644
--- a/src/libcamera/ipa_proxy.cpp
+++ b/src/libcamera/ipa_proxy.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_proxy.cpp - Image Processing Algorithm proxy
+ * Image Processing Algorithm proxy
  */
 
 #include "libcamera/internal/ipa_proxy.h"
diff --git a/src/libcamera/ipa_pub_key.cpp.in b/src/libcamera/ipa_pub_key.cpp.in
index 01e5333..5d8c92c 100644
--- a/src/libcamera/ipa_pub_key.cpp.in
+++ b/src/libcamera/ipa_pub_key.cpp.in
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Laurent Pinchart <laurent.pinchart@ideasonboard.com>
  *
- * ipa_pub_key.cpp - IPA module signing public key
+ * IPA module signing public key
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/src/libcamera/ipc_pipe.cpp b/src/libcamera/ipc_pipe.cpp
index 31a0ca0..548299d 100644
--- a/src/libcamera/ipc_pipe.cpp
+++ b/src/libcamera/ipc_pipe.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * ipc_pipe.cpp - Image Processing Algorithm IPC module for IPA proxies
+ * Image Processing Algorithm IPC module for IPA proxies
  */
 
 #include "libcamera/internal/ipc_pipe.h"
diff --git a/src/libcamera/ipc_pipe_unixsocket.cpp b/src/libcamera/ipc_pipe_unixsocket.cpp
index da2cffc..668ec73 100644
--- a/src/libcamera/ipc_pipe_unixsocket.cpp
+++ b/src/libcamera/ipc_pipe_unixsocket.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * ipc_pipe_unixsocket.cpp - Image Processing Algorithm IPC module using unix socket
+ * Image Processing Algorithm IPC module using unix socket
  */
 
 #include "libcamera/internal/ipc_pipe_unixsocket.h"
diff --git a/src/libcamera/ipc_unixsocket.cpp b/src/libcamera/ipc_unixsocket.cpp
index 1980d37..75285b6 100644
--- a/src/libcamera/ipc_unixsocket.cpp
+++ b/src/libcamera/ipc_unixsocket.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipc_unixsocket.cpp - IPC mechanism based on Unix sockets
+ * IPC mechanism based on Unix sockets
  */
 
 #include "libcamera/internal/ipc_unixsocket.h"
diff --git a/src/libcamera/mapped_framebuffer.cpp b/src/libcamera/mapped_framebuffer.cpp
index 6860069..b3104e0 100644
--- a/src/libcamera/mapped_framebuffer.cpp
+++ b/src/libcamera/mapped_framebuffer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * mapped_framebuffer.cpp - Mapped Framebuffer support
+ * Mapped Framebuffer support
  */
 
 #include "libcamera/internal/mapped_framebuffer.h"
diff --git a/src/libcamera/media_device.cpp b/src/libcamera/media_device.cpp
index 2949816..bd05455 100644
--- a/src/libcamera/media_device.cpp
+++ b/src/libcamera/media_device.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * media_device.cpp - Media device handler
+ * Media device handler
  */
 
 #include "libcamera/internal/media_device.h"
diff --git a/src/libcamera/media_object.cpp b/src/libcamera/media_object.cpp
index c78f475..1b191a1 100644
--- a/src/libcamera/media_object.cpp
+++ b/src/libcamera/media_object.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * media_object.cpp - Media device objects: entities, pads and links
+ * Media device objects: entities, pads and links
  */
 
 #include "libcamera/internal/media_object.h"
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index b24f829..89504ce 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -7,8 +7,6 @@
     'camera_controls.cpp',
     'camera_lens.cpp',
     'camera_manager.cpp',
-    'camera_sensor.cpp',
-    'camera_sensor_properties.cpp',
     'color_space.cpp',
     'controls.cpp',
     'control_serializer.cpp',
@@ -17,6 +15,7 @@
     'delayed_controls.cpp',
     'device_enumerator.cpp',
     'device_enumerator_sysfs.cpp',
+    'dma_buf_allocator.cpp',
     'fence.cpp',
     'formats.cpp',
     'framebuffer.cpp',
@@ -34,11 +33,13 @@
     'mapped_framebuffer.cpp',
     'media_device.cpp',
     'media_object.cpp',
+    'orientation.cpp',
     'pipeline_handler.cpp',
     'pixel_format.cpp',
     'process.cpp',
     'pub_key.cpp',
     'request.cpp',
+    'shared_mem_object.cpp',
     'source_paths.cpp',
     'stream.cpp',
     'sysfs.cpp',
@@ -68,6 +69,8 @@
 subdir('ipa')
 subdir('pipeline')
 subdir('proxy')
+subdir('sensor')
+subdir('software_isp')
 
 null_dep = dependency('', required : false)
 
@@ -126,12 +129,27 @@
 
 control_sources = []
 
-foreach source : control_source_files
-    input_files = files(source +'.yaml', source + '.cpp.in')
-    control_sources += custom_target(source + '_cpp',
+controls_mode_files = {
+    'controls' : controls_files,
+    'properties' : properties_files,
+}
+
+foreach mode, input_files : controls_mode_files
+    input_files = files(input_files)
+
+    if mode == 'controls'
+        template_file = files('control_ids.cpp.in')
+    else
+        template_file = files('property_ids.cpp.in')
+    endif
+
+    ranges_file = files('control_ranges.yaml')
+    control_sources += custom_target(mode + '_cpp',
                                      input : input_files,
-                                     output : source + '.cpp',
-                                     command : [gen_controls, '-o', '@OUTPUT@', '@INPUT@'])
+                                     output : mode + '_ids.cpp',
+                                     command : [gen_controls, '-o', '@OUTPUT@',
+                                                '--mode', mode, '-t', template_file,
+                                                '-r', ranges_file, '@INPUT@'])
 endforeach
 
 libcamera_sources += control_sources
diff --git a/src/libcamera/orientation.cpp b/src/libcamera/orientation.cpp
new file mode 100644
index 0000000..47fd6a3
--- /dev/null
+++ b/src/libcamera/orientation.cpp
@@ -0,0 +1,115 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Ideas On Board Oy
+ *
+ * Image orientation
+ */
+
+#include <libcamera/orientation.h>
+
+#include <array>
+#include <string>
+
+/**
+ * \file libcamera/orientation.h
+ * \brief Image orientation definition
+ */
+
+namespace libcamera {
+
+/**
+ * \enum Orientation
+ * \brief The image orientation in a memory buffer
+ *
+ * The Orientation enumeration describes the orientation of the images
+ * produced by the camera pipeline as they get received by the application
+ * inside memory buffers.
+ *
+ * The image orientation expressed using the Orientation enumeration can be then
+ * inferred by applying to a naturally oriented image a multiple of a 90 degrees
+ * rotation in the clockwise direction from the origin and then by applying an
+ * optional horizontal mirroring.
+ *
+ * The enumeration numerical values follow the ones defined by the EXIF
+ * Specification version 2.32, Tag 274 "Orientation", while the names of the
+ * enumerated values report the rotation and mirroring operations performed.
+ *
+ * For example, Orientation::Rotate90Mirror describes the orientation obtained
+ * by rotating the image 90 degrees clockwise first and then applying a
+ * horizontal mirroring.
+ *
+ * \var CameraConfiguration::Rotate0
+ * \image html rotation/rotate0.svg
+ * \var CameraConfiguration::Rotate0Mirror
+ * \image html rotation/rotate0Mirror.svg
+ * \var CameraConfiguration::Rotate180
+ * \image html rotation/rotate180.svg
+ * \var CameraConfiguration::Rotate180Mirror
+ * \image html rotation/rotate180Mirror.svg
+ * \var CameraConfiguration::Rotate90Mirror
+ * \image html rotation/rotate90Mirror.svg
+ * \var CameraConfiguration::Rotate270
+ * \image html rotation/rotate270.svg
+ * \var CameraConfiguration::Rotate270Mirror
+ * \image html rotation/rotate270Mirror.svg
+ * \var CameraConfiguration::Rotate90
+ * \image html rotation/rotate90.svg
+ */
+
+/**
+ * \brief Return the orientation representing a rotation of the given angle
+ * clockwise
+ * \param[in] angle The angle of rotation in a clockwise sense. Negative values
+ * can be used to represent anticlockwise rotations
+ * \param[out] success Set to `true` if the angle is a multiple of 90 degrees,
+ * otherwise `false`
+ * \return The orientation corresponding to the rotation if \a success was set
+ * to `true`, otherwise the `Rotate0` orientation
+ */
+Orientation orientationFromRotation(int angle, bool *success)
+{
+	angle = angle % 360;
+	if (angle < 0)
+		angle += 360;
+
+	if (success != nullptr)
+		*success = true;
+
+	switch (angle) {
+	case 0:
+		return Orientation::Rotate0;
+	case 90:
+		return Orientation::Rotate90;
+	case 180:
+		return Orientation::Rotate180;
+	case 270:
+		return Orientation::Rotate270;
+	}
+
+	if (success != nullptr)
+		*success = false;
+
+	return Orientation::Rotate0;
+}
+
+/**
+ * \brief Prints human-friendly names for Orientation items
+ * \param[in] out The output stream
+ * \param[in] orientation The Orientation item
+ * \return The output stream \a out
+ */
+std::ostream &operator<<(std::ostream &out, const Orientation &orientation)
+{
+	constexpr std::array<const char *, 9> orientationNames = {
+		"", /* Orientation starts counting from 1. */
+		"Rotate0", "Rotate0Mirror",
+		"Rotate180", "Rotate180Mirror",
+		"Rotate90Mirror", "Rotate270",
+		"Rotate270Mirror", "Rotate90",
+	};
+
+	out << orientationNames[static_cast<unsigned int>(orientation)];
+	return out;
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp b/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
index 9bdfff0..72aa6c7 100644
--- a/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
+++ b/src/libcamera/pipeline/imx8-isi/imx8-isi.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022 - Jacopo Mondi <jacopo@jmondi.org>
  *
- * imx8-isi.cpp - Pipeline handler for ISI interface found on NXP i.MX8 SoC
+ * Pipeline handler for ISI interface found on NXP i.MX8 SoC
  */
 
 #include <algorithm>
@@ -554,7 +554,7 @@
 	PixelFormat pixelFormat = config_[0].pixelFormat;
 
 	V4L2SubdeviceFormat sensorFormat{};
-	sensorFormat.mbus_code = data_->getMediaBusFormat(&pixelFormat);
+	sensorFormat.code = data_->getMediaBusFormat(&pixelFormat);
 	sensorFormat.size = maxSize;
 
 	LOG(ISI, Debug) << "Computed sensor configuration: " << sensorFormat;
@@ -569,7 +569,7 @@
 	 * the smallest larger format without considering the aspect ratio
 	 * as the ISI can freely scale.
 	 */
-	auto sizes = sensor->sizes(sensorFormat.mbus_code);
+	auto sizes = sensor->sizes(sensorFormat.code);
 	Size bestSize;
 
 	for (const Size &s : sizes) {
@@ -595,7 +595,7 @@
 		return Invalid;
 	}
 
-	sensorFormat_.mbus_code = sensorFormat.mbus_code;
+	sensorFormat_.code = sensorFormat.code;
 	sensorFormat_.size = bestSize;
 
 	LOG(ISI, Debug) << "Selected sensor format: " << sensorFormat_;
@@ -632,7 +632,7 @@
 
 	/* Adjust the requested size to the sensor's capabilities. */
 	V4L2SubdeviceFormat sensorFmt;
-	sensorFmt.mbus_code = mbusCode;
+	sensorFmt.code = mbusCode;
 	sensorFmt.size = size;
 
 	int ret = data->sensor_->tryFormat(&sensorFmt);
@@ -827,16 +827,10 @@
 	unsigned int xbarFirstSource = crossbar_->entity()->pads().size() / 2 + 1;
 
 	for (const auto &[idx, config] : utils::enumerate(*c)) {
-		struct v4l2_subdev_route route = {
-			.sink_pad = data->xbarSink_,
-			.sink_stream = 0,
-			.source_pad = static_cast<uint32_t>(xbarFirstSource + idx),
-			.source_stream = 0,
-			.flags = V4L2_SUBDEV_ROUTE_FL_ACTIVE,
-			.reserved = {}
-		};
-
-		routing.push_back(route);
+		uint32_t sourcePad = xbarFirstSource + idx;
+		routing.emplace_back(V4L2Subdevice::Stream{ data->xbarSink_, 0 },
+				     V4L2Subdevice::Stream{ sourcePad, 0 },
+				     V4L2_SUBDEV_ROUTE_FL_ACTIVE);
 	}
 
 	int ret = crossbar_->setRouting(&routing, V4L2Subdevice::ActiveFormat);
@@ -891,7 +885,7 @@
 		unsigned int isiCode = ISICameraConfiguration::formatsMap_.at(config.pixelFormat);
 
 		V4L2SubdeviceFormat isiFormat{};
-		isiFormat.mbus_code = isiCode;
+		isiFormat.code = isiCode;
 		isiFormat.size = config.size;
 
 		ret = pipe->isi->setFormat(1, &isiFormat);
@@ -1118,6 +1112,6 @@
 	completeRequest(request);
 }
 
-REGISTER_PIPELINE_HANDLER(PipelineHandlerISI)
+REGISTER_PIPELINE_HANDLER(PipelineHandlerISI, "imx8-isi")
 
 } /* namespace libcamera */
diff --git a/src/libcamera/pipeline/ipu3/cio2.cpp b/src/libcamera/pipeline/ipu3/cio2.cpp
index 7400cb0..81a7a8a 100644
--- a/src/libcamera/pipeline/ipu3/cio2.cpp
+++ b/src/libcamera/pipeline/ipu3/cio2.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * cio2.cpp - Intel IPU3 CIO2
+ * Intel IPU3 CIO2
  */
 
 #include "cio2.h"
@@ -202,7 +202,7 @@
 	if (ret)
 		return ret;
 
-	const auto &itInfo = mbusCodesToPixelFormat.find(sensorFormat.mbus_code);
+	const auto &itInfo = mbusCodesToPixelFormat.find(sensorFormat.code);
 	if (itInfo == mbusCodesToPixelFormat.end())
 		return -EINVAL;
 
@@ -230,13 +230,13 @@
 	/* Query the sensor static information for closest match. */
 	std::vector<unsigned int> mbusCodes = utils::map_keys(mbusCodesToPixelFormat);
 	V4L2SubdeviceFormat sensorFormat = getSensorFormat(mbusCodes, size);
-	if (!sensorFormat.mbus_code) {
+	if (!sensorFormat.code) {
 		LOG(IPU3, Error) << "Sensor does not support mbus code";
 		return {};
 	}
 
 	cfg.size = sensorFormat.size;
-	cfg.pixelFormat = mbusCodesToPixelFormat.at(sensorFormat.mbus_code);
+	cfg.pixelFormat = mbusCodesToPixelFormat.at(sensorFormat.code);
 	cfg.bufferCount = kBufferCount;
 
 	return cfg;
@@ -326,7 +326,7 @@
 	}
 
 	V4L2SubdeviceFormat format{};
-	format.mbus_code = bestCode;
+	format.code = bestCode;
 	format.size = bestSize;
 
 	return format;
diff --git a/src/libcamera/pipeline/ipu3/cio2.h b/src/libcamera/pipeline/ipu3/cio2.h
index bbd87eb..963c2f6 100644
--- a/src/libcamera/pipeline/ipu3/cio2.h
+++ b/src/libcamera/pipeline/ipu3/cio2.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * cio2.h - Intel IPU3 CIO2
+ * Intel IPU3 CIO2
  */
 
 #pragma once
diff --git a/src/libcamera/pipeline/ipu3/frames.cpp b/src/libcamera/pipeline/ipu3/frames.cpp
index a4c3477..88eb9d0 100644
--- a/src/libcamera/pipeline/ipu3/frames.cpp
+++ b/src/libcamera/pipeline/ipu3/frames.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * frames.cpp - Intel IPU3 Frames helper
+ * Intel IPU3 Frames helper
  */
 
 #include "frames.h"
diff --git a/src/libcamera/pipeline/ipu3/frames.h b/src/libcamera/pipeline/ipu3/frames.h
index 6e3cb91..a347b66 100644
--- a/src/libcamera/pipeline/ipu3/frames.h
+++ b/src/libcamera/pipeline/ipu3/frames.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * frames.h - Intel IPU3 Frames helper
+ * Intel IPU3 Frames helper
  */
 
 #pragma once
diff --git a/src/libcamera/pipeline/ipu3/imgu.cpp b/src/libcamera/pipeline/ipu3/imgu.cpp
index 531879f..7be7809 100644
--- a/src/libcamera/pipeline/ipu3/imgu.cpp
+++ b/src/libcamera/pipeline/ipu3/imgu.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * imgu.cpp - Intel IPU3 ImgU
+ * Intel IPU3 ImgU
  */
 
 #include "imgu.h"
@@ -504,7 +504,7 @@
 	LOG(IPU3, Debug) << "ImgU BDS rectangle = " << bds;
 
 	V4L2SubdeviceFormat gdcFormat = {};
-	gdcFormat.mbus_code = MEDIA_BUS_FMT_FIXED;
+	gdcFormat.code = MEDIA_BUS_FMT_FIXED;
 	gdcFormat.size = pipeConfig.gdc;
 
 	ret = imgu_->setFormat(PAD_INPUT, &gdcFormat);
@@ -543,7 +543,7 @@
 				     V4L2DeviceFormat *outputFormat)
 {
 	V4L2SubdeviceFormat imguFormat = {};
-	imguFormat.mbus_code = MEDIA_BUS_FMT_FIXED;
+	imguFormat.code = MEDIA_BUS_FMT_FIXED;
 	imguFormat.size = cfg.size;
 
 	int ret = imgu_->setFormat(pad, &imguFormat);
diff --git a/src/libcamera/pipeline/ipu3/imgu.h b/src/libcamera/pipeline/ipu3/imgu.h
index 0af4dd8..fa50831 100644
--- a/src/libcamera/pipeline/ipu3/imgu.h
+++ b/src/libcamera/pipeline/ipu3/imgu.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * imgu.h - Intel IPU3 ImgU
+ * Intel IPU3 ImgU
  */
 
 #pragma once
diff --git a/src/libcamera/pipeline/ipu3/ipu3.cpp b/src/libcamera/pipeline/ipu3/ipu3.cpp
index a81c817..066fd4a 100644
--- a/src/libcamera/pipeline/ipu3/ipu3.cpp
+++ b/src/libcamera/pipeline/ipu3/ipu3.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipu3.cpp - Pipeline handler for Intel IPU3
+ * Pipeline handler for Intel IPU3
  */
 
 #include <algorithm>
@@ -187,9 +187,9 @@
 	 * rotation and store the final combined transform that configure() will
 	 * need to apply to the sensor to save us working it out again.
 	 */
-	Transform requestedTransform = transform;
-	combinedTransform_ = data_->cio2_.sensor()->validateTransform(&transform);
-	if (transform != requestedTransform)
+	Orientation requestedOrientation = orientation;
+	combinedTransform_ = data_->cio2_.sensor()->computeTransform(&orientation);
+	if (orientation != requestedOrientation)
 		status = Adjusted;
 
 	/* Cap the number of entries to the available streams. */
@@ -1420,6 +1420,6 @@
 				*testPatternMode);
 }
 
-REGISTER_PIPELINE_HANDLER(PipelineHandlerIPU3)
+REGISTER_PIPELINE_HANDLER(PipelineHandlerIPU3, "ipu3")
 
 } /* namespace libcamera */
diff --git a/src/libcamera/pipeline/mali-c55/mali-c55.cpp b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
new file mode 100644
index 0000000..45c71c1
--- /dev/null
+++ b/src/libcamera/pipeline/mali-c55/mali-c55.cpp
@@ -0,0 +1,1066 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2024, Ideas on Board Oy
+ *
+ * Pipeline Handler for ARM's Mali-C55 ISP
+ */
+
+#include <algorithm>
+#include <array>
+#include <map>
+#include <memory>
+#include <set>
+#include <string>
+
+#include <linux/media-bus-format.h>
+#include <linux/media.h>
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/camera.h>
+#include <libcamera/formats.h>
+#include <libcamera/geometry.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/camera.h"
+#include "libcamera/internal/camera_sensor.h"
+#include "libcamera/internal/device_enumerator.h"
+#include "libcamera/internal/media_device.h"
+#include "libcamera/internal/pipeline_handler.h"
+#include "libcamera/internal/v4l2_subdevice.h"
+#include "libcamera/internal/v4l2_videodevice.h"
+
+namespace {
+
+bool isFormatRaw(const libcamera::PixelFormat &pixFmt)
+{
+	return libcamera::PixelFormatInfo::info(pixFmt).colourEncoding ==
+	       libcamera::PixelFormatInfo::ColourEncodingRAW;
+}
+
+} /* namespace */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(MaliC55)
+
+const std::map<libcamera::PixelFormat, unsigned int> maliC55FmtToCode = {
+	/* \todo Support all formats supported by the driver in libcamera. */
+
+	{ formats::RGB565, MEDIA_BUS_FMT_RGB121212_1X36 },
+	{ formats::RGB888, MEDIA_BUS_FMT_RGB121212_1X36 },
+	{ formats::YUYV, MEDIA_BUS_FMT_YUV10_1X30 },
+	{ formats::UYVY, MEDIA_BUS_FMT_YUV10_1X30 },
+	{ formats::R8, MEDIA_BUS_FMT_YUV10_1X30 },
+	{ formats::NV12, MEDIA_BUS_FMT_YUV10_1X30 },
+	{ formats::NV21, MEDIA_BUS_FMT_YUV10_1X30 },
+
+	/* RAW formats, FR pipe only. */
+	{ formats::SGBRG8, MEDIA_BUS_FMT_SGBRG8_1X8 },
+	{ formats::SRGGB8, MEDIA_BUS_FMT_SRGGB8_1X8 },
+	{ formats::SBGGR8, MEDIA_BUS_FMT_SBGGR8_1X8 },
+	{ formats::SGRBG8, MEDIA_BUS_FMT_SGRBG8_1X8 },
+	{ formats::SGBRG10, MEDIA_BUS_FMT_SGBRG10_1X10 },
+	{ formats::SRGGB10, MEDIA_BUS_FMT_SRGGB10_1X10 },
+	{ formats::SBGGR10, MEDIA_BUS_FMT_SBGGR10_1X10 },
+	{ formats::SGRBG10, MEDIA_BUS_FMT_SGRBG10_1X10 },
+	{ formats::SGBRG12, MEDIA_BUS_FMT_SGBRG12_1X12 },
+	{ formats::SRGGB12, MEDIA_BUS_FMT_SRGGB12_1X12 },
+	{ formats::SBGGR12, MEDIA_BUS_FMT_SBGGR12_1X12 },
+	{ formats::SGRBG12, MEDIA_BUS_FMT_SGRBG12_1X12 },
+	{ formats::SGBRG14, MEDIA_BUS_FMT_SGBRG14_1X14 },
+	{ formats::SRGGB14, MEDIA_BUS_FMT_SRGGB14_1X14 },
+	{ formats::SBGGR14, MEDIA_BUS_FMT_SBGGR14_1X14 },
+	{ formats::SGRBG14, MEDIA_BUS_FMT_SGRBG14_1X14 },
+	{ formats::SGBRG16, MEDIA_BUS_FMT_SGBRG16_1X16 },
+	{ formats::SRGGB16, MEDIA_BUS_FMT_SRGGB16_1X16 },
+	{ formats::SBGGR16, MEDIA_BUS_FMT_SBGGR16_1X16 },
+	{ formats::SGRBG16, MEDIA_BUS_FMT_SGRBG16_1X16 },
+};
+
+constexpr Size kMaliC55MinSize = { 128, 128 };
+constexpr Size kMaliC55MaxSize = { 8192, 8192 };
+constexpr unsigned int kMaliC55ISPInternalFormat = MEDIA_BUS_FMT_RGB121212_1X36;
+
+class MaliC55CameraData : public Camera::Private
+{
+public:
+	MaliC55CameraData(PipelineHandler *pipe, MediaEntity *entity)
+		: Camera::Private(pipe), entity_(entity)
+	{
+	}
+
+	int init();
+
+	/* Deflect these functionalities to either TPG or CameraSensor. */
+	const std::vector<unsigned int> mbusCodes() const;
+	const std::vector<Size> sizes(unsigned int mbusCode) const;
+	const Size resolution() const;
+
+	PixelFormat bestRawFormat() const;
+
+	PixelFormat adjustRawFormat(const PixelFormat &pixFmt) const;
+	Size adjustRawSizes(const PixelFormat &pixFmt, const Size &rawSize) const;
+
+	std::unique_ptr<CameraSensor> sensor_;
+
+	MediaEntity *entity_;
+	std::unique_ptr<V4L2Subdevice> csi_;
+	std::unique_ptr<V4L2Subdevice> sd_;
+	Stream frStream_;
+	Stream dsStream_;
+
+private:
+	void initTPGData();
+
+	std::string id_;
+	std::vector<unsigned int> tpgCodes_;
+	std::vector<Size> tpgSizes_;
+	Size tpgResolution_;
+};
+
+int MaliC55CameraData::init()
+{
+	int ret;
+
+	sd_ = std::make_unique<V4L2Subdevice>(entity_);
+	ret = sd_->open();
+	if (ret) {
+		LOG(MaliC55, Error) << "Failed to open sensor subdevice";
+		return ret;
+	}
+
+	/* If this camera is created from TPG, we return here. */
+	if (entity_->name() == "mali-c55 tpg") {
+		initTPGData();
+		return 0;
+	}
+
+	/*
+	 * Register a CameraSensor if we connect to a sensor and create
+	 * an entity for the connected CSI-2 receiver.
+	 */
+	sensor_ = std::make_unique<CameraSensor>(entity_);
+	ret = sensor_->init();
+	if (ret)
+		return ret;
+
+	const MediaPad *sourcePad = entity_->getPadByIndex(0);
+	MediaEntity *csiEntity = sourcePad->links()[0]->sink()->entity();
+
+	csi_ = std::make_unique<V4L2Subdevice>(csiEntity);
+	if (csi_->open()) {
+		LOG(MaliC55, Error) << "Failed to open CSI-2 subdevice";
+		return false;
+	}
+
+	return 0;
+}
+
+void MaliC55CameraData::initTPGData()
+{
+	/* Replicate the CameraSensor implementation for TPG. */
+	V4L2Subdevice::Formats formats = sd_->formats(0);
+	if (formats.empty())
+		return;
+
+	tpgCodes_ = utils::map_keys(formats);
+	std::sort(tpgCodes_.begin(), tpgCodes_.end());
+
+	for (const auto &format : formats) {
+		const std::vector<SizeRange> &ranges = format.second;
+		std::transform(ranges.begin(), ranges.end(), std::back_inserter(tpgSizes_),
+			       [](const SizeRange &range) { return range.max; });
+	}
+
+	tpgResolution_ = tpgSizes_.back();
+}
+
+const std::vector<unsigned int> MaliC55CameraData::mbusCodes() const
+{
+	if (sensor_)
+		return sensor_->mbusCodes();
+
+	return tpgCodes_;
+}
+
+const std::vector<Size> MaliC55CameraData::sizes(unsigned int mbusCode) const
+{
+	if (sensor_)
+		return sensor_->sizes(mbusCode);
+
+	V4L2Subdevice::Formats formats = sd_->formats(0);
+	if (formats.empty())
+		return {};
+
+	std::vector<Size> sizes;
+	const auto &format = formats.find(mbusCode);
+	if (format == formats.end())
+		return {};
+
+	const std::vector<SizeRange> &ranges = format->second;
+	std::transform(ranges.begin(), ranges.end(), std::back_inserter(sizes),
+		       [](const SizeRange &range) { return range.max; });
+
+	std::sort(sizes.begin(), sizes.end());
+
+	return sizes;
+}
+
+const Size MaliC55CameraData::resolution() const
+{
+	if (sensor_)
+		return sensor_->resolution();
+
+	return tpgResolution_;
+}
+
+PixelFormat MaliC55CameraData::bestRawFormat() const
+{
+	unsigned int bitDepth = 0;
+	PixelFormat rawFormat;
+
+	/*
+	 * Iterate over all the supported PixelFormat and find the one
+	 * supported by the camera with the largest bitdepth.
+	 */
+	for (const auto &maliFormat : maliC55FmtToCode) {
+		PixelFormat pixFmt = maliFormat.first;
+		if (!isFormatRaw(pixFmt))
+			continue;
+
+		unsigned int rawCode = maliFormat.second;
+		const auto rawSizes = sizes(rawCode);
+		if (rawSizes.empty())
+			continue;
+
+		BayerFormat bayer = BayerFormat::fromMbusCode(rawCode);
+		if (bayer.bitDepth > bitDepth) {
+			bitDepth = bayer.bitDepth;
+			rawFormat = pixFmt;
+		}
+	}
+
+	return rawFormat;
+}
+
+/*
+ * Make sure the provided raw pixel format is supported and adjust it to
+ * one of the supported ones if it's not.
+ */
+PixelFormat MaliC55CameraData::adjustRawFormat(const PixelFormat &rawFmt) const
+{
+	/* Make sure the provided raw format is supported by the pipeline. */
+	auto it = maliC55FmtToCode.find(rawFmt);
+	if (it == maliC55FmtToCode.end())
+		return bestRawFormat();
+
+	/* Now make sure the RAW mbus code is supported by the image source. */
+	unsigned int rawCode = it->second;
+	const auto rawSizes = sizes(rawCode);
+	if (rawSizes.empty())
+		return bestRawFormat();
+
+	return rawFmt;
+}
+
+Size MaliC55CameraData::adjustRawSizes(const PixelFormat &rawFmt, const Size &rawSize) const
+{
+	/* Just make sure the format is supported. */
+	auto it = maliC55FmtToCode.find(rawFmt);
+	if (it == maliC55FmtToCode.end())
+		return {};
+
+	/* Check if the size is natively supported. */
+	unsigned int rawCode = it->second;
+	const auto rawSizes = sizes(rawCode);
+	auto sizeIt = std::find(rawSizes.begin(), rawSizes.end(), rawSize);
+	if (sizeIt != rawSizes.end())
+		return rawSize;
+
+	/* Or adjust it to the closest supported size. */
+	uint16_t distance = std::numeric_limits<uint16_t>::max();
+	Size bestSize;
+	for (const Size &size : rawSizes) {
+		uint16_t dist = std::abs(static_cast<int>(rawSize.width) -
+					 static_cast<int>(size.width)) +
+				std::abs(static_cast<int>(rawSize.height) -
+					 static_cast<int>(size.height));
+		if (dist < distance) {
+			dist = distance;
+			bestSize = size;
+		}
+	}
+
+	return bestSize;
+}
+
+class MaliC55CameraConfiguration : public CameraConfiguration
+{
+public:
+	MaliC55CameraConfiguration(MaliC55CameraData *data)
+		: CameraConfiguration(), data_(data)
+	{
+	}
+
+	Status validate() override;
+
+	V4L2SubdeviceFormat sensorFormat_;
+
+private:
+	static constexpr unsigned int kMaxStreams = 2;
+
+	const MaliC55CameraData *data_;
+};
+
+CameraConfiguration::Status MaliC55CameraConfiguration::validate()
+{
+	Status status = Valid;
+
+	if (config_.empty())
+		return Invalid;
+
+	/* Only 2 streams available. */
+	if (config_.size() > kMaxStreams) {
+		config_.resize(kMaxStreams);
+		status = Adjusted;
+	}
+
+	bool frPipeAvailable = true;
+	StreamConfiguration *rawConfig = nullptr;
+	for (StreamConfiguration &config : config_) {
+		if (!isFormatRaw(config.pixelFormat))
+			continue;
+
+		if (rawConfig) {
+			LOG(MaliC55, Error)
+				<< "Only a single RAW stream is supported";
+			return Invalid;
+		}
+
+		rawConfig = &config;
+	}
+
+	Size maxSize = kMaliC55MaxSize;
+	if (rawConfig) {
+		/*
+		 * \todo Take into account the Bayer components ordering once
+		 * we support rotations.
+		 */
+		PixelFormat rawFormat =
+			data_->adjustRawFormat(rawConfig->pixelFormat);
+		if (rawFormat != rawConfig->pixelFormat) {
+			LOG(MaliC55, Debug)
+				<< "RAW format adjusted to " << rawFormat;
+			rawConfig->pixelFormat = rawFormat;
+			status = Adjusted;
+		}
+
+		Size rawSize =
+			data_->adjustRawSizes(rawFormat, rawConfig->size);
+		if (rawSize != rawConfig->size) {
+			LOG(MaliC55, Debug)
+				<< "RAW sizes adjusted to " << rawSize;
+			rawConfig->size = rawSize;
+			status = Adjusted;
+		}
+
+		maxSize = rawSize;
+
+		rawConfig->setStream(const_cast<Stream *>(&data_->frStream_));
+		frPipeAvailable = false;
+	}
+
+	/* Adjust processed streams. */
+	Size maxYuvSize;
+	for (StreamConfiguration &config : config_) {
+		if (isFormatRaw(config.pixelFormat))
+			continue;
+
+		/* Adjust format and size for processed streams. */
+		const auto it = maliC55FmtToCode.find(config.pixelFormat);
+		if (it == maliC55FmtToCode.end()) {
+			LOG(MaliC55, Debug)
+				<< "Format adjusted to " << formats::RGB565;
+			config.pixelFormat = formats::RGB565;
+			status = Adjusted;
+		}
+
+		Size size = std::clamp(config.size, kMaliC55MinSize, maxSize);
+		if (size != config.size) {
+			LOG(MaliC55, Debug)
+				<< "Size adjusted to " << size;
+			config.size = size;
+			status = Adjusted;
+		}
+
+		if (maxYuvSize < size)
+			maxYuvSize = size;
+
+		if (frPipeAvailable) {
+			config.setStream(const_cast<Stream *>(&data_->frStream_));
+			frPipeAvailable = false;
+		} else {
+			config.setStream(const_cast<Stream *>(&data_->dsStream_));
+		}
+	}
+
+	/* Compute the sensor format. */
+
+	/* If there's a RAW config, sensor configuration follows it. */
+	if (rawConfig) {
+		const auto it = maliC55FmtToCode.find(rawConfig->pixelFormat);
+		sensorFormat_.code = it->second;
+		sensorFormat_.size = rawConfig->size;
+
+		return status;
+	}
+
+	/* If there's no RAW config, compute the sensor configuration here. */
+	PixelFormat rawFormat = data_->bestRawFormat();
+	const auto it = maliC55FmtToCode.find(rawFormat);
+	sensorFormat_.code = it->second;
+
+	uint16_t distance = std::numeric_limits<uint16_t>::max();
+	const auto sizes = data_->sizes(it->second);
+	Size bestSize;
+	for (const auto &size : sizes) {
+		/* Skip sensor sizes that are smaller than the max YUV size. */
+		if (maxYuvSize.width > size.width ||
+		    maxYuvSize.height > size.height)
+			continue;
+
+		uint16_t dist = std::abs(static_cast<int>(maxYuvSize.width) -
+					 static_cast<int>(size.width)) +
+				std::abs(static_cast<int>(maxYuvSize.height) -
+					 static_cast<int>(size.height));
+		if (dist < distance) {
+			dist = distance;
+			bestSize = size;
+		}
+	}
+	sensorFormat_.size = bestSize;
+
+	LOG(MaliC55, Debug) << "Computed sensor configuration " << sensorFormat_;
+
+	return status;
+}
+
+class PipelineHandlerMaliC55 : public PipelineHandler
+{
+public:
+	PipelineHandlerMaliC55(CameraManager *manager);
+
+	std::unique_ptr<CameraConfiguration> generateConfiguration(Camera *camera,
+								   Span<const StreamRole> roles) override;
+	int configure(Camera *camera, CameraConfiguration *config) override;
+
+	int exportFrameBuffers(Camera *camera, Stream *stream,
+			       std::vector<std::unique_ptr<FrameBuffer>> *buffers) override;
+
+	int start(Camera *camera, const ControlList *controls) override;
+	void stopDevice(Camera *camera) override;
+
+	int queueRequestDevice(Camera *camera, Request *request) override;
+
+	void bufferReady(FrameBuffer *buffer);
+
+	bool match(DeviceEnumerator *enumerator) override;
+
+private:
+	struct MaliC55Pipe {
+		std::unique_ptr<V4L2Subdevice> resizer;
+		std::unique_ptr<V4L2VideoDevice> cap;
+		Stream *stream;
+	};
+
+	enum {
+		MaliC55FR,
+		MaliC55DS,
+		MaliC55NumPipes,
+	};
+
+	MaliC55CameraData *cameraData(Camera *camera)
+	{
+		return static_cast<MaliC55CameraData *>(camera->_d());
+	}
+
+	MaliC55Pipe *pipeFromStream(MaliC55CameraData *data, Stream *stream)
+	{
+		if (stream == &data->frStream_)
+			return &pipes_[MaliC55FR];
+		else if (stream == &data->dsStream_)
+			return &pipes_[MaliC55DS];
+		else
+			LOG(MaliC55, Fatal) << "Stream " << stream << " not valid";
+		return nullptr;
+	}
+
+	MaliC55Pipe *pipeFromStream(MaliC55CameraData *data, const Stream *stream)
+	{
+		return pipeFromStream(data, const_cast<Stream *>(stream));
+	}
+
+	void resetPipes()
+	{
+		for (MaliC55Pipe &pipe : pipes_)
+			pipe.stream = nullptr;
+	}
+
+	int configureRawStream(MaliC55CameraData *data,
+			       const StreamConfiguration &config,
+			       V4L2SubdeviceFormat &subdevFormat);
+	int configureProcessedStream(MaliC55CameraData *data,
+				     const StreamConfiguration &config,
+				     V4L2SubdeviceFormat &subdevFormat);
+
+	void registerMaliCamera(std::unique_ptr<MaliC55CameraData> data,
+				const std::string &name);
+	bool registerTPGCamera(MediaLink *link);
+	bool registerSensorCamera(MediaLink *link);
+
+	MediaDevice *media_;
+	std::unique_ptr<V4L2Subdevice> isp_;
+
+	std::array<MaliC55Pipe, MaliC55NumPipes> pipes_;
+
+	bool dsFitted_;
+};
+
+PipelineHandlerMaliC55::PipelineHandlerMaliC55(CameraManager *manager)
+	: PipelineHandler(manager), dsFitted_(true)
+{
+}
+
+std::unique_ptr<CameraConfiguration>
+PipelineHandlerMaliC55::generateConfiguration(Camera *camera,
+					      Span<const StreamRole> roles)
+{
+	MaliC55CameraData *data = cameraData(camera);
+	std::unique_ptr<CameraConfiguration> config =
+		std::make_unique<MaliC55CameraConfiguration>(data);
+	bool frPipeAvailable = true;
+
+	if (roles.empty())
+		return config;
+
+	/* Check if one stream is RAW to reserve the FR pipe for it. */
+	if (std::find(roles.begin(), roles.end(), StreamRole::Raw) != roles.end())
+		frPipeAvailable = false;
+
+	for (const StreamRole &role : roles) {
+		struct MaliC55Pipe *pipe;
+
+		/* Assign pipe for this role. */
+		if (role == StreamRole::Raw) {
+			pipe = &pipes_[MaliC55FR];
+		} else {
+			if (frPipeAvailable) {
+				pipe = &pipes_[MaliC55FR];
+				frPipeAvailable = false;
+			} else {
+				pipe = &pipes_[MaliC55DS];
+			}
+		}
+
+		Size size = std::min(Size{ 1920, 1080 }, data->resolution());
+		PixelFormat pixelFormat;
+
+		switch (role) {
+		case StreamRole::StillCapture:
+			size = data->resolution();
+			[[fallthrough]];
+		case StreamRole::VideoRecording:
+			pixelFormat = formats::NV12;
+			break;
+
+		case StreamRole::Viewfinder:
+			pixelFormat = formats::RGB565;
+			break;
+
+		case StreamRole::Raw:
+			pixelFormat = data->bestRawFormat();
+			if (!pixelFormat.isValid()) {
+				LOG(MaliC55, Error)
+					<< "Camera does not support RAW formats";
+				return nullptr;
+			}
+
+			size = data->resolution();
+			break;
+
+		default:
+			LOG(MaliC55, Error)
+				<< "Requested stream role not supported: " << role;
+			return nullptr;
+		}
+
+		std::map<PixelFormat, std::vector<SizeRange>> formats;
+		for (const auto &maliFormat : maliC55FmtToCode) {
+			PixelFormat pixFmt = maliFormat.first;
+			bool isRaw = isFormatRaw(pixFmt);
+
+			/* RAW formats are only supported on the FR pipe. */
+			if (pipe != &pipes_[MaliC55FR] && isRaw)
+				continue;
+
+			if (isRaw) {
+				/* Make sure the mbus code is supported. */
+				unsigned int rawCode = maliFormat.second;
+				const auto sizes = data->sizes(rawCode);
+				if (sizes.empty())
+					continue;
+
+				/* And list all sizes the sensor can produce. */
+				std::vector<SizeRange> sizeRanges;
+				std::transform(sizes.begin(), sizes.end(),
+					       std::back_inserter(sizeRanges),
+					       [](const Size &s) {
+						       return SizeRange(s);
+					       });
+
+				formats[pixFmt] = sizeRanges;
+			} else {
+				/* Processed formats are always available. */
+				Size maxSize = std::min(kMaliC55MaxSize,
+							data->resolution());
+				formats[pixFmt] = { kMaliC55MinSize, maxSize };
+			}
+		}
+
+		StreamFormats streamFormats(formats);
+		StreamConfiguration cfg(streamFormats);
+		cfg.pixelFormat = pixelFormat;
+		cfg.bufferCount = 4;
+		cfg.size = size;
+
+		config->addConfiguration(cfg);
+	}
+
+	if (config->validate() == CameraConfiguration::Invalid)
+		return nullptr;
+
+	return config;
+}
+
+int PipelineHandlerMaliC55::configureRawStream(MaliC55CameraData *data,
+					       const StreamConfiguration &config,
+					       V4L2SubdeviceFormat &subdevFormat)
+{
+	Stream *stream = config.stream();
+	MaliC55Pipe *pipe = pipeFromStream(data, stream);
+
+	if (pipe != &pipes_[MaliC55FR]) {
+		LOG(MaliC55, Fatal) << "Only the FR pipe supports RAW capture.";
+		return -EINVAL;
+	}
+
+	/* Enable the debayer route to set fixed internal format on pad #0. */
+	V4L2Subdevice::Routing routing = {};
+	routing.emplace_back(V4L2Subdevice::Stream{ 0, 0 },
+			     V4L2Subdevice::Stream{ 1, 0 },
+			     V4L2_SUBDEV_ROUTE_FL_ACTIVE);
+
+	int ret = pipe->resizer->setRouting(&routing, V4L2Subdevice::ActiveFormat);
+	if (ret)
+		return ret;
+
+	unsigned int rawCode = subdevFormat.code;
+	subdevFormat.code = kMaliC55ISPInternalFormat;
+	ret = pipe->resizer->setFormat(0, &subdevFormat);
+	if (ret)
+		return ret;
+
+	/* Enable the bypass route and apply RAW formats there. */
+	routing.clear();
+	routing.emplace_back(V4L2Subdevice::Stream{ 2, 0 },
+			     V4L2Subdevice::Stream{ 1, 0 },
+			     V4L2_SUBDEV_ROUTE_FL_ACTIVE);
+	ret = pipe->resizer->setRouting(&routing, V4L2Subdevice::ActiveFormat);
+	if (ret)
+		return ret;
+
+	subdevFormat.code = rawCode;
+	ret = pipe->resizer->setFormat(2, &subdevFormat);
+	if (ret)
+		return ret;
+
+	ret = pipe->resizer->setFormat(1, &subdevFormat);
+	if (ret)
+		return ret;
+
+	return 0;
+}
+
+int PipelineHandlerMaliC55::configureProcessedStream(MaliC55CameraData *data,
+						     const StreamConfiguration &config,
+						     V4L2SubdeviceFormat &subdevFormat)
+{
+	Stream *stream = config.stream();
+	MaliC55Pipe *pipe = pipeFromStream(data, stream);
+
+	/* Enable the debayer route on the resizer pipe. */
+	V4L2Subdevice::Routing routing = {};
+	routing.emplace_back(V4L2Subdevice::Stream{ 0, 0 },
+			     V4L2Subdevice::Stream{ 1, 0 },
+			     V4L2_SUBDEV_ROUTE_FL_ACTIVE);
+
+	int ret = pipe->resizer->setRouting(&routing, V4L2Subdevice::ActiveFormat);
+	if (ret)
+		return ret;
+
+	subdevFormat.code = kMaliC55ISPInternalFormat;
+	ret = pipe->resizer->setFormat(0, &subdevFormat);
+	if (ret)
+		return ret;
+
+	/* \todo Configure the resizer crop/compose rectangles. */
+	Rectangle ispCrop = { 0, 0, config.size };
+	ret = pipe->resizer->setSelection(0, V4L2_SEL_TGT_CROP, &ispCrop);
+	if (ret)
+		return ret;
+
+	ret = pipe->resizer->setSelection(0, V4L2_SEL_TGT_COMPOSE, &ispCrop);
+	if (ret)
+		return ret;
+
+	subdevFormat.code = maliC55FmtToCode.find(config.pixelFormat)->second;
+	return pipe->resizer->setFormat(1, &subdevFormat);
+}
+
+int PipelineHandlerMaliC55::configure(Camera *camera,
+				      CameraConfiguration *config)
+{
+	resetPipes();
+
+	int ret = media_->disableLinks();
+	if (ret)
+		return ret;
+
+	/* Link the graph depending if we are operating the TPG or a sensor. */
+	MaliC55CameraData *data = cameraData(camera);
+	if (data->csi_) {
+		const MediaEntity *csiEntity = data->csi_->entity();
+		ret = csiEntity->getPadByIndex(1)->links()[0]->setEnabled(true);
+	} else {
+		ret = data->entity_->getPadByIndex(0)->links()[0]->setEnabled(true);
+	}
+	if (ret)
+		return ret;
+
+	MaliC55CameraConfiguration *maliConfig =
+		static_cast<MaliC55CameraConfiguration *>(config);
+	V4L2SubdeviceFormat subdevFormat = maliConfig->sensorFormat_;
+	ret = data->sd_->getFormat(0, &subdevFormat);
+	if (ret)
+		return ret;
+
+	if (data->csi_) {
+		ret = data->csi_->setFormat(0, &subdevFormat);
+		if (ret)
+			return ret;
+
+		ret = data->csi_->setFormat(1, &subdevFormat);
+		if (ret)
+			return ret;
+	}
+
+	/*
+	 * Propagate the format to the ISP sink pad and configure the input
+	 * crop rectangle (no crop at the moment).
+	 *
+	 * \todo Configure the CSI-2 receiver.
+	 */
+	ret = isp_->setFormat(0, &subdevFormat);
+	if (ret)
+		return ret;
+
+	Rectangle ispCrop(0, 0, subdevFormat.size);
+	ret = isp_->setSelection(0, V4L2_SEL_TGT_CROP, &ispCrop);
+	if (ret)
+		return ret;
+
+	/*
+	 * Configure the resizer: fixed format the sink pad; use the media
+	 * bus code associated with the desired capture format on the source
+	 * pad.
+	 *
+	 * Configure the crop and compose rectangles to match the desired
+	 * stream output size
+	 *
+	 * \todo Make the crop/scaler configurable
+	 */
+	for (const StreamConfiguration &streamConfig : *config) {
+		Stream *stream = streamConfig.stream();
+		MaliC55Pipe *pipe = pipeFromStream(data, stream);
+
+		if (isFormatRaw(streamConfig.pixelFormat))
+			ret = configureRawStream(data, streamConfig, subdevFormat);
+		else
+			ret = configureProcessedStream(data, streamConfig, subdevFormat);
+		if (ret) {
+			LOG(MaliC55, Error) << "Failed to configure pipeline";
+			return ret;
+		}
+
+		/* Now apply the pixel format and size to the capture device. */
+		V4L2DeviceFormat captureFormat;
+		captureFormat.fourcc = pipe->cap->toV4L2PixelFormat(streamConfig.pixelFormat);
+		captureFormat.size = streamConfig.size;
+
+		ret = pipe->cap->setFormat(&captureFormat);
+		if (ret)
+			return ret;
+
+		pipe->stream = stream;
+	}
+
+	return 0;
+}
+
+int PipelineHandlerMaliC55::exportFrameBuffers(Camera *camera, Stream *stream,
+					       std::vector<std::unique_ptr<FrameBuffer>> *buffers)
+{
+	MaliC55Pipe *pipe = pipeFromStream(cameraData(camera), stream);
+	unsigned int count = stream->configuration().bufferCount;
+
+	return pipe->cap->exportBuffers(count, buffers);
+}
+
+int PipelineHandlerMaliC55::start([[maybe_unused]] Camera *camera, [[maybe_unused]] const ControlList *controls)
+{
+	for (MaliC55Pipe &pipe : pipes_) {
+		if (!pipe.stream)
+			continue;
+
+		Stream *stream = pipe.stream;
+
+		int ret = pipe.cap->importBuffers(stream->configuration().bufferCount);
+		if (ret) {
+			LOG(MaliC55, Error) << "Failed to import buffers";
+			return ret;
+		}
+
+		ret = pipe.cap->streamOn();
+		if (ret) {
+			LOG(MaliC55, Error) << "Failed to start stream";
+			return ret;
+		}
+	}
+
+	return 0;
+}
+
+void PipelineHandlerMaliC55::stopDevice([[maybe_unused]] Camera *camera)
+{
+	for (MaliC55Pipe &pipe : pipes_) {
+		if (!pipe.stream)
+			continue;
+
+		pipe.cap->streamOff();
+		pipe.cap->releaseBuffers();
+	}
+}
+
+int PipelineHandlerMaliC55::queueRequestDevice(Camera *camera, Request *request)
+{
+	int ret;
+
+	for (auto &[stream, buffer] : request->buffers()) {
+		MaliC55Pipe *pipe = pipeFromStream(cameraData(camera), stream);
+
+		ret = pipe->cap->queueBuffer(buffer);
+		if (ret)
+			return ret;
+	}
+
+	return 0;
+}
+
+void PipelineHandlerMaliC55::bufferReady(FrameBuffer *buffer)
+{
+	Request *request = buffer->request();
+
+	completeBuffer(request, buffer);
+
+	if (request->hasPendingBuffers())
+		return;
+
+	completeRequest(request);
+}
+
+void PipelineHandlerMaliC55::registerMaliCamera(std::unique_ptr<MaliC55CameraData> data,
+						const std::string &name)
+{
+	std::set<Stream *> streams{ &data->frStream_ };
+	if (dsFitted_)
+		streams.insert(&data->dsStream_);
+
+	std::shared_ptr<Camera> camera = Camera::create(std::move(data),
+							name, streams);
+	registerCamera(std::move(camera));
+}
+
+/*
+ * The only camera we support through direct connection to the ISP is the
+ * Mali-C55 TPG. Check we have that and warn if not.
+ */
+bool PipelineHandlerMaliC55::registerTPGCamera(MediaLink *link)
+{
+	const std::string &name = link->source()->entity()->name();
+	if (name != "mali-c55 tpg") {
+		LOG(MaliC55, Warning) << "Unsupported direct connection to "
+				      << link->source()->entity()->name();
+		/*
+		 * Return true and just skip registering a camera for this
+		 * entity.
+		 */
+		return true;
+	}
+
+	std::unique_ptr<MaliC55CameraData> data =
+		std::make_unique<MaliC55CameraData>(this, link->source()->entity());
+
+	if (data->init())
+		return false;
+
+	registerMaliCamera(std::move(data), name);
+
+	return true;
+}
+
+/*
+ * Register a Camera for each sensor connected to the ISP through a CSI-2
+ * receiver.
+ *
+ * \todo Support more complex topologies, such as video muxes.
+ */
+bool PipelineHandlerMaliC55::registerSensorCamera(MediaLink *ispLink)
+{
+	MediaEntity *csi2 = ispLink->source()->entity();
+	const MediaPad *csi2Sink = csi2->getPadByIndex(0);
+
+	for (MediaLink *link : csi2Sink->links()) {
+		MediaEntity *sensor = link->source()->entity();
+		unsigned int function = sensor->function();
+
+		if (function != MEDIA_ENT_F_CAM_SENSOR)
+			continue;
+
+		std::unique_ptr<MaliC55CameraData> data =
+			std::make_unique<MaliC55CameraData>(this, sensor);
+		if (data->init())
+			return false;
+
+		/* \todo: Init properties and controls. */
+
+		registerMaliCamera(std::move(data), sensor->name());
+	}
+
+	return true;
+}
+
+bool PipelineHandlerMaliC55::match(DeviceEnumerator *enumerator)
+{
+	const MediaPad *ispSink;
+
+	/*
+	 * We search for just the ISP subdevice and the full resolution pipe.
+	 * The TPG and the downscale pipe are both optional blocks and may not
+	 * be fitted.
+	 */
+	DeviceMatch dm("mali-c55");
+	dm.add("mali-c55 isp");
+	dm.add("mali-c55 resizer fr");
+	dm.add("mali-c55 fr");
+
+	media_ = acquireMediaDevice(enumerator, dm);
+	if (!media_)
+		return false;
+
+	isp_ = V4L2Subdevice::fromEntityName(media_, "mali-c55 isp");
+	if (isp_->open() < 0)
+		return false;
+
+	MaliC55Pipe *frPipe = &pipes_[MaliC55FR];
+	frPipe->resizer = V4L2Subdevice::fromEntityName(media_, "mali-c55 resizer fr");
+	if (frPipe->resizer->open() < 0)
+		return false;
+
+	frPipe->cap = V4L2VideoDevice::fromEntityName(media_, "mali-c55 fr");
+	if (frPipe->cap->open() < 0)
+		return false;
+
+	frPipe->cap->bufferReady.connect(this, &PipelineHandlerMaliC55::bufferReady);
+
+	dsFitted_ = !!media_->getEntityByName("mali-c55 ds");
+	if (dsFitted_) {
+		LOG(MaliC55, Debug) << "Downscaler pipe is fitted";
+
+		MaliC55Pipe *dsPipe = &pipes_[MaliC55DS];
+
+		dsPipe->resizer = V4L2Subdevice::fromEntityName(media_, "mali-c55 resizer ds");
+		if (dsPipe->resizer->open() < 0)
+			return false;
+
+		dsPipe->cap = V4L2VideoDevice::fromEntityName(media_, "mali-c55 ds");
+		if (dsPipe->cap->open() < 0)
+			return false;
+
+		dsPipe->cap->bufferReady.connect(this, &PipelineHandlerMaliC55::bufferReady);
+	}
+
+	ispSink = isp_->entity()->getPadByIndex(0);
+	if (!ispSink || ispSink->links().empty()) {
+		LOG(MaliC55, Error) << "ISP sink pad error";
+		return false;
+	}
+
+	/*
+	 * We could have several links pointing to the ISP's sink pad, which
+	 * will be from entities with one of the following functions:
+	 *
+	 * MEDIA_ENT_F_CAM_SENSOR - The test pattern generator
+	 * MEDIA_ENT_F_VID_IF_BRIDGE - A CSI-2 receiver
+	 * MEDIA_ENT_F_IO_V4L - An input device
+	 *
+	 * The last one will be unsupported for now. The TPG is relatively easy,
+	 * we just register a Camera for it. If we have a CSI-2 receiver we need
+	 * to check its sink pad and register Cameras for anything connected to
+	 * it (probably...there are some complex situations in which that might
+	 * not be true but let's pretend they don't exist until we come across
+	 * them)
+	 */
+	bool registered;
+	for (MediaLink *link : ispSink->links()) {
+		unsigned int function = link->source()->entity()->function();
+
+		switch (function) {
+		case MEDIA_ENT_F_CAM_SENSOR:
+			registered = registerTPGCamera(link);
+			if (!registered)
+				return registered;
+
+			break;
+		case MEDIA_ENT_F_VID_IF_BRIDGE:
+			registered = registerSensorCamera(link);
+			if (!registered)
+				return registered;
+
+			break;
+		case MEDIA_ENT_F_IO_V4L:
+			LOG(MaliC55, Warning) << "Memory input not yet supported";
+			break;
+		default:
+			LOG(MaliC55, Error) << "Unsupported entity function";
+			return false;
+		}
+	}
+
+	return true;
+}
+
+REGISTER_PIPELINE_HANDLER(PipelineHandlerMaliC55, "mali-c55")
+
+} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/mali-c55/meson.build b/src/libcamera/pipeline/mali-c55/meson.build
new file mode 100644
index 0000000..30fd29b
--- /dev/null
+++ b/src/libcamera/pipeline/mali-c55/meson.build
@@ -0,0 +1,5 @@
+# SPDX-License-Identifier: CC0-1.0
+
+libcamera_sources += files([
+    'mali-c55.cpp'
+])
diff --git a/src/libcamera/pipeline/rkisp1/rkisp1.cpp b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
index 6efa79f..4cbf105 100644
--- a/src/libcamera/pipeline/rkisp1/rkisp1.cpp
+++ b/src/libcamera/pipeline/rkisp1/rkisp1.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * rkisp1.cpp - Pipeline handler for Rockchip ISP1
+ * Pipeline handler for Rockchip ISP1
  */
 
 #include <algorithm>
@@ -481,9 +481,9 @@
 		status = Adjusted;
 	}
 
-	Transform requestedTransform = transform;
-	Transform combined = sensor->validateTransform(&transform);
-	if (transform != requestedTransform)
+	Orientation requestedOrientation = orientation;
+	combinedTransform_ = data_->sensor_->computeTransform(&orientation);
+	if (orientation != requestedOrientation)
 		status = Adjusted;
 
 	/*
@@ -595,8 +595,6 @@
 	if (sensorFormat_.size.isNull())
 		sensorFormat_.size = sensor->resolution();
 
-	combinedTransform_ = combined;
-
 	return status;
 }
 
@@ -763,7 +761,7 @@
 
 	/* YUYV8_2X8 is required on the ISP source path pad for YUV output. */
 	if (!isRaw_)
-		format.mbus_code = MEDIA_BUS_FMT_YUYV8_2X8;
+		format.code = MEDIA_BUS_FMT_YUYV8_2X8;
 
 	LOG(RkISP1, Debug)
 		<< "Configuring ISP output pad with " << format
@@ -1324,6 +1322,6 @@
 				       data->delayedCtrls_->get(buffer->metadata().sequence));
 }
 
-REGISTER_PIPELINE_HANDLER(PipelineHandlerRkISP1)
+REGISTER_PIPELINE_HANDLER(PipelineHandlerRkISP1, "rkisp1")
 
 } /* namespace libcamera */
diff --git a/src/libcamera/pipeline/rkisp1/rkisp1_path.cpp b/src/libcamera/pipeline/rkisp1/rkisp1_path.cpp
index b62b645..c49017d 100644
--- a/src/libcamera/pipeline/rkisp1/rkisp1_path.cpp
+++ b/src/libcamera/pipeline/rkisp1/rkisp1_path.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * rkisp1path.cpp - Rockchip ISP1 path helper
+ * Rockchip ISP1 path helper
  */
 
 #include "rkisp1_path.h"
@@ -365,7 +365,7 @@
 	 * The configuration has been validated, the pixel format is guaranteed
 	 * to be supported and thus found in formatToMediaBus.
 	 */
-	ispFormat.mbus_code = formatToMediaBus.at(config.pixelFormat);
+	ispFormat.code = formatToMediaBus.at(config.pixelFormat);
 
 	ret = resizer_->setFormat(1, &ispFormat);
 	if (ret < 0)
diff --git a/src/libcamera/pipeline/rkisp1/rkisp1_path.h b/src/libcamera/pipeline/rkisp1/rkisp1_path.h
index cd77957..08edefe 100644
--- a/src/libcamera/pipeline/rkisp1/rkisp1_path.h
+++ b/src/libcamera/pipeline/rkisp1/rkisp1_path.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * rkisp1path.h - Rockchip ISP1 path helper
+ * Rockchip ISP1 path helper
  */
 
 #pragma once
diff --git a/src/libcamera/pipeline/rpi/common/delayed_controls.cpp b/src/libcamera/pipeline/rpi/common/delayed_controls.cpp
index 3db92e7..ad50a7c 100644
--- a/src/libcamera/pipeline/rpi/common/delayed_controls.cpp
+++ b/src/libcamera/pipeline/rpi/common/delayed_controls.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * delayed_controls.cpp - Helper to deal with controls that take effect with a delay
+ * Helper to deal with controls that take effect with a delay
  *
  * Note: This has been forked from the libcamera core implementation.
  */
diff --git a/src/libcamera/pipeline/rpi/common/delayed_controls.h b/src/libcamera/pipeline/rpi/common/delayed_controls.h
index 61f755f..487b005 100644
--- a/src/libcamera/pipeline/rpi/common/delayed_controls.h
+++ b/src/libcamera/pipeline/rpi/common/delayed_controls.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * delayed_controls.h - Helper to deal with controls that take effect with a delay
+ * Helper to deal with controls that take effect with a delay
  *
  * Note: This has been forked from the libcamera core implementation.
  */
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
index 51fa1bb..3041fd1 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2023, Raspberry Pi Ltd
  *
- * pipeline_base.cpp - Pipeline handler base class for Raspberry Pi devices
+ * Pipeline handler base class for Raspberry Pi devices
  */
 
 #include "pipeline_base.h"
@@ -37,16 +37,10 @@
 
 constexpr unsigned int defaultRawBitDepth = 12;
 
-bool isRaw(const PixelFormat &pixFmt)
-{
-	/* This test works for both Bayer and raw mono formats. */
-	return BayerFormat::fromPixelFormat(pixFmt).isValid();
-}
-
-PixelFormat mbusCodeToPixelFormat(unsigned int mbus_code,
+PixelFormat mbusCodeToPixelFormat(unsigned int code,
 				  BayerFormat::Packing packingReq)
 {
-	BayerFormat bayer = BayerFormat::fromMbusCode(mbus_code);
+	BayerFormat bayer = BayerFormat::fromMbusCode(code);
 
 	ASSERT(bayer.isValid());
 
@@ -91,22 +85,6 @@
 	return std::nullopt;
 }
 
-bool isRgb(const PixelFormat &pixFmt)
-{
-	const PixelFormatInfo &info = PixelFormatInfo::info(pixFmt);
-	return info.colourEncoding == PixelFormatInfo::ColourEncodingRGB;
-}
-
-bool isYuv(const PixelFormat &pixFmt)
-{
-	/* The code below would return true for raw mono streams, so weed those out first. */
-	if (isRaw(pixFmt))
-		return false;
-
-	const PixelFormatInfo &info = PixelFormatInfo::info(pixFmt);
-	return info.colourEncoding == PixelFormatInfo::ColourEncodingYUV;
-}
-
 } /* namespace */
 
 /*
@@ -129,7 +107,7 @@
 
 	for (auto cfg : config_) {
 		/* First fix up raw streams to have the "raw" colour space. */
-		if (isRaw(cfg.pixelFormat)) {
+		if (PipelineHandlerBase::isRaw(cfg.pixelFormat)) {
 			/* If there was no value here, that doesn't count as "adjusted". */
 			if (cfg.colorSpace && cfg.colorSpace != ColorSpace::Raw)
 				status = Adjusted;
@@ -156,13 +134,13 @@
 		if (cfg.colorSpace == ColorSpace::Raw)
 			continue;
 
-		if (isYuv(cfg.pixelFormat) && cfg.colorSpace != yuvColorSpace_) {
+		if (PipelineHandlerBase::isYuv(cfg.pixelFormat) && cfg.colorSpace != yuvColorSpace_) {
 			/* Again, no value means "not adjusted". */
 			if (cfg.colorSpace)
 				status = Adjusted;
 			cfg.colorSpace = yuvColorSpace_;
 		}
-		if (isRgb(cfg.pixelFormat) && cfg.colorSpace != rgbColorSpace_) {
+		if (PipelineHandlerBase::isRgb(cfg.pixelFormat) && cfg.colorSpace != rgbColorSpace_) {
 			/* Be nice, and let the YUV version count as non-adjusted too. */
 			if (cfg.colorSpace && cfg.colorSpace != yuvColorSpace_)
 				status = Adjusted;
@@ -180,6 +158,15 @@
 	if (config_.empty())
 		return Invalid;
 
+	/*
+	 * Make sure that if a sensor configuration has been requested it
+	 * is valid.
+	 */
+	if (sensorConfig && !sensorConfig->isValid()) {
+		LOG(RPI, Error) << "Invalid sensor configuration request";
+		return Invalid;
+	}
+
 	status = validateColorSpaces(ColorSpaceFlag::StreamsShareColorSpace);
 
 	/*
@@ -187,132 +174,203 @@
 	 * rotation and store the final combined transform that configure() will
 	 * need to apply to the sensor to save us working it out again.
 	 */
-	Transform requestedTransform = transform;
-	combinedTransform_ = data_->sensor_->validateTransform(&transform);
-	if (transform != requestedTransform)
+	Orientation requestedOrientation = orientation;
+	combinedTransform_ = data_->sensor_->computeTransform(&orientation);
+	if (orientation != requestedOrientation)
 		status = Adjusted;
 
-	std::vector<CameraData::StreamParams> rawStreams, outStreams;
+	rawStreams_.clear();
+	outStreams_.clear();
+
 	for (const auto &[index, cfg] : utils::enumerate(config_)) {
-		if (isRaw(cfg.pixelFormat))
-			rawStreams.emplace_back(index, &cfg);
+		if (PipelineHandlerBase::isRaw(cfg.pixelFormat))
+			rawStreams_.emplace_back(index, &cfg);
 		else
-			outStreams.emplace_back(index, &cfg);
+			outStreams_.emplace_back(index, &cfg);
 	}
 
 	/* Sort the streams so the highest resolution is first. */
-	std::sort(rawStreams.begin(), rawStreams.end(),
+	std::sort(rawStreams_.begin(), rawStreams_.end(),
 		  [](auto &l, auto &r) { return l.cfg->size > r.cfg->size; });
 
-	std::sort(outStreams.begin(), outStreams.end(),
+	std::sort(outStreams_.begin(), outStreams_.end(),
 		  [](auto &l, auto &r) { return l.cfg->size > r.cfg->size; });
 
-	/* Compute the sensor configuration. */
-	unsigned int bitDepth = defaultRawBitDepth;
-	if (!rawStreams.empty()) {
-		BayerFormat bayerFormat = BayerFormat::fromPixelFormat(rawStreams[0].cfg->pixelFormat);
+	/* Compute the sensor's format then do any platform specific fixups. */
+	unsigned int bitDepth;
+	Size sensorSize;
+
+	if (sensorConfig) {
+		/* Use the application provided sensor configuration. */
+		bitDepth = sensorConfig->bitDepth;
+		sensorSize = sensorConfig->outputSize;
+	} else if (!rawStreams_.empty()) {
+		/* Use the RAW stream format and size. */
+		BayerFormat bayerFormat = BayerFormat::fromPixelFormat(rawStreams_[0].cfg->pixelFormat);
 		bitDepth = bayerFormat.bitDepth;
+		sensorSize = rawStreams_[0].cfg->size;
+	} else {
+		bitDepth = defaultRawBitDepth;
+		sensorSize = outStreams_[0].cfg->size;
 	}
 
-	sensorFormat_ = data_->findBestFormat(rawStreams.empty() ? outStreams[0].cfg->size
-								 : rawStreams[0].cfg->size,
-					      bitDepth);
+	sensorFormat_ = data_->findBestFormat(sensorSize, bitDepth);
 
-	/* Do any platform specific fixups. */
-	status = data_->platformValidate(rawStreams, outStreams);
-	if (status == Invalid)
-		return Invalid;
+	/*
+	 * If a sensor configuration has been requested, it should apply
+	 * without modifications.
+	 */
+	if (sensorConfig) {
+		BayerFormat bayer = BayerFormat::fromMbusCode(sensorFormat_.code);
 
-	/* Further fixups on the RAW streams. */
-	for (auto &raw : rawStreams) {
-		StreamConfiguration &cfg = config_.at(raw.index);
-
-		V4L2DeviceFormat rawFormat;
-		rawFormat.fourcc = raw.dev->toV4L2PixelFormat(cfg.pixelFormat);
-		rawFormat.size = cfg.size;
-		rawFormat.colorSpace = cfg.colorSpace;
-
-		int ret = raw.dev->tryFormat(&rawFormat);
-		if (ret)
+		if (bayer.bitDepth != sensorConfig->bitDepth ||
+		    sensorFormat_.size != sensorConfig->outputSize) {
+			LOG(RPI, Error) << "Invalid sensor configuration: "
+					<< "bitDepth/size mismatch";
 			return Invalid;
-		/*
-		 * Some sensors change their Bayer order when they are h-flipped
-		 * or v-flipped, according to the transform. If this one does, we
-		 * must advertise the transformed Bayer order in the raw stream.
-		 * Note how we must fetch the "native" (i.e. untransformed) Bayer
-		 * order, because the sensor may currently be flipped!
-		 */
-		V4L2PixelFormat fourcc = rawFormat.fourcc;
-		if (data_->flipsAlterBayerOrder_) {
-			BayerFormat bayer = BayerFormat::fromV4L2PixelFormat(fourcc);
-			bayer.order = data_->nativeBayerOrder_;
-			bayer = bayer.transform(combinedTransform_);
-			fourcc = bayer.toV4L2PixelFormat();
 		}
+	}
 
-		PixelFormat inputPixFormat = fourcc.toPixelFormat();
-		if (raw.cfg->size != rawFormat.size || raw.cfg->pixelFormat != inputPixFormat) {
-			raw.cfg->size = rawFormat.size;
-			raw.cfg->pixelFormat = inputPixFormat;
+	/* Start with some initial generic RAW stream adjustments. */
+	for (auto &raw : rawStreams_) {
+		StreamConfiguration *rawStream = raw.cfg;
+
+		/*
+		 * Some sensors change their Bayer order when they are
+		 * h-flipped or v-flipped, according to the transform. Adjust
+		 * the RAW stream to match the computed sensor format by
+		 * applying the sensor Bayer order resulting from the transform
+		 * to the user request.
+		 */
+
+		BayerFormat cfgBayer = BayerFormat::fromPixelFormat(rawStream->pixelFormat);
+		cfgBayer.order = data_->sensor_->bayerOrder(combinedTransform_);
+
+		if (rawStream->pixelFormat != cfgBayer.toPixelFormat()) {
+			rawStream->pixelFormat = cfgBayer.toPixelFormat();
 			status = Adjusted;
 		}
+	}
 
-		raw.cfg->stride = rawFormat.planes[0].bpl;
-		raw.cfg->frameSize = rawFormat.planes[0].size;
+	/* Do any platform specific fixups. */
+	Status st = data_->platformValidate(this);
+	if (st == Invalid)
+		return Invalid;
+	else if (st == Adjusted)
+		status = Adjusted;
+
+	/* Further fixups on the RAW streams. */
+	for (auto &raw : rawStreams_) {
+		int ret = raw.dev->tryFormat(&raw.format);
+		if (ret)
+			return Invalid;
+
+		if (RPi::PipelineHandlerBase::updateStreamConfig(raw.cfg, raw.format))
+			status = Adjusted;
 	}
 
 	/* Further fixups on the ISP output streams. */
-	for (auto &out : outStreams) {
-		StreamConfiguration &cfg = config_.at(out.index);
-		PixelFormat &cfgPixFmt = cfg.pixelFormat;
-		V4L2VideoDevice::Formats fmts = out.dev->formats();
-
-		if (fmts.find(out.dev->toV4L2PixelFormat(cfgPixFmt)) == fmts.end()) {
-			/* If we cannot find a native format, use a default one. */
-			cfgPixFmt = formats::NV12;
-			status = Adjusted;
-		}
-
-		V4L2DeviceFormat format;
-		format.fourcc = out.dev->toV4L2PixelFormat(cfg.pixelFormat);
-		format.size = cfg.size;
-		/* We want to send the associated YCbCr info through to the driver. */
-		format.colorSpace = yuvColorSpace_;
-
-		LOG(RPI, Debug)
-			<< "Try color space " << ColorSpace::toString(cfg.colorSpace);
-
-		int ret = out.dev->tryFormat(&format);
-		if (ret)
-			return Invalid;
+	for (auto &out : outStreams_) {
 
 		/*
+		 * We want to send the associated YCbCr info through to the driver.
+		 *
 		 * But for RGB streams, the YCbCr info gets overwritten on the way back
 		 * so we must check against what the stream cfg says, not what we actually
 		 * requested (which carefully included the YCbCr info)!
 		 */
-		if (cfg.colorSpace != format.colorSpace) {
-			status = Adjusted;
-			LOG(RPI, Debug)
-				<< "Color space changed from "
-				<< ColorSpace::toString(cfg.colorSpace) << " to "
-				<< ColorSpace::toString(format.colorSpace);
-		}
+		out.format.colorSpace = yuvColorSpace_;
 
-		cfg.colorSpace = format.colorSpace;
-		cfg.stride = format.planes[0].bpl;
-		cfg.frameSize = format.planes[0].size;
+		LOG(RPI, Debug)
+			<< "Try color space " << ColorSpace::toString(out.cfg->colorSpace);
+
+		int ret = out.dev->tryFormat(&out.format);
+		if (ret)
+			return Invalid;
+
+		if (RPi::PipelineHandlerBase::updateStreamConfig(out.cfg, out.format))
+			status = Adjusted;
 	}
 
 	return status;
 }
 
+bool PipelineHandlerBase::isRgb(const PixelFormat &pixFmt)
+{
+	const PixelFormatInfo &info = PixelFormatInfo::info(pixFmt);
+	return info.colourEncoding == PixelFormatInfo::ColourEncodingRGB;
+}
+
+bool PipelineHandlerBase::isYuv(const PixelFormat &pixFmt)
+{
+	/* The code below would return true for raw mono streams, so weed those out first. */
+	if (PipelineHandlerBase::isRaw(pixFmt))
+		return false;
+
+	const PixelFormatInfo &info = PixelFormatInfo::info(pixFmt);
+	return info.colourEncoding == PixelFormatInfo::ColourEncodingYUV;
+}
+
+bool PipelineHandlerBase::isRaw(const PixelFormat &pixFmt)
+{
+	/* This test works for both Bayer and raw mono formats. */
+	return BayerFormat::fromPixelFormat(pixFmt).isValid();
+}
+
+/*
+ * Adjust a StreamConfiguration fields to match a video device format.
+ * Returns true if the StreamConfiguration has been adjusted.
+ */
+bool PipelineHandlerBase::updateStreamConfig(StreamConfiguration *stream,
+					     const V4L2DeviceFormat &format)
+{
+	const PixelFormat &pixFormat = format.fourcc.toPixelFormat();
+	bool adjusted = false;
+
+	if (stream->pixelFormat != pixFormat || stream->size != format.size) {
+		stream->pixelFormat = pixFormat;
+		stream->size = format.size;
+		adjusted = true;
+	}
+
+	if (stream->colorSpace != format.colorSpace) {
+		stream->colorSpace = format.colorSpace;
+		adjusted = true;
+		LOG(RPI, Debug)
+			<< "Color space changed from "
+			<< ColorSpace::toString(stream->colorSpace) << " to "
+			<< ColorSpace::toString(format.colorSpace);
+	}
+
+	stream->stride = format.planes[0].bpl;
+	stream->frameSize = format.planes[0].size;
+
+	return adjusted;
+}
+
+/*
+ * Populate and return a video device format using a StreamConfiguration. */
+V4L2DeviceFormat PipelineHandlerBase::toV4L2DeviceFormat(const V4L2VideoDevice *dev,
+							 const StreamConfiguration *stream)
+{
+	V4L2DeviceFormat deviceFormat;
+
+	const PixelFormatInfo &info = PixelFormatInfo::info(stream->pixelFormat);
+	deviceFormat.planesCount = info.numPlanes();
+	deviceFormat.fourcc = dev->toV4L2PixelFormat(stream->pixelFormat);
+	deviceFormat.size = stream->size;
+	deviceFormat.planes[0].bpl = stream->stride;
+	deviceFormat.colorSpace = stream->colorSpace;
+
+	return deviceFormat;
+}
+
 V4L2DeviceFormat PipelineHandlerBase::toV4L2DeviceFormat(const V4L2VideoDevice *dev,
 							 const V4L2SubdeviceFormat &format,
 							 BayerFormat::Packing packingReq)
 {
-	unsigned int mbus_code = format.mbus_code;
-	const PixelFormat pix = mbusCodeToPixelFormat(mbus_code, packingReq);
+	unsigned int code = format.code;
+	const PixelFormat pix = mbusCodeToPixelFormat(code, packingReq);
 	V4L2DeviceFormat deviceFormat;
 
 	deviceFormat.fourcc = dev->toV4L2PixelFormat(pix);
@@ -343,7 +401,7 @@
 		case StreamRole::Raw:
 			size = sensorSize;
 			sensorFormat = data->findBestFormat(size, defaultRawBitDepth);
-			pixelFormat = mbusCodeToPixelFormat(sensorFormat.mbus_code,
+			pixelFormat = mbusCodeToPixelFormat(sensorFormat.code,
 							    BayerFormat::Packing::CSI2);
 			ASSERT(pixelFormat.isValid());
 			colorSpace = ColorSpace::Raw;
@@ -352,7 +410,7 @@
 
 		case StreamRole::StillCapture:
 			fmts = data->ispFormats();
-			pixelFormat = formats::NV12;
+			pixelFormat = formats::YUV420;
 			/*
 			 * Still image codecs usually expect the sYCC color space.
 			 * Even RGB codecs will be fine as the RGB we get with the
@@ -386,7 +444,7 @@
 
 		case StreamRole::Viewfinder:
 			fmts = data->ispFormats();
-			pixelFormat = formats::ARGB8888;
+			pixelFormat = formats::XRGB8888;
 			colorSpace = ColorSpace::Sycc;
 			size = { 800, 600 };
 			bufferCount = 4;
@@ -416,7 +474,11 @@
 			 */
 			for (const auto &format : fmts) {
 				PixelFormat pf = format.first.toPixelFormat();
-				if (pf.isValid()) {
+				/*
+				 * Some V4L2 formats translate to the same pixel format (e.g. YU12, YM12
+				 * both give YUV420). We must avoid duplicating the range in this case.
+				 */
+				if (pf.isValid() && deviceFormats.find(pf) == deviceFormats.end()) {
 					const SizeRange &ispSizes = format.second[0];
 					deviceFormats[pf].emplace_back(ispSizes.min, sensorSize,
 								       ispSizes.hStep, ispSizes.vStep);
@@ -434,8 +496,6 @@
 		config->addConfiguration(cfg);
 	}
 
-	config->validate();
-
 	return config;
 }
 
@@ -449,46 +509,31 @@
 	for (auto const stream : data->streams_)
 		stream->clearFlags(StreamFlag::External);
 
-	std::vector<CameraData::StreamParams> rawStreams, ispStreams;
+	/*
+	 * Apply the format on the sensor with any cached transform.
+	 *
+	 * If the application has provided a sensor configuration apply it
+	 * instead of just applying a format.
+	 */
+	RPiCameraConfiguration *rpiConfig = static_cast<RPiCameraConfiguration *>(config);
+	V4L2SubdeviceFormat *sensorFormat = &rpiConfig->sensorFormat_;
 
-	for (unsigned i = 0; i < config->size(); i++) {
-		StreamConfiguration *cfg = &config->at(i);
-
-		if (isRaw(cfg->pixelFormat))
-			rawStreams.emplace_back(i, cfg);
-		else
-			ispStreams.emplace_back(i, cfg);
+	if (rpiConfig->sensorConfig) {
+		ret = data->sensor_->applyConfiguration(*rpiConfig->sensorConfig,
+							rpiConfig->combinedTransform_,
+							sensorFormat);
+	} else {
+		ret = data->sensor_->setFormat(sensorFormat,
+					       rpiConfig->combinedTransform_);
 	}
-
-	/* Sort the streams so the highest resolution is first. */
-	std::sort(rawStreams.begin(), rawStreams.end(),
-		  [](auto &l, auto &r) { return l.cfg->size > r.cfg->size; });
-
-	std::sort(ispStreams.begin(), ispStreams.end(),
-		  [](auto &l, auto &r) { return l.cfg->size > r.cfg->size; });
-
-	/* Apply the format on the sensor with any cached transform. */
-	const RPiCameraConfiguration *rpiConfig =
-				static_cast<const RPiCameraConfiguration *>(config);
-	V4L2SubdeviceFormat sensorFormat = rpiConfig->sensorFormat_;
-
-	ret = data->sensor_->setFormat(&sensorFormat, rpiConfig->combinedTransform_);
 	if (ret)
 		return ret;
 
-	/* Use the user requested packing/bit-depth. */
-	std::optional<BayerFormat::Packing> packing;
-	if (!rawStreams.empty()) {
-		BayerFormat bayerFormat =
-			BayerFormat::fromPixelFormat(rawStreams[0].cfg->pixelFormat);
-		packing = bayerFormat.packing;
-	}
-
 	/*
 	 * Platform specific internal stream configuration. This also assigns
 	 * external streams which get configured below.
 	 */
-	ret = data->platformConfigure(sensorFormat, packing, rawStreams, ispStreams);
+	ret = data->platformConfigure(rpiConfig);
 	if (ret)
 		return ret;
 
@@ -552,11 +597,11 @@
 		 */
 		link->setEnabled(true);
 		const MediaPad *sinkPad = link->sink();
-		ret = device->setFormat(sinkPad->index(), &sensorFormat);
+		ret = device->setFormat(sinkPad->index(), sensorFormat);
 		if (ret) {
 			LOG(RPI, Error) << "Failed to set format on " << device->entity()->name()
 					<< " pad " << sinkPad->index()
-					<< " with format  " << sensorFormat
+					<< " with format  " << *sensorFormat
 					<< ": " << ret;
 			return ret;
 		}
@@ -681,7 +726,8 @@
 	if (!data->isRunning())
 		return -EINVAL;
 
-	LOG(RPI, Debug) << "queueRequestDevice: New request.";
+	LOG(RPI, Debug) << "queueRequestDevice: New request sequence: "
+			<< request->sequence();
 
 	/* Push all buffers supplied in the Request to the respective streams. */
 	for (auto stream : data->streams_) {
@@ -788,40 +834,6 @@
 	 */
 	data->properties_.set(properties::ScalerCropMaximum, Rectangle{});
 
-	/*
-	 * We cache two things about the sensor in relation to transforms
-	 * (meaning horizontal and vertical flips): if they affect the Bayer
-	 * ordering, and what the "native" Bayer order is, when no transforms
-	 * are applied.
-	 *
-	 * If flips are supported verify if they affect the Bayer ordering
-	 * and what the "native" Bayer order is, when no transforms are
-	 * applied.
-	 *
-	 * We note that the sensor's cached list of supported formats is
-	 * already in the "native" order, with any flips having been undone.
-	 */
-	const V4L2Subdevice *sensor = data->sensor_->device();
-	const struct v4l2_query_ext_ctrl *hflipCtrl = sensor->controlInfo(V4L2_CID_HFLIP);
-	if (hflipCtrl) {
-		/* We assume it will support vflips too... */
-		data->flipsAlterBayerOrder_ = hflipCtrl->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT;
-	}
-
-	/* Look for a valid Bayer format. */
-	BayerFormat bayerFormat;
-	for (const auto &iter : data->sensorFormats_) {
-		bayerFormat = BayerFormat::fromMbusCode(iter.first);
-		if (bayerFormat.isValid())
-			break;
-	}
-
-	if (!bayerFormat.isValid()) {
-		LOG(RPI, Error) << "No Bayer format found";
-		return -EINVAL;
-	}
-	data->nativeBayerOrder_ = bayerFormat.order;
-
 	ret = platformRegister(cameraData, frontend, backend);
 	if (ret)
 		return ret;
@@ -855,7 +867,7 @@
 	 */
 	for (auto const &it : buffers) {
 		bufferIds.push_back(IPABuffer(mask | it.first,
-					      it.second->planes()));
+					      it.second.buffer->planes()));
 		data->bufferIds_.insert(mask | it.first);
 	}
 
@@ -938,7 +950,7 @@
 
 			if (score <= bestScore) {
 				bestScore = score;
-				bestFormat.mbus_code = mbusCode;
+				bestFormat.code = mbusCode;
 				bestFormat.size = size;
 			}
 
@@ -1164,7 +1176,6 @@
 
 int CameraData::configureIPA(const CameraConfiguration *config, ipa::RPi::ConfigResult *result)
 {
-	std::map<unsigned int, ControlInfoMap> entityControls;
 	ipa::RPi::ConfigParams params;
 	int ret;
 
@@ -1184,7 +1195,8 @@
 	}
 
 	/* Always send the user transform to the IPA. */
-	params.transform = static_cast<unsigned int>(config->transform);
+	Transform transform = config->orientation / Orientation::Rotate0;
+	params.transform = static_cast<unsigned int>(transform);
 
 	/* Ready the IPA - it must know about the sensor resolution. */
 	ret = ipa_->configure(sensorInfo_, params, result);
@@ -1386,6 +1398,8 @@
 		 * Tag the buffer as completed, returning it to the
 		 * application.
 		 */
+		LOG(RPI, Debug) << "Completing request buffer for stream "
+				<< stream->name();
 		pipe()->completeBuffer(request, buffer);
 	} else {
 		/*
@@ -1394,6 +1408,8 @@
 		 * unconditionally for internal streams), or there is no pending
 		 * request, so we can recycle it.
 		 */
+		LOG(RPI, Debug) << "Returning buffer to stream "
+				<< stream->name();
 		stream->returnBuffer(buffer);
 	}
 }
@@ -1437,6 +1453,9 @@
 		if (state_ != State::IpaComplete)
 			return;
 
+		LOG(RPI, Debug) << "Completing request sequence: "
+				<< request->sequence();
+
 		pipe()->completeRequest(request);
 		requestQueue_.pop();
 		requestCompleted = true;
@@ -1449,6 +1468,7 @@
 	if (state_ == State::IpaComplete &&
 	    ((ispOutputCount_ == ispOutputTotal_ && dropFrameCount_) ||
 	     requestCompleted)) {
+		LOG(RPI, Debug) << "Going into Idle state";
 		state_ = State::Idle;
 		if (dropFrameCount_) {
 			dropFrameCount_--;
diff --git a/src/libcamera/pipeline/rpi/common/pipeline_base.h b/src/libcamera/pipeline/rpi/common/pipeline_base.h
index dbabc61..f9cecf7 100644
--- a/src/libcamera/pipeline/rpi/common/pipeline_base.h
+++ b/src/libcamera/pipeline/rpi/common/pipeline_base.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2023, Raspberry Pi Ltd
  *
- * pipeline_base.h - Pipeline handler base class for Raspberry Pi devices
+ * Pipeline handler base class for Raspberry Pi devices
  */
 
 #include <map>
@@ -42,12 +42,13 @@
 /* Map of mbus codes to supported sizes reported by the sensor. */
 using SensorFormats = std::map<unsigned int, std::vector<Size>>;
 
+class RPiCameraConfiguration;
 class CameraData : public Camera::Private
 {
 public:
 	CameraData(PipelineHandler *pipe)
 		: Camera::Private(pipe), state_(State::Stopped),
-		  flipsAlterBayerOrder_(false), dropFrameCount_(0), buffersAllocated_(false),
+		  dropFrameCount_(0), buffersAllocated_(false),
 		  ispOutputCount_(0), ispOutputTotal_(0)
 	{
 	}
@@ -56,28 +57,8 @@
 	{
 	}
 
-	struct StreamParams {
-		StreamParams()
-			: index(0), cfg(nullptr), dev(nullptr)
-		{
-		}
-
-		StreamParams(unsigned int index_, StreamConfiguration *cfg_)
-			: index(index_), cfg(cfg_), dev(nullptr)
-		{
-		}
-
-		unsigned int index;
-		StreamConfiguration *cfg;
-		V4L2VideoDevice *dev;
-	};
-
-	virtual CameraConfiguration::Status platformValidate(std::vector<StreamParams> &rawStreams,
-							     std::vector<StreamParams> &outStreams) const = 0;
-	virtual int platformConfigure(const V4L2SubdeviceFormat &sensorFormat,
-				      std::optional<BayerFormat::Packing> packing,
-				      std::vector<StreamParams> &rawStreams,
-				      std::vector<StreamParams> &outStreams) = 0;
+	virtual CameraConfiguration::Status platformValidate(RPiCameraConfiguration *rpiConfig) const = 0;
+	virtual int platformConfigure(const RPiCameraConfiguration *rpiConfig) = 0;
 	virtual void platformStart() = 0;
 	virtual void platformStop() = 0;
 
@@ -150,10 +131,6 @@
 
 	std::queue<Request *> requestQueue_;
 
-	/* Store the "native" Bayer order (that is, with no transforms applied). */
-	bool flipsAlterBayerOrder_;
-	BayerFormat::Order nativeBayerOrder_;
-
 	/* For handling digital zoom. */
 	IPACameraSensorInfo sensorInfo_;
 	Rectangle ispCrop_; /* crop in ISP (camera mode) pixels */
@@ -211,6 +188,14 @@
 	{
 	}
 
+	static bool isRgb(const PixelFormat &pixFmt);
+	static bool isYuv(const PixelFormat &pixFmt);
+	static bool isRaw(const PixelFormat &pixFmt);
+
+	static bool updateStreamConfig(StreamConfiguration *stream,
+				       const V4L2DeviceFormat &format);
+	static V4L2DeviceFormat toV4L2DeviceFormat(const V4L2VideoDevice *dev,
+						   const StreamConfiguration *stream);
 	static V4L2DeviceFormat toV4L2DeviceFormat(const V4L2VideoDevice *dev,
 						   const V4L2SubdeviceFormat &format,
 						   BayerFormat::Packing packingReq);
@@ -264,16 +249,36 @@
 	/* The sensor format computed in validate() */
 	V4L2SubdeviceFormat sensorFormat_;
 
-private:
-	const CameraData *data_;
+	struct StreamParams {
+		StreamParams()
+			: index(0), cfg(nullptr), dev(nullptr)
+		{
+		}
+
+		StreamParams(unsigned int index_, StreamConfiguration *cfg_)
+			: index(index_), cfg(cfg_), dev(nullptr)
+		{
+		}
+
+		unsigned int index;
+		StreamConfiguration *cfg;
+		V4L2VideoDevice *dev;
+		V4L2DeviceFormat format;
+	};
+
+	std::vector<StreamParams> rawStreams_;
+	std::vector<StreamParams> outStreams_;
 
 	/*
 	 * Store the colour spaces that all our streams will have. RGB format streams
 	 * will have the same colorspace as YUV streams, with YCbCr field cleared and
 	 * range set to full.
-         */
+	 */
 	std::optional<ColorSpace> yuvColorSpace_;
 	std::optional<ColorSpace> rgbColorSpace_;
+
+private:
+	const CameraData *data_;
 };
 
 } /* namespace RPi */
diff --git a/src/libcamera/pipeline/rpi/common/rpi_stream.cpp b/src/libcamera/pipeline/rpi/common/rpi_stream.cpp
index 7319f51..accf59e 100644
--- a/src/libcamera/pipeline/rpi/common/rpi_stream.cpp
+++ b/src/libcamera/pipeline/rpi/common/rpi_stream.cpp
@@ -2,10 +2,14 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * rpi_stream.cpp - Raspberry Pi device stream abstraction class.
+ * Raspberry Pi device stream abstraction class.
  */
 #include "rpi_stream.h"
 
+#include <algorithm>
+#include <tuple>
+#include <utility>
+
 #include <libcamera/base/log.h>
 
 /* Maximum number of buffer slots to allocate in the V4L2 device driver. */
@@ -17,8 +21,13 @@
 
 namespace RPi {
 
+const BufferObject Stream::errorBufferObject{ nullptr, false };
+
 void Stream::setFlags(StreamFlags flags)
 {
+	/* We don't want dynamic mmapping. */
+	ASSERT(!(flags & StreamFlag::RequiresMmap));
+
 	flags_ |= flags;
 
 	/* Import streams cannot be external. */
@@ -27,6 +36,9 @@
 
 void Stream::clearFlags(StreamFlags flags)
 {
+	/* We don't want dynamic mmapping. */
+	ASSERT(!(flags & StreamFlag::RequiresMmap));
+
 	flags_ &= ~flags;
 }
 
@@ -45,6 +57,16 @@
 	return name_;
 }
 
+unsigned int Stream::swDownscale() const
+{
+	return swDownscale_;
+}
+
+void Stream::setSwDownscale(unsigned int swDownscale)
+{
+	swDownscale_ = swDownscale;
+}
+
 void Stream::resetBuffers()
 {
 	/* Add all internal buffers to the queue of usable buffers. */
@@ -56,7 +78,7 @@
 void Stream::setExportedBuffers(std::vector<std::unique_ptr<FrameBuffer>> *buffers)
 {
 	for (auto const &buffer : *buffers)
-		bufferMap_.emplace(++id_, buffer.get());
+		bufferEmplace(++id_, buffer.get());
 }
 
 const BufferMap &Stream::getBuffers() const
@@ -71,7 +93,7 @@
 
 	/* Find the buffer in the map, and return the buffer id. */
 	auto it = std::find_if(bufferMap_.begin(), bufferMap_.end(),
-			       [&buffer](auto const &p) { return p.second == buffer; });
+			       [&buffer](auto const &p) { return p.second.buffer == buffer; });
 
 	if (it == bufferMap_.end())
 		return 0;
@@ -81,7 +103,7 @@
 
 void Stream::setExportedBuffer(FrameBuffer *buffer)
 {
-	bufferMap_.emplace(++id_, buffer);
+	bufferEmplace(++id_, buffer);
 }
 
 int Stream::prepareBuffers(unsigned int count)
@@ -143,7 +165,7 @@
 
 void Stream::returnBuffer(FrameBuffer *buffer)
 {
-	if (!(flags_ & StreamFlag::External)) {
+	if (!(flags_ & StreamFlag::External) && !(flags_ & StreamFlag::Recurrent)) {
 		/* For internal buffers, simply requeue back to the device. */
 		queueToDevice(buffer);
 		return;
@@ -180,11 +202,32 @@
 	}
 }
 
+const BufferObject &Stream::getBuffer(unsigned int id)
+{
+	auto const &it = bufferMap_.find(id);
+	if (it == bufferMap_.end())
+		return errorBufferObject;
+
+	return it->second;
+}
+
+const BufferObject &Stream::acquireBuffer()
+{
+	/* No id provided, so pick up the next available buffer if possible. */
+	if (availableBuffers_.empty())
+		return errorBufferObject;
+
+	unsigned int id = getBufferId(availableBuffers_.front());
+	availableBuffers_.pop();
+
+	return getBuffer(id);
+}
+
 int Stream::queueAllBuffers()
 {
 	int ret;
 
-	if (flags_ & StreamFlag::External)
+	if ((flags_ & StreamFlag::External) || (flags_ & StreamFlag::Recurrent))
 		return 0;
 
 	while (!availableBuffers_.empty()) {
@@ -204,6 +247,16 @@
 	clearBuffers();
 }
 
+void Stream::bufferEmplace(unsigned int id, FrameBuffer *buffer)
+{
+	if (flags_ & StreamFlag::RequiresMmap)
+		bufferMap_.emplace(std::piecewise_construct, std::forward_as_tuple(id),
+				   std::forward_as_tuple(buffer, true));
+	else
+		bufferMap_.emplace(std::piecewise_construct, std::forward_as_tuple(id),
+				   std::forward_as_tuple(buffer, false));
+}
+
 void Stream::clearBuffers()
 {
 	availableBuffers_ = std::queue<FrameBuffer *>{};
diff --git a/src/libcamera/pipeline/rpi/common/rpi_stream.h b/src/libcamera/pipeline/rpi/common/rpi_stream.h
index 889b499..a13d5dc 100644
--- a/src/libcamera/pipeline/rpi/common/rpi_stream.h
+++ b/src/libcamera/pipeline/rpi/common/rpi_stream.h
@@ -2,27 +2,29 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * rpi_stream.h - Raspberry Pi device stream abstraction class.
+ * Raspberry Pi device stream abstraction class.
  */
 
 #pragma once
 
+#include <optional>
 #include <queue>
 #include <string>
 #include <unordered_map>
 #include <vector>
 
 #include <libcamera/base/flags.h>
+#include <libcamera/base/utils.h>
+
 #include <libcamera/stream.h>
 
+#include "libcamera/internal/mapped_framebuffer.h"
 #include "libcamera/internal/v4l2_videodevice.h"
 
 namespace libcamera {
 
 namespace RPi {
 
-using BufferMap = std::unordered_map<unsigned int, FrameBuffer *>;
-
 enum BufferMask {
 	MaskID			= 0x00ffff,
 	MaskStats		= 0x010000,
@@ -30,6 +32,21 @@
 	MaskBayerData		= 0x040000,
 };
 
+struct BufferObject {
+	BufferObject(FrameBuffer *b, bool requiresMmap)
+		: buffer(b), mapped(std::nullopt)
+	{
+		if (requiresMmap)
+			mapped = std::make_optional<MappedFrameBuffer>
+					(b, MappedFrameBuffer::MapFlag::ReadWrite);
+	}
+
+	FrameBuffer *buffer;
+	std::optional<MappedFrameBuffer> mapped;
+};
+
+using BufferMap = std::unordered_map<unsigned int, BufferObject>;
+
 /*
  * Device stream abstraction for either an internal or external stream.
  * Used for both Unicam and the ISP.
@@ -49,18 +66,35 @@
 		 * buffers might be provided by (and returned to) the application.
 		 */
 		External	= (1 << 1),
+		/*
+		 * Indicates that the stream buffers need to be mmaped and returned
+		 * to the pipeline handler when requested.
+		 */
+		RequiresMmap	= (1 << 2),
+		/*
+		 * Indicates a stream that needs buffers recycled every frame internally
+		 * in the pipeline handler, e.g. stitch, TDN, config. All buffer
+		 * management will be handled by the pipeline handler.
+		 */
+		Recurrent	= (1 << 3),
+		/*
+		 * Indicates that the output stream needs a software format conversion
+		 * to be applied after ISP processing.
+		 */
+		Needs32bitConv	= (1 << 4),
 	};
 
 	using StreamFlags = Flags<StreamFlag>;
 
 	Stream()
-		: flags_(StreamFlag::None), id_(0)
+		: flags_(StreamFlag::None), id_(0), swDownscale_(0)
 	{
 	}
 
 	Stream(const char *name, MediaEntity *dev, StreamFlags flags = StreamFlag::None)
 		: flags_(flags), name_(name),
-		  dev_(std::make_unique<V4L2VideoDevice>(dev)), id_(0)
+		  dev_(std::make_unique<V4L2VideoDevice>(dev)), id_(0),
+		  swDownscale_(0)
 	{
 	}
 
@@ -72,6 +106,9 @@
 	const std::string &name() const;
 	void resetBuffers();
 
+	unsigned int swDownscale() const;
+	void setSwDownscale(unsigned int swDownscale);
+
 	void setExportedBuffers(std::vector<std::unique_ptr<FrameBuffer>> *buffers);
 	const BufferMap &getBuffers() const;
 	unsigned int getBufferId(FrameBuffer *buffer) const;
@@ -82,10 +119,17 @@
 	int queueBuffer(FrameBuffer *buffer);
 	void returnBuffer(FrameBuffer *buffer);
 
+	const BufferObject &getBuffer(unsigned int id);
+	const BufferObject &acquireBuffer();
+
 	int queueAllBuffers();
 	void releaseBuffers();
 
+	/* For error handling. */
+	static const BufferObject errorBufferObject;
+
 private:
+	void bufferEmplace(unsigned int id, FrameBuffer *buffer);
 	void clearBuffers();
 	int queueToDevice(FrameBuffer *buffer);
 
@@ -100,6 +144,9 @@
 	/* Tracks a unique id key for the bufferMap_ */
 	unsigned int id_;
 
+	/* Power of 2 greater than one if software downscaling will be required. */
+	unsigned int swDownscale_;
+
 	/* All frame buffers associated with this device stream. */
 	BufferMap bufferMap_;
 
@@ -134,19 +181,14 @@
 template<typename E, std::size_t N>
 class Device : public std::array<class Stream, N>
 {
-private:
-	constexpr auto index(E e) const noexcept
-	{
-		return static_cast<std::underlying_type_t<E>>(e);
-	}
 public:
 	Stream &operator[](E e)
 	{
-		return std::array<class Stream, N>::operator[](index(e));
+		return std::array<class Stream, N>::operator[](utils::to_underlying(e));
 	}
 	const Stream &operator[](E e) const
 	{
-		return std::array<class Stream, N>::operator[](index(e));
+		return std::array<class Stream, N>::operator[](utils::to_underlying(e));
 	}
 };
 
diff --git a/src/libcamera/pipeline/rpi/vc4/data/meson.build b/src/libcamera/pipeline/rpi/vc4/data/meson.build
index cca5e38..179feeb 100644
--- a/src/libcamera/pipeline/rpi/vc4/data/meson.build
+++ b/src/libcamera/pipeline/rpi/vc4/data/meson.build
@@ -5,4 +5,5 @@
 ])
 
 install_data(conf_files,
-             install_dir : pipeline_data_dir / 'rpi' / 'vc4')
+             install_dir : pipeline_data_dir / 'rpi' / 'vc4',
+             install_tag : 'runtime')
diff --git a/src/libcamera/pipeline/rpi/vc4/dma_heaps.cpp b/src/libcamera/pipeline/rpi/vc4/dma_heaps.cpp
deleted file mode 100644
index 317b1fc..0000000
--- a/src/libcamera/pipeline/rpi/vc4/dma_heaps.cpp
+++ /dev/null
@@ -1,90 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2020, Raspberry Pi Ltd
- *
- * dma_heaps.h - Helper class for dma-heap allocations.
- */
-
-#include "dma_heaps.h"
-
-#include <array>
-#include <fcntl.h>
-#include <linux/dma-buf.h>
-#include <linux/dma-heap.h>
-#include <sys/ioctl.h>
-#include <unistd.h>
-
-#include <libcamera/base/log.h>
-
-/*
- * /dev/dma-heap/linux,cma is the dma-heap allocator, which allows dmaheap-cma
- * to only have to worry about importing.
- *
- * Annoyingly, should the cma heap size be specified on the kernel command line
- * instead of DT, the heap gets named "reserved" instead.
- */
-static constexpr std::array<const char *, 2> heapNames = {
-	"/dev/dma_heap/linux,cma",
-	"/dev/dma_heap/reserved"
-};
-
-namespace libcamera {
-
-LOG_DECLARE_CATEGORY(RPI)
-
-namespace RPi {
-
-DmaHeap::DmaHeap()
-{
-	for (const char *name : heapNames) {
-		int ret = ::open(name, O_RDWR | O_CLOEXEC, 0);
-		if (ret < 0) {
-			ret = errno;
-			LOG(RPI, Debug) << "Failed to open " << name << ": "
-					<< strerror(ret);
-			continue;
-		}
-
-		dmaHeapHandle_ = UniqueFD(ret);
-		break;
-	}
-
-	if (!dmaHeapHandle_.isValid())
-		LOG(RPI, Error) << "Could not open any dmaHeap device";
-}
-
-DmaHeap::~DmaHeap() = default;
-
-UniqueFD DmaHeap::alloc(const char *name, std::size_t size)
-{
-	int ret;
-
-	if (!name)
-		return {};
-
-	struct dma_heap_allocation_data alloc = {};
-
-	alloc.len = size;
-	alloc.fd_flags = O_CLOEXEC | O_RDWR;
-
-	ret = ::ioctl(dmaHeapHandle_.get(), DMA_HEAP_IOCTL_ALLOC, &alloc);
-	if (ret < 0) {
-		LOG(RPI, Error) << "dmaHeap allocation failure for "
-				<< name;
-		return {};
-	}
-
-	UniqueFD allocFd(alloc.fd);
-	ret = ::ioctl(allocFd.get(), DMA_BUF_SET_NAME, name);
-	if (ret < 0) {
-		LOG(RPI, Error) << "dmaHeap naming failure for "
-				<< name;
-		return {};
-	}
-
-	return allocFd;
-}
-
-} /* namespace RPi */
-
-} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/rpi/vc4/dma_heaps.h b/src/libcamera/pipeline/rpi/vc4/dma_heaps.h
deleted file mode 100644
index 0a4a8d8..0000000
--- a/src/libcamera/pipeline/rpi/vc4/dma_heaps.h
+++ /dev/null
@@ -1,32 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-/*
- * Copyright (C) 2020, Raspberry Pi Ltd
- *
- * dma_heaps.h - Helper class for dma-heap allocations.
- */
-
-#pragma once
-
-#include <stddef.h>
-
-#include <libcamera/base/unique_fd.h>
-
-namespace libcamera {
-
-namespace RPi {
-
-class DmaHeap
-{
-public:
-	DmaHeap();
-	~DmaHeap();
-	bool isValid() const { return dmaHeapHandle_.isValid(); }
-	UniqueFD alloc(const char *name, std::size_t size);
-
-private:
-	UniqueFD dmaHeapHandle_;
-};
-
-} /* namespace RPi */
-
-} /* namespace libcamera */
diff --git a/src/libcamera/pipeline/rpi/vc4/meson.build b/src/libcamera/pipeline/rpi/vc4/meson.build
index cdb049c..386e229 100644
--- a/src/libcamera/pipeline/rpi/vc4/meson.build
+++ b/src/libcamera/pipeline/rpi/vc4/meson.build
@@ -1,7 +1,6 @@
 # SPDX-License-Identifier: CC0-1.0
 
 libcamera_sources += files([
-    'dma_heaps.cpp',
     'vc4.cpp',
 ])
 
diff --git a/src/libcamera/pipeline/rpi/vc4/vc4.cpp b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
index 018cf48..e5b6ef2 100644
--- a/src/libcamera/pipeline/rpi/vc4/vc4.cpp
+++ b/src/libcamera/pipeline/rpi/vc4/vc4.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019-2023, Raspberry Pi Ltd
  *
- * vc4.cpp - Pipeline handler for VC4-based Raspberry Pi devices
+ * Pipeline handler for VC4-based Raspberry Pi devices
  */
 
 #include <linux/bcm2835-isp.h>
@@ -12,12 +12,11 @@
 #include <libcamera/formats.h>
 
 #include "libcamera/internal/device_enumerator.h"
+#include "libcamera/internal/dma_buf_allocator.h"
 
 #include "../common/pipeline_base.h"
 #include "../common/rpi_stream.h"
 
-#include "dma_heaps.h"
-
 using namespace std::chrono_literals;
 
 namespace libcamera {
@@ -25,6 +24,7 @@
 LOG_DECLARE_CATEGORY(RPI)
 
 using StreamFlag = RPi::Stream::StreamFlag;
+using StreamParams = RPi::RPiCameraConfiguration::StreamParams;
 
 namespace {
 
@@ -65,8 +65,7 @@
 	{
 	}
 
-	CameraConfiguration::Status platformValidate(std::vector<StreamParams> &rawStreams,
-						     std::vector<StreamParams> &outStreams) const override;
+	CameraConfiguration::Status platformValidate(RPi::RPiCameraConfiguration *rpiConfig) const override;
 
 	int platformPipelineConfigure(const std::unique_ptr<YamlObject> &root) override;
 
@@ -78,7 +77,7 @@
 	void ispOutputDequeue(FrameBuffer *buffer);
 
 	void processStatsComplete(const ipa::RPi::BufferIds &buffers);
-	void prepareIspComplete(const ipa::RPi::BufferIds &buffers);
+	void prepareIspComplete(const ipa::RPi::BufferIds &buffers, bool stitchSwapBuffers);
 	void setIspControls(const ControlList &controls);
 	void setCameraTimeout(uint32_t maxFrameLengthMs);
 
@@ -87,7 +86,7 @@
 	RPi::Device<Isp, 4> isp_;
 
 	/* DMAHEAP allocation helper. */
-	RPi::DmaHeap dmaHeap_;
+	DmaBufAllocator dmaHeap_;
 	SharedFD lsTable_;
 
 	struct Config {
@@ -115,10 +114,7 @@
 		isp_[Isp::Input].dev()->setSelection(V4L2_SEL_TGT_CROP, &ispCrop_);
 	}
 
-	int platformConfigure(const V4L2SubdeviceFormat &sensorFormat,
-			      std::optional<BayerFormat::Packing> packing,
-			      std::vector<StreamParams> &rawStreams,
-			      std::vector<StreamParams> &outStreams) override;
+	int platformConfigure(const RPi::RPiCameraConfiguration *rpiConfig) override;
 	int platformConfigureIpa(ipa::RPi::ConfigParams &params) override;
 
 	int platformInitIpa([[maybe_unused]] ipa::RPi::InitParams &params) override
@@ -261,10 +257,21 @@
 
 		} else if (stream == &data->unicam_[Unicam::Embedded]) {
 			/*
-			 * Embedded data buffers are (currently) for internal use,
-			 * so allocate the minimum required to avoid frame drops.
+			 * Embedded data buffers are (currently) for internal use, and
+			 * are small enough (typically 1-2KB) that we can
+			 * allocate them generously to avoid causing problems in the
+			 * IPA when we cannot supply the metadata.
+			 *
+			 * 12 are allocated as a typical application will have 8-10
+			 * input buffers, so allocating more embedded buffers than that
+			 * is a sensible choice.
+			 *
+			 * The lifetimes of these buffers are smaller than those of the
+			 * raw buffers, so allocating a fixed number will still suffice
+			 * if the application requests a greater number of raw
+			 * buffers, as these will be recycled quicker.
 			 */
-			numBuffers = minBuffers;
+			numBuffers = 12;
 		} else {
 			/*
 			 * Since the ISP runs synchronous with the IPA and requests,
@@ -275,6 +282,9 @@
 			numBuffers = 1;
 		}
 
+		LOG(RPI, Debug) << "Preparing " << numBuffers
+				<< " buffers for stream " << stream->name();
+
 		ret = stream->prepareBuffers(numBuffers);
 		if (ret < 0)
 			return ret;
@@ -394,9 +404,11 @@
 	return 0;
 }
 
-CameraConfiguration::Status Vc4CameraData::platformValidate(std::vector<StreamParams> &rawStreams,
-							    std::vector<StreamParams> &outStreams) const
+CameraConfiguration::Status Vc4CameraData::platformValidate(RPi::RPiCameraConfiguration *rpiConfig) const
 {
+	std::vector<StreamParams> &rawStreams = rpiConfig->rawStreams_;
+	std::vector<StreamParams> &outStreams = rpiConfig->outStreams_;
+
 	CameraConfiguration::Status status = CameraConfiguration::Status::Valid;
 
 	/* Can only output 1 RAW stream, or 2 YUV/RGB streams. */
@@ -405,9 +417,45 @@
 		return CameraConfiguration::Status::Invalid;
 	}
 
-	if (!rawStreams.empty())
+	if (!rawStreams.empty()) {
 		rawStreams[0].dev = unicam_[Unicam::Image].dev();
 
+		/* Adjust the RAW stream to match the computed sensor format. */
+		StreamConfiguration *rawStream = rawStreams[0].cfg;
+		BayerFormat rawBayer = BayerFormat::fromPixelFormat(rawStream->pixelFormat);
+
+		/* Apply the sensor bitdepth. */
+		rawBayer.bitDepth = BayerFormat::fromMbusCode(rpiConfig->sensorFormat_.code).bitDepth;
+
+		/* Default to CSI2 packing if the user request is unsupported. */
+		if (rawBayer.packing != BayerFormat::Packing::CSI2 &&
+		    rawBayer.packing != BayerFormat::Packing::None)
+			rawBayer.packing = BayerFormat::Packing::CSI2;
+
+		PixelFormat rawFormat = rawBayer.toPixelFormat();
+
+		/*
+		 * Try for an unpacked format if a packed one wasn't available.
+		 * This catches 8 (and 16) bit formats which would otherwise
+		 * fail.
+		 */
+		if (!rawFormat.isValid() && rawBayer.packing != BayerFormat::Packing::None) {
+			rawBayer.packing = BayerFormat::Packing::None;
+			rawFormat = rawBayer.toPixelFormat();
+		}
+
+		if (rawStream->pixelFormat != rawFormat ||
+		    rawStream->size != rpiConfig->sensorFormat_.size) {
+			rawStream->pixelFormat = rawFormat;
+			rawStream->size = rpiConfig->sensorFormat_.size;
+
+			status = CameraConfiguration::Adjusted;
+		}
+
+		rawStreams[0].format =
+			RPi::PipelineHandlerBase::toV4L2DeviceFormat(unicam_[Unicam::Image].dev(), rawStream);
+	}
+
 	/*
 	 * For the two ISP outputs, one stream must be equal or smaller than the
 	 * other in all dimensions.
@@ -417,6 +465,11 @@
 	for (unsigned int i = 0; i < outStreams.size(); i++) {
 		Size size;
 
+		/*
+		 * \todo Should we warn if upscaling, as it reduces the image
+		 * quality and is usually undesired ?
+		 */
+
 		size.width = std::min(outStreams[i].cfg->size.width,
 				      outStreams[0].cfg->size.width);
 		size.height = std::min(outStreams[i].cfg->size.height,
@@ -432,6 +485,8 @@
 		 * have that fixed up in the code above.
 		 */
 		outStreams[i].dev = isp_[i == 0 ? Isp::Output0 : Isp::Output1].dev();
+
+		outStreams[i].format = RPi::PipelineHandlerBase::toV4L2DeviceFormat(outStreams[i].dev, outStreams[i].cfg);
 	}
 
 	return status;
@@ -479,22 +534,14 @@
 	return 0;
 }
 
-int Vc4CameraData::platformConfigure(const V4L2SubdeviceFormat &sensorFormat,
-				     std::optional<BayerFormat::Packing> packing,
-				     std::vector<StreamParams> &rawStreams,
-				     std::vector<StreamParams> &outStreams)
+int Vc4CameraData::platformConfigure(const RPi::RPiCameraConfiguration *rpiConfig)
 {
+	const std::vector<StreamParams> &rawStreams = rpiConfig->rawStreams_;
+	const std::vector<StreamParams> &outStreams = rpiConfig->outStreams_;
 	int ret;
 
-	if (!packing)
-		packing = BayerFormat::Packing::CSI2;
-
 	V4L2VideoDevice *unicam = unicam_[Unicam::Image].dev();
-	V4L2DeviceFormat unicamFormat = RPi::PipelineHandlerBase::toV4L2DeviceFormat(unicam, sensorFormat, *packing);
-
-	ret = unicam->setFormat(&unicamFormat);
-	if (ret)
-		return ret;
+	V4L2DeviceFormat unicamFormat;
 
 	/*
 	 * See which streams are requested, and route the user
@@ -503,14 +550,24 @@
 	if (!rawStreams.empty()) {
 		rawStreams[0].cfg->setStream(&unicam_[Unicam::Image]);
 		unicam_[Unicam::Image].setFlags(StreamFlag::External);
+		unicamFormat = rawStreams[0].format;
+	} else {
+		unicamFormat =
+			RPi::PipelineHandlerBase::toV4L2DeviceFormat(unicam,
+								     rpiConfig->sensorFormat_,
+								     BayerFormat::Packing::CSI2);
 	}
 
+	ret = unicam->setFormat(&unicamFormat);
+	if (ret)
+		return ret;
+
 	ret = isp_[Isp::Input].dev()->setFormat(&unicamFormat);
 	if (ret)
 		return ret;
 
 	LOG(RPI, Info) << "Sensor: " << sensor_->id()
-		       << " - Selected sensor format: " << sensorFormat
+		       << " - Selected sensor format: " << rpiConfig->sensorFormat_
 		       << " - Selected unicam format: " << unicamFormat;
 
 	/* Use a sensible small default size if no output streams are configured. */
@@ -522,11 +579,7 @@
 
 		/* The largest resolution gets routed to the ISP Output 0 node. */
 		RPi::Stream *stream = i == 0 ? &isp_[Isp::Output0] : &isp_[Isp::Output1];
-
-		V4L2PixelFormat fourcc = stream->dev()->toV4L2PixelFormat(cfg->pixelFormat);
-		format.size = cfg->size;
-		format.fourcc = fourcc;
-		format.colorSpace = cfg->colorSpace;
+		format = outStreams[i].format;
 
 		LOG(RPI, Debug) << "Setting " << stream->name() << " to "
 				<< format;
@@ -535,13 +588,6 @@
 		if (ret)
 			return -EINVAL;
 
-		if (format.size != cfg->size || format.fourcc != fourcc) {
-			LOG(RPI, Error)
-				<< "Failed to set requested format on " << stream->name()
-				<< ", returned " << format;
-			return -EINVAL;
-		}
-
 		LOG(RPI, Debug)
 			<< "Stream " << stream->name() << " has color space "
 			<< ColorSpace::toString(cfg->colorSpace);
@@ -595,7 +641,7 @@
 	 * \todo If Output 1 format is not YUV420, Output 1 ought to be disabled as
 	 * colour denoise will not run.
 	 */
-	if (outStreams.size() == 1) {
+	if (outStreams.size() <= 1) {
 		V4L2VideoDevice *dev = isp_[Isp::Output1].dev();
 
 		V4L2DeviceFormat output1Format;
@@ -756,7 +802,7 @@
 void Vc4CameraData::ispOutputDequeue(FrameBuffer *buffer)
 {
 	RPi::Stream *stream = nullptr;
-	unsigned int index;
+	unsigned int index = 0;
 
 	if (!isRunning())
 		return;
@@ -804,7 +850,7 @@
 	if (!isRunning())
 		return;
 
-	FrameBuffer *buffer = isp_[Isp::Stats].getBuffers().at(buffers.stats & RPi::MaskID);
+	FrameBuffer *buffer = isp_[Isp::Stats].getBuffers().at(buffers.stats & RPi::MaskID).buffer;
 
 	handleStreamBuffer(buffer, &isp_[Isp::Stats]);
 
@@ -812,7 +858,8 @@
 	handleState();
 }
 
-void Vc4CameraData::prepareIspComplete(const ipa::RPi::BufferIds &buffers)
+void Vc4CameraData::prepareIspComplete(const ipa::RPi::BufferIds &buffers,
+				       [[maybe_unused]] bool stitchSwapBuffers)
 {
 	unsigned int embeddedId = buffers.embedded & RPi::MaskID;
 	unsigned int bayer = buffers.bayer & RPi::MaskID;
@@ -821,7 +868,7 @@
 	if (!isRunning())
 		return;
 
-	buffer = unicam_[Unicam::Image].getBuffers().at(bayer & RPi::MaskID);
+	buffer = unicam_[Unicam::Image].getBuffers().at(bayer & RPi::MaskID).buffer;
 	LOG(RPI, Debug) << "Input re-queue to ISP, buffer id " << (bayer & RPi::MaskID)
 			<< ", timestamp: " << buffer->metadata().timestamp;
 
@@ -829,7 +876,7 @@
 	ispOutputCount_ = 0;
 
 	if (sensorMetadata_ && embeddedId) {
-		buffer = unicam_[Unicam::Embedded].getBuffers().at(embeddedId & RPi::MaskID);
+		buffer = unicam_[Unicam::Embedded].getBuffers().at(embeddedId & RPi::MaskID).buffer;
 		handleStreamBuffer(buffer, &unicam_[Unicam::Embedded]);
 	}
 
@@ -908,6 +955,7 @@
 	params.requestControls = request->controls();
 	params.ipaContext = request->sequence();
 	params.delayContext = bayerFrame.delayContext;
+	params.buffers.embedded = 0;
 
 	if (embeddedBuffer) {
 		unsigned int embeddedId = unicam_[Unicam::Embedded].getBufferId(embeddedBuffer);
@@ -970,6 +1018,6 @@
 	return true;
 }
 
-REGISTER_PIPELINE_HANDLER(PipelineHandlerVc4)
+REGISTER_PIPELINE_HANDLER(PipelineHandlerVc4, "rpi/vc4")
 
 } /* namespace libcamera */
diff --git a/src/libcamera/pipeline/simple/simple.cpp b/src/libcamera/pipeline/simple/simple.cpp
index 05ba76b..eb36578 100644
--- a/src/libcamera/pipeline/simple/simple.cpp
+++ b/src/libcamera/pipeline/simple/simple.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2020, Laurent Pinchart
  * Copyright (C) 2019, Martijn Braam
  *
- * simple.cpp - Pipeline handler for simple pipelines
+ * Pipeline handler for simple pipelines
  */
 
 #include <algorithm>
@@ -13,8 +13,8 @@
 #include <memory>
 #include <queue>
 #include <set>
-#include <string>
 #include <string.h>
+#include <string>
 #include <unordered_map>
 #include <utility>
 #include <vector>
@@ -34,10 +34,10 @@
 #include "libcamera/internal/device_enumerator.h"
 #include "libcamera/internal/media_device.h"
 #include "libcamera/internal/pipeline_handler.h"
+#include "libcamera/internal/software_isp/software_isp.h"
 #include "libcamera/internal/v4l2_subdevice.h"
 #include "libcamera/internal/v4l2_videodevice.h"
 
-
 namespace libcamera {
 
 LOG_DEFINE_CATEGORY(SimplePipeline)
@@ -185,17 +185,24 @@
 	 * and the number of streams it supports.
 	 */
 	std::vector<std::pair<const char *, unsigned int>> converters;
+	/*
+	 * Using Software ISP is to be enabled per driver.
+	 *
+	 * The Software ISP can't be used together with the converters.
+	 */
+	bool swIspEnabled;
 };
 
 namespace {
 
 static const SimplePipelineInfo supportedDevices[] = {
-	{ "dcmipp", {} },
-	{ "imx7-csi", { { "pxp", 1 } } },
-	{ "j721e-csi2rx", {} },
-	{ "mxc-isi", {} },
-	{ "qcom-camss", {} },
-	{ "sun6i-csi", {} },
+	{ "dcmipp", {}, false },
+	{ "imx7-csi", { { "pxp", 1 } }, false },
+	{ "j721e-csi2rx", {}, false },
+	{ "mtk-seninf", { { "mtk-mdp", 3 } }, false },
+	{ "mxc-isi", {}, false },
+	{ "qcom-camss", {}, true },
+	{ "sun6i-csi", {}, false },
 };
 
 } /* namespace */
@@ -269,17 +276,22 @@
 	std::vector<Configuration> configs_;
 	std::map<PixelFormat, std::vector<const Configuration *>> formats_;
 
+	std::vector<std::unique_ptr<FrameBuffer>> conversionBuffers_;
+	std::queue<std::map<unsigned int, FrameBuffer *>> conversionQueue_;
+	bool useConversion_;
+
 	std::unique_ptr<Converter> converter_;
-	std::vector<std::unique_ptr<FrameBuffer>> converterBuffers_;
-	bool useConverter_;
-	std::queue<std::map<unsigned int, FrameBuffer *>> converterQueue_;
+	std::unique_ptr<SoftwareIsp> swIsp_;
 
 private:
 	void tryPipeline(unsigned int code, const Size &size);
 	static std::vector<const MediaPad *> routedSourcePads(MediaPad *sink);
 
-	void converterInputDone(FrameBuffer *buffer);
-	void converterOutputDone(FrameBuffer *buffer);
+	void conversionInputDone(FrameBuffer *buffer);
+	void conversionOutputDone(FrameBuffer *buffer);
+
+	void ispStatsReady();
+	void setSensorControls(const ControlList &sensorControls);
 };
 
 class SimpleCameraConfiguration : public CameraConfiguration
@@ -331,6 +343,7 @@
 	V4L2VideoDevice *video(const MediaEntity *entity);
 	V4L2Subdevice *subdev(const MediaEntity *entity);
 	MediaDevice *converter() { return converter_; }
+	bool swIspEnabled() const { return swIspEnabled_; }
 
 protected:
 	int queueRequestDevice(Camera *camera, Request *request) override;
@@ -359,6 +372,7 @@
 	std::map<const MediaEntity *, EntityData> entities_;
 
 	MediaDevice *converter_;
+	bool swIspEnabled_;
 };
 
 /* -----------------------------------------------------------------------------
@@ -503,8 +517,39 @@
 				<< "Failed to create converter, disabling format conversion";
 			converter_.reset();
 		} else {
-			converter_->inputBufferReady.connect(this, &SimpleCameraData::converterInputDone);
-			converter_->outputBufferReady.connect(this, &SimpleCameraData::converterOutputDone);
+			converter_->inputBufferReady.connect(this, &SimpleCameraData::conversionInputDone);
+			converter_->outputBufferReady.connect(this, &SimpleCameraData::conversionOutputDone);
+		}
+	}
+
+	/*
+	 * Instantiate Soft ISP if this is enabled for the given driver and no converter is used.
+	 */
+	if (!converter_ && pipe->swIspEnabled()) {
+		swIsp_ = std::make_unique<SoftwareIsp>(pipe, sensor_.get());
+		if (!swIsp_->isValid()) {
+			LOG(SimplePipeline, Warning)
+				<< "Failed to create software ISP, disabling software debayering";
+			swIsp_.reset();
+		} else {
+			/*
+			 * The inputBufferReady signal is emitted from the soft ISP thread,
+			 * and needs to be handled in the pipeline handler thread. Signals
+			 * implement queued delivery, but this works transparently only if
+			 * the receiver is bound to the target thread. As the
+			 * SimpleCameraData class doesn't inherit from the Object class, it
+			 * is not bound to any thread, and the signal would be delivered
+			 * synchronously. Instead, connect the signal to a lambda function
+			 * bound explicitly to the pipe, which is bound to the pipeline
+			 * handler thread. The function then simply forwards the call to
+			 * conversionInputDone().
+			 */
+			swIsp_->inputBufferReady.connect(pipe, [this](FrameBuffer *buffer) {
+				this->conversionInputDone(buffer);
+			});
+			swIsp_->outputBufferReady.connect(this, &SimpleCameraData::conversionOutputDone);
+			swIsp_->ispStatsReady.connect(this, &SimpleCameraData::ispStatsReady);
+			swIsp_->setSensorControls.connect(this, &SimpleCameraData::setSensorControls);
 		}
 	}
 
@@ -562,13 +607,13 @@
 	 * corresponding possible V4L2 pixel formats on the video node.
 	 */
 	V4L2SubdeviceFormat format{};
-	format.mbus_code = code;
+	format.code = code;
 	format.size = size;
 
 	int ret = setupFormats(&format, V4L2Subdevice::TryFormat);
 	if (ret < 0) {
 		/* Pipeline configuration failed, skip this configuration. */
-		format.mbus_code = code;
+		format.code = code;
 		format.size = size;
 		LOG(SimplePipeline, Debug)
 			<< "Sensor format " << format
@@ -576,7 +621,7 @@
 		return;
 	}
 
-	V4L2VideoDevice::Formats videoFormats = video_->formats(format.mbus_code);
+	V4L2VideoDevice::Formats videoFormats = video_->formats(format.code);
 
 	LOG(SimplePipeline, Debug)
 		<< "Adding configuration for " << format.size
@@ -598,12 +643,20 @@
 		config.captureFormat = pixelFormat;
 		config.captureSize = format.size;
 
-		if (!converter_) {
-			config.outputFormats = { pixelFormat };
-			config.outputSizes = config.captureSize;
-		} else {
+		if (converter_) {
 			config.outputFormats = converter_->formats(pixelFormat);
 			config.outputSizes = converter_->sizes(format.size);
+		} else if (swIsp_) {
+			config.outputFormats = swIsp_->formats(pixelFormat);
+			config.outputSizes = swIsp_->sizes(pixelFormat, format.size);
+			if (config.outputFormats.empty()) {
+				/* Do not use swIsp for unsupported pixelFormat's. */
+				config.outputFormats = { pixelFormat };
+				config.outputSizes = config.captureSize;
+			}
+		} else {
+			config.outputFormats = { pixelFormat };
+			config.outputSizes = config.captureSize;
 		}
 
 		configs_.push_back(config);
@@ -706,7 +759,7 @@
 			if (ret < 0)
 				return ret;
 
-			if (format->mbus_code != sourceFormat.mbus_code ||
+			if (format->code != sourceFormat.code ||
 			    format->size != sourceFormat.size) {
 				LOG(SimplePipeline, Debug)
 					<< "Source '" << source->entity()->name()
@@ -740,7 +793,7 @@
 	 * point converting an erroneous buffer.
 	 */
 	if (buffer->metadata().status != FrameMetadata::FrameSuccess) {
-		if (!useConverter_) {
+		if (!useConversion_) {
 			/* No conversion, just complete the request. */
 			Request *request = buffer->request();
 			pipe->completeBuffer(request, buffer);
@@ -749,23 +802,23 @@
 		}
 
 		/*
-		 * The converter is in use. Requeue the internal buffer for
-		 * capture (unless the stream is being stopped), and complete
-		 * the request with all the user-facing buffers.
+		 * The converter or Software ISP is in use. Requeue the internal
+		 * buffer for capture (unless the stream is being stopped), and
+		 * complete the request with all the user-facing buffers.
 		 */
 		if (buffer->metadata().status != FrameMetadata::FrameCancelled)
 			video_->queueBuffer(buffer);
 
-		if (converterQueue_.empty())
+		if (conversionQueue_.empty())
 			return;
 
 		Request *request = nullptr;
-		for (auto &item : converterQueue_.front()) {
+		for (auto &item : conversionQueue_.front()) {
 			FrameBuffer *outputBuffer = item.second;
 			request = outputBuffer->request();
 			pipe->completeBuffer(request, outputBuffer);
 		}
-		converterQueue_.pop();
+		conversionQueue_.pop();
 
 		if (request)
 			pipe->completeRequest(request);
@@ -782,9 +835,9 @@
 	 */
 	Request *request = buffer->request();
 
-	if (useConverter_ && !converterQueue_.empty()) {
+	if (useConversion_ && !conversionQueue_.empty()) {
 		const std::map<unsigned int, FrameBuffer *> &outputs =
-			converterQueue_.front();
+			conversionQueue_.front();
 		if (!outputs.empty()) {
 			FrameBuffer *outputBuffer = outputs.begin()->second;
 			if (outputBuffer)
@@ -797,18 +850,22 @@
 					buffer->metadata().timestamp);
 
 	/*
-	 * Queue the captured and the request buffer to the converter if format
-	 * conversion is needed. If there's no queued request, just requeue the
-	 * captured buffer for capture.
+	 * Queue the captured and the request buffer to the converter or Software
+	 * ISP if format conversion is needed. If there's no queued request, just
+	 * requeue the captured buffer for capture.
 	 */
-	if (useConverter_) {
-		if (converterQueue_.empty()) {
+	if (useConversion_) {
+		if (conversionQueue_.empty()) {
 			video_->queueBuffer(buffer);
 			return;
 		}
 
-		converter_->queueBuffers(buffer, converterQueue_.front());
-		converterQueue_.pop();
+		if (converter_)
+			converter_->queueBuffers(buffer, conversionQueue_.front());
+		else
+			swIsp_->queueBuffers(buffer, conversionQueue_.front());
+
+		conversionQueue_.pop();
 		return;
 	}
 
@@ -817,13 +874,13 @@
 	pipe->completeRequest(request);
 }
 
-void SimpleCameraData::converterInputDone(FrameBuffer *buffer)
+void SimpleCameraData::conversionInputDone(FrameBuffer *buffer)
 {
 	/* Queue the input buffer back for capture. */
 	video_->queueBuffer(buffer);
 }
 
-void SimpleCameraData::converterOutputDone(FrameBuffer *buffer)
+void SimpleCameraData::conversionOutputDone(FrameBuffer *buffer)
 {
 	SimplePipelineHandler *pipe = SimpleCameraData::pipe();
 
@@ -833,6 +890,19 @@
 		pipe->completeRequest(request);
 }
 
+void SimpleCameraData::ispStatsReady()
+{
+	/* \todo Use the DelayedControls class */
+	swIsp_->processStats(sensor_->getControls({ V4L2_CID_ANALOGUE_GAIN,
+						    V4L2_CID_EXPOSURE }));
+}
+
+void SimpleCameraData::setSensorControls(const ControlList &sensorControls)
+{
+	ControlList ctrls(sensorControls);
+	sensor_->setControls(&ctrls);
+}
+
 /* Retrieve all source pads connected to a sink pad through active routes. */
 std::vector<const MediaPad *> SimpleCameraData::routedSourcePads(MediaPad *sink)
 {
@@ -851,17 +921,17 @@
 
 	std::vector<const MediaPad *> pads;
 
-	for (const struct v4l2_subdev_route &route : routing) {
-		if (sink->index() != route.sink_pad ||
+	for (const V4L2Subdevice::Route &route : routing) {
+		if (sink->index() != route.sink.pad ||
 		    !(route.flags & V4L2_SUBDEV_ROUTE_FL_ACTIVE))
 			continue;
 
-		const MediaPad *pad = entity->getPadByIndex(route.source_pad);
+		const MediaPad *pad = entity->getPadByIndex(route.source.pad);
 		if (!pad) {
 			LOG(SimplePipeline, Warning)
 				<< "Entity " << entity->name()
 				<< " has invalid route source pad "
-				<< route.source_pad;
+				<< route.source.pad;
 		}
 
 		pads.push_back(pad);
@@ -881,6 +951,33 @@
 {
 }
 
+namespace {
+
+static Size adjustSize(const Size &requestedSize, const SizeRange &supportedSizes)
+{
+	ASSERT(supportedSizes.min <= supportedSizes.max);
+
+	if (supportedSizes.min == supportedSizes.max)
+		return supportedSizes.max;
+
+	unsigned int hStep = supportedSizes.hStep;
+	unsigned int vStep = supportedSizes.vStep;
+
+	if (hStep == 0)
+		hStep = supportedSizes.max.width - supportedSizes.min.width;
+	if (vStep == 0)
+		vStep = supportedSizes.max.height - supportedSizes.min.height;
+
+	Size adjusted = requestedSize.boundedTo(supportedSizes.max)
+				.expandedTo(supportedSizes.min);
+
+	return adjusted.shrunkBy(supportedSizes.min)
+		.alignedDownTo(hStep, vStep)
+		.grownBy(supportedSizes.min);
+}
+
+} /* namespace */
+
 CameraConfiguration::Status SimpleCameraConfiguration::validate()
 {
 	const CameraSensor *sensor = data_->sensor_.get();
@@ -889,9 +986,9 @@
 	if (config_.empty())
 		return Invalid;
 
-	Transform requestedTransform = transform;
-	combinedTransform_ = sensor->validateTransform(&transform);
-	if (transform != requestedTransform)
+	Orientation requestedOrientation = orientation;
+	combinedTransform_ = sensor->computeTransform(&orientation);
+	if (orientation != requestedOrientation)
 		status = Adjusted;
 
 	/* Cap the number of entries to the available streams. */
@@ -997,10 +1094,19 @@
 		}
 
 		if (!pipeConfig_->outputSizes.contains(cfg.size)) {
+			Size adjustedSize = pipeConfig_->captureSize;
+			/*
+			 * The converter (when present) may not be able to output
+			 * a size identical to its input size. The capture size is thus
+			 * not guaranteed to be a valid output size. In such cases, use
+			 * the smaller valid output size closest to the requested.
+			 */
+			if (!pipeConfig_->outputSizes.contains(adjustedSize))
+				adjustedSize = adjustSize(cfg.size, pipeConfig_->outputSizes);
 			LOG(SimplePipeline, Debug)
 				<< "Adjusting size from " << cfg.size
-				<< " to " << pipeConfig_->captureSize;
-			cfg.size = pipeConfig_->captureSize;
+				<< " to " << adjustedSize;
+			cfg.size = adjustedSize;
 			status = Adjusted;
 		}
 
@@ -1012,8 +1118,11 @@
 		/* Set the stride, frameSize and bufferCount. */
 		if (needConversion_) {
 			std::tie(cfg.stride, cfg.frameSize) =
-				data_->converter_->strideAndFrameSize(cfg.pixelFormat,
-								      cfg.size);
+				data_->converter_
+					? data_->converter_->strideAndFrameSize(cfg.pixelFormat,
+										cfg.size)
+					: data_->swIsp_->strideAndFrameSize(cfg.pixelFormat,
+									    cfg.size);
 			if (cfg.stride == 0)
 				return Invalid;
 		} else {
@@ -1120,7 +1229,7 @@
 
 	const SimpleCameraData::Configuration *pipeConfig = config->pipeConfig();
 	V4L2SubdeviceFormat format{};
-	format.mbus_code = pipeConfig->code;
+	format.code = pipeConfig->code;
 	format.size = pipeConfig->sensorSize;
 
 	ret = data->setupFormats(&format, V4L2Subdevice::ActiveFormat,
@@ -1156,14 +1265,14 @@
 
 	/* Configure the converter if needed. */
 	std::vector<std::reference_wrapper<StreamConfiguration>> outputCfgs;
-	data->useConverter_ = config->needConversion();
+	data->useConversion_ = config->needConversion();
 
 	for (unsigned int i = 0; i < config->size(); ++i) {
 		StreamConfiguration &cfg = config->at(i);
 
 		cfg.setStream(&data->streams_[i]);
 
-		if (data->useConverter_)
+		if (data->useConversion_)
 			outputCfgs.push_back(cfg);
 	}
 
@@ -1176,7 +1285,10 @@
 	inputCfg.stride = captureFormat.planes[0].bpl;
 	inputCfg.bufferCount = kNumInternalBuffers;
 
-	return data->converter_->configure(inputCfg, outputCfgs);
+	return data->converter_
+		       ? data->converter_->configure(inputCfg, outputCfgs)
+		       : data->swIsp_->configure(inputCfg, outputCfgs,
+						 data->sensor_->controls());
 }
 
 int SimplePipelineHandler::exportFrameBuffers(Camera *camera, Stream *stream,
@@ -1189,9 +1301,12 @@
 	 * Export buffers on the converter or capture video node, depending on
 	 * whether the converter is used or not.
 	 */
-	if (data->useConverter_)
-		return data->converter_->exportBuffers(data->streamIndex(stream),
-						       count, buffers);
+	if (data->useConversion_)
+		return data->converter_
+			       ? data->converter_->exportBuffers(data->streamIndex(stream),
+								 count, buffers)
+			       : data->swIsp_->exportBuffers(data->streamIndex(stream),
+							     count, buffers);
 	else
 		return data->video_->exportBuffers(count, buffers);
 }
@@ -1210,13 +1325,13 @@
 		return -EBUSY;
 	}
 
-	if (data->useConverter_) {
+	if (data->useConversion_) {
 		/*
 		 * When using the converter allocate a fixed number of internal
 		 * buffers.
 		 */
 		ret = video->allocateBuffers(kNumInternalBuffers,
-					     &data->converterBuffers_);
+					     &data->conversionBuffers_);
 	} else {
 		/* Otherwise, prepare for using buffers from the only stream. */
 		Stream *stream = &data->streams_[0];
@@ -1235,15 +1350,21 @@
 		return ret;
 	}
 
-	if (data->useConverter_) {
-		ret = data->converter_->start();
+	if (data->useConversion_) {
+		if (data->converter_)
+			ret = data->converter_->start();
+		else if (data->swIsp_)
+			ret = data->swIsp_->start();
+		else
+			ret = 0;
+
 		if (ret < 0) {
 			stop(camera);
 			return ret;
 		}
 
 		/* Queue all internal buffers for capture. */
-		for (std::unique_ptr<FrameBuffer> &buffer : data->converterBuffers_)
+		for (std::unique_ptr<FrameBuffer> &buffer : data->conversionBuffers_)
 			video->queueBuffer(buffer.get());
 	}
 
@@ -1255,15 +1376,19 @@
 	SimpleCameraData *data = cameraData(camera);
 	V4L2VideoDevice *video = data->video_;
 
-	if (data->useConverter_)
-		data->converter_->stop();
+	if (data->useConversion_) {
+		if (data->converter_)
+			data->converter_->stop();
+		else if (data->swIsp_)
+			data->swIsp_->stop();
+	}
 
 	video->streamOff();
 	video->releaseBuffers();
 
 	video->bufferReady.disconnect(data, &SimpleCameraData::bufferReady);
 
-	data->converterBuffers_.clear();
+	data->conversionBuffers_.clear();
 
 	releasePipeline(data);
 }
@@ -1281,7 +1406,7 @@
 		 * queue, it will be handed to the converter in the capture
 		 * completion handler.
 		 */
-		if (data->useConverter_) {
+		if (data->useConversion_) {
 			buffers.emplace(data->streamIndex(stream), buffer);
 		} else {
 			ret = data->video_->queueBuffer(buffer);
@@ -1290,8 +1415,8 @@
 		}
 	}
 
-	if (data->useConverter_)
-		data->converterQueue_.push(std::move(buffers));
+	if (data->useConversion_)
+		data->conversionQueue_.push(std::move(buffers));
 
 	return 0;
 }
@@ -1387,7 +1512,7 @@
 
 	LOG(SimplePipeline, Debug)
 		<< "Routing table of " << subdev->deviceNode()
-		<< " reset to " << routing.toString();
+		<< " reset to " << routing;
 
 	return 0;
 }
@@ -1418,6 +1543,8 @@
 		}
 	}
 
+	swIspEnabled_ = info->swIspEnabled;
+
 	/* Locate the sensors. */
 	std::vector<MediaEntity *> sensors = locateSensors();
 	if (sensors.empty()) {
@@ -1604,6 +1731,6 @@
 	}
 }
 
-REGISTER_PIPELINE_HANDLER(SimplePipelineHandler)
+REGISTER_PIPELINE_HANDLER(SimplePipelineHandler, "simple")
 
 } /* namespace libcamera */
diff --git a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
index 38f48a5..8a7409f 100644
--- a/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
+++ b/src/libcamera/pipeline/uvcvideo/uvcvideo.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * uvcvideo.cpp - Pipeline handler for uvcvideo devices
+ * Pipeline handler for uvcvideo devices
  */
 
 #include <algorithm>
@@ -111,8 +111,8 @@
 	if (config_.empty())
 		return Invalid;
 
-	if (transform != Transform::Identity) {
-		transform = Transform::Identity;
+	if (orientation != Orientation::Rotate0) {
+		orientation = Orientation::Rotate0;
 		status = Adjusted;
 	}
 
@@ -709,6 +709,6 @@
 	pipe()->completeRequest(request);
 }
 
-REGISTER_PIPELINE_HANDLER(PipelineHandlerUVC)
+REGISTER_PIPELINE_HANDLER(PipelineHandlerUVC, "uvcvideo")
 
 } /* namespace libcamera */
diff --git a/src/libcamera/pipeline/vimc/vimc.cpp b/src/libcamera/pipeline/vimc/vimc.cpp
index 00e6f4c..0ec9928 100644
--- a/src/libcamera/pipeline/vimc/vimc.cpp
+++ b/src/libcamera/pipeline/vimc/vimc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * vimc.cpp - Pipeline handler for the vimc device
+ * Pipeline handler for the vimc device
  */
 
 #include <algorithm>
@@ -114,6 +114,9 @@
 	{ formats::BGR888, MEDIA_BUS_FMT_RGB888_1X24 },
 };
 
+static constexpr Size kMinSize{ 16, 16 };
+static constexpr Size kMaxSize{ 4096, 2160 };
+
 } /* namespace */
 
 VimcCameraConfiguration::VimcCameraConfiguration(VimcCameraData *data)
@@ -128,8 +131,8 @@
 	if (config_.empty())
 		return Invalid;
 
-	if (transform != Transform::Identity) {
-		transform = Transform::Identity;
+	if (orientation != Orientation::Rotate0) {
+		orientation = Orientation::Rotate0;
 		status = Adjusted;
 	}
 
@@ -153,14 +156,20 @@
 	const Size size = cfg.size;
 
 	/*
-	 * The scaler hardcodes a x3 scale-up ratio, and the sensor output size
-	 * is aligned to two pixels in both directions. The output width and
-	 * height thus have to be multiples of 6.
+	 * The sensor output size is aligned to two pixels in both directions.
+	 * Additionally, prior to v5.16, the scaler hardcodes a x3 scale-up
+	 * ratio, requiring the output width and height to be multiples of 6.
 	 */
-	cfg.size.width = std::max(48U, std::min(4096U, cfg.size.width));
-	cfg.size.height = std::max(48U, std::min(2160U, cfg.size.height));
-	cfg.size.width -= cfg.size.width % 6;
-	cfg.size.height -= cfg.size.height % 6;
+	Size minSize{ kMinSize };
+	unsigned int alignment = 2;
+
+	if (data_->media_->version() < KERNEL_VERSION(5, 16, 0)) {
+		minSize *= 3;
+		alignment *= 3;
+	}
+
+	cfg.size.expandTo(minSize).boundTo(kMaxSize)
+		.alignDownTo(alignment, alignment);
 
 	if (cfg.size != size) {
 		LOG(VIMC, Debug)
@@ -216,10 +225,12 @@
 			}
 		}
 
-		/* The scaler hardcodes a x3 scale-up ratio. */
-		std::vector<SizeRange> sizes{
-			SizeRange{ { 48, 48 }, { 4096, 2160 } }
-		};
+		/* Prior to v5.16, the scaler hardcodes a x3 scale-up ratio. */
+		Size minSize{ kMinSize };
+		if (data->media_->version() < KERNEL_VERSION(5, 16, 0))
+			minSize *= 3;
+
+		std::vector<SizeRange> sizes{ { minSize, kMaxSize } };
 		formats[pixelformat.first] = sizes;
 	}
 
@@ -242,10 +253,18 @@
 	StreamConfiguration &cfg = config->at(0);
 	int ret;
 
-	/* The scaler hardcodes a x3 scale-up ratio. */
+	/*
+	 * Prior to v5.16, the scaler hardcodes a x3 scale-up ratio. For newer
+	 * kernels, use a sensor resolution of 1920x1080 and let the scaler
+	 * produce the requested stream size.
+	 */
+	Size sensorSize{ 1920, 1080 };
+	if (data->media_->version() < KERNEL_VERSION(5, 16, 0))
+		sensorSize = { cfg.size.width / 3, cfg.size.height / 3 };
+
 	V4L2SubdeviceFormat subformat = {};
-	subformat.mbus_code = MEDIA_BUS_FMT_SGRBG8_1X8;
-	subformat.size = { cfg.size.width / 3, cfg.size.height / 3 };
+	subformat.code = MEDIA_BUS_FMT_SGRBG8_1X8;
+	subformat.size = sensorSize;
 
 	ret = data->sensor_->setFormat(&subformat);
 	if (ret)
@@ -255,7 +274,7 @@
 	if (ret)
 		return ret;
 
-	subformat.mbus_code = pixelformats.find(cfg.pixelFormat)->second;
+	subformat.code = pixelformats.find(cfg.pixelFormat)->second;
 	ret = data->debayer_->setFormat(1, &subformat);
 	if (ret)
 		return ret;
@@ -293,7 +312,7 @@
 	 * vimc driver will fail pipeline validation.
 	 */
 	format.fourcc = V4L2PixelFormat(V4L2_PIX_FMT_SGRBG8);
-	format.size = { cfg.size.width / 3, cfg.size.height / 3 };
+	format.size = sensorSize;
 
 	ret = data->raw_->setFormat(&format);
 	if (ret)
@@ -623,6 +642,6 @@
 {
 }
 
-REGISTER_PIPELINE_HANDLER(PipelineHandlerVimc)
+REGISTER_PIPELINE_HANDLER(PipelineHandlerVimc, "vimc")
 
 } /* namespace libcamera */
diff --git a/src/libcamera/pipeline_handler.cpp b/src/libcamera/pipeline_handler.cpp
index 9c74c6c..5ea2ca7 100644
--- a/src/libcamera/pipeline_handler.cpp
+++ b/src/libcamera/pipeline_handler.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * pipeline_handler.cpp - Pipeline handler infrastructure
+ * Pipeline handler infrastructure
  */
 
 #include "libcamera/internal/pipeline_handler.h"
@@ -649,7 +649,7 @@
  */
 void PipelineHandler::hotplugMediaDevice(MediaDevice *media)
 {
-	media->disconnected.connect(this, [=]() { mediaDeviceDisconnected(media); });
+	media->disconnected.connect(this, [this, media] { mediaDeviceDisconnected(media); });
 }
 
 /**
@@ -795,6 +795,28 @@
 }
 
 /**
+ * \brief Return the factory for the pipeline handler with name \a name
+ * \param[in] name The pipeline handler name
+ * \return The factory of the pipeline with name \a name, or nullptr if not found
+ */
+const PipelineHandlerFactoryBase *PipelineHandlerFactoryBase::getFactoryByName(const std::string &name)
+{
+	const std::vector<PipelineHandlerFactoryBase *> &factories =
+		PipelineHandlerFactoryBase::factories();
+
+	auto iter = std::find_if(factories.begin(),
+				 factories.end(),
+				 [&name](const PipelineHandlerFactoryBase *f) {
+					 return f->name() == name;
+				 });
+
+	if (iter != factories.end())
+		return *iter;
+
+	return nullptr;
+}
+
+/**
  * \class PipelineHandlerFactory
  * \brief Registration of PipelineHandler classes and creation of instances
  * \tparam _PipelineHandler The pipeline handler class type for this factory
@@ -830,6 +852,8 @@
  * \def REGISTER_PIPELINE_HANDLER
  * \brief Register a pipeline handler with the pipeline handler factory
  * \param[in] handler Class name of PipelineHandler derived class to register
+ * \param[in] name Name assigned to the pipeline handler, matching the pipeline
+ * subdirectory name in the source tree.
  *
  * Register a PipelineHandler subclass with the factory and make it available to
  * try and match devices.
diff --git a/src/libcamera/pixel_format.cpp b/src/libcamera/pixel_format.cpp
index 80c2207..314179a 100644
--- a/src/libcamera/pixel_format.cpp
+++ b/src/libcamera/pixel_format.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * pixel_format.cpp - libcamera Pixel Format
+ * libcamera Pixel Format
  */
 
 #include <libcamera/formats.h>
diff --git a/src/libcamera/process.cpp b/src/libcamera/process.cpp
index 86a382f..86d27b2 100644
--- a/src/libcamera/process.cpp
+++ b/src/libcamera/process.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * process.cpp - Process object
+ * Process object
  */
 
 #include "libcamera/internal/process.h"
diff --git a/src/libcamera/property_ids.cpp.in b/src/libcamera/property_ids.cpp.in
index f917e33..8b274c3 100644
--- a/src/libcamera/property_ids.cpp.in
+++ b/src/libcamera/property_ids.cpp.in
@@ -23,14 +23,7 @@
 
 ${controls_doc}
 
-/**
- * \brief Namespace for libcamera draft properties
- */
-namespace draft {
-
-${draft_controls_doc}
-
-} /* namespace draft */
+${vendor_controls_doc}
 
 #ifndef __DOXYGEN__
 /*
@@ -39,11 +32,8 @@
  */
 ${controls_def}
 
-namespace draft {
+${vendor_controls_def}
 
-${draft_controls_def}
-
-} /* namespace draft */
 #endif
 
 /**
diff --git a/src/libcamera/property_ids.yaml b/src/libcamera/property_ids_core.yaml
similarity index 95%
rename from src/libcamera/property_ids.yaml
rename to src/libcamera/property_ids_core.yaml
index 5bddafc..834454a 100644
--- a/src/libcamera/property_ids.yaml
+++ b/src/libcamera/property_ids_core.yaml
@@ -4,6 +4,7 @@
 #
 %YAML 1.1
 ---
+vendor: libcamera
 controls:
   - Location:
       type: int32_t
@@ -29,10 +30,10 @@
   - Rotation:
       type: int32_t
       description: |
-        The camera rotation is expressed as the angular difference in degrees
-        between two reference systems, one relative to the camera module, and
-        one defined on the external world scene to be captured when projected
-        on the image sensor pixel array.
+        The camera physical mounting rotation. It is expressed as the angular
+        difference in degrees between two reference systems, one relative to the
+        camera module, and one defined on the external world scene to be
+        captured when projected on the image sensor pixel array.
 
         A camera sensor has a 2-dimensional reference system 'Rc' defined by
         its pixel array read-out order. The origin is set to the first pixel
@@ -700,37 +701,4 @@
 
         Different cameras may report identical devices.
 
-  # ----------------------------------------------------------------------------
-  # Draft properties section
-
-  - ColorFilterArrangement:
-      type: int32_t
-      draft: true
-      description: |
-        The arrangement of color filters on sensor; represents the colors in the
-        top-left 2x2 section of the sensor, in reading order. Currently
-        identical to ANDROID_SENSOR_INFO_COLOR_FILTER_ARRANGEMENT.
-      enum:
-        - name: RGGB
-          value: 0
-          description: RGGB Bayer pattern
-        - name: GRBG
-          value: 1
-          description: GRBG Bayer pattern
-        - name: GBRG
-          value: 2
-          description: GBRG Bayer pattern
-        - name: BGGR
-          value: 3
-          description: BGGR Bayer pattern
-        - name: RGB
-          value: 4
-          description: |
-            Sensor is not Bayer; output has 3 16-bit values for each pixel,
-            instead of just 1 16-bit value per pixel.
-        - name: MONO
-          value: 5
-          description: |
-            Sensor is not Bayer; output consists of a single colour channel.
-
 ...
diff --git a/src/libcamera/property_ids_draft.yaml b/src/libcamera/property_ids_draft.yaml
new file mode 100644
index 0000000..62f0e24
--- /dev/null
+++ b/src/libcamera/property_ids_draft.yaml
@@ -0,0 +1,39 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+#
+# Copyright (C) 2019, Google Inc.
+#
+%YAML 1.1
+---
+vendor: draft
+controls:
+  - ColorFilterArrangement:
+      type: int32_t
+      vendor: draft
+      description: |
+        The arrangement of color filters on sensor; represents the colors in the
+        top-left 2x2 section of the sensor, in reading order. Currently
+        identical to ANDROID_SENSOR_INFO_COLOR_FILTER_ARRANGEMENT.
+      enum:
+        - name: RGGB
+          value: 0
+          description: RGGB Bayer pattern
+        - name: GRBG
+          value: 1
+          description: GRBG Bayer pattern
+        - name: GBRG
+          value: 2
+          description: GBRG Bayer pattern
+        - name: BGGR
+          value: 3
+          description: BGGR Bayer pattern
+        - name: RGB
+          value: 4
+          description: |
+            Sensor is not Bayer; output has 3 16-bit values for each pixel,
+            instead of just 1 16-bit value per pixel.
+        - name: MONO
+          value: 5
+          description: |
+            Sensor is not Bayer; output consists of a single colour channel.
+
+...
diff --git a/src/libcamera/pub_key.cpp b/src/libcamera/pub_key.cpp
index 64dfa23..f1d73a5 100644
--- a/src/libcamera/pub_key.cpp
+++ b/src/libcamera/pub_key.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * pub_key.cpp - Public key signature verification
+ * Public key signature verification
  */
 
 #include "libcamera/internal/pub_key.h"
diff --git a/src/libcamera/request.cpp b/src/libcamera/request.cpp
index 949c556..cfb451e 100644
--- a/src/libcamera/request.cpp
+++ b/src/libcamera/request.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * request.cpp - Capture request handling
+ * Capture request handling
  */
 
 #include "libcamera/internal/request.h"
diff --git a/src/libcamera/camera_sensor.cpp b/src/libcamera/sensor/camera_sensor.cpp
similarity index 80%
rename from src/libcamera/camera_sensor.cpp
rename to src/libcamera/sensor/camera_sensor.cpp
index f3a5aa3..c6d7f80 100644
--- a/src/libcamera/camera_sensor.cpp
+++ b/src/libcamera/sensor/camera_sensor.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_sensor.cpp - A camera sensor
+ * A camera sensor
  */
 
 #include "libcamera/internal/camera_sensor.h"
@@ -15,6 +15,8 @@
 #include <math.h>
 #include <string.h>
 
+#include <libcamera/camera.h>
+#include <libcamera/orientation.h>
 #include <libcamera/property_ids.h>
 
 #include <libcamera/base/utils.h>
@@ -56,7 +58,7 @@
 CameraSensor::CameraSensor(const MediaEntity *entity)
 	: entity_(entity), pad_(UINT_MAX), staticProps_(nullptr),
 	  bayerFormat_(nullptr), supportFlips_(false),
-	  properties_(properties::properties)
+	  flipsAlterBayerOrder_(false), properties_(properties::properties)
 {
 }
 
@@ -186,23 +188,12 @@
 	 * Set HBLANK to the minimum to start with a well-defined line length,
 	 * allowing IPA modules that do not modify HBLANK to use the sensor
 	 * minimum line length in their calculations.
-	 *
-	 * 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.
-	 *
-	 * \todo The control API ought to have a flag to specify if a control
-	 * is read-only which could be used below.
 	 */
-	const ControlInfo hblank = ctrls.infoMap()->at(V4L2_CID_HBLANK);
-	const int32_t hblankMin = hblank.min().get<int32_t>();
-	const int32_t hblankMax = hblank.max().get<int32_t>();
-
-	if (hblankMin != hblankMax) {
+	const struct v4l2_query_ext_ctrl *hblankInfo = subdev_->controlInfo(V4L2_CID_HBLANK);
+	if (hblankInfo && !(hblankInfo->flags & V4L2_CTRL_FLAG_READ_ONLY)) {
 		ControlList ctrl(subdev_->controls());
 
-		ctrl.set(V4L2_CID_HBLANK, hblankMin);
+		ctrl.set(V4L2_CID_HBLANK, static_cast<int32_t>(hblankInfo->minimum));
 		ret = subdev_->setControls(&ctrl);
 		if (ret)
 			return ret;
@@ -211,6 +202,30 @@
 	return applyTestPatternMode(controls::draft::TestPatternModeEnum::TestPatternModeOff);
 }
 
+int CameraSensor::generateId()
+{
+	const std::string devPath = subdev_->devicePath();
+
+	/* Try to get ID from firmware description. */
+	id_ = sysfs::firmwareNodePath(devPath);
+	if (!id_.empty())
+		return 0;
+
+	/*
+	 * Virtual sensors not described in firmware
+	 *
+	 * Verify it's a platform device and construct ID from the device path
+	 * and model of sensor.
+	 */
+	if (devPath.find("/sys/devices/platform/", 0) == 0) {
+		id_ = devPath.substr(strlen("/sys/devices/")) + " " + model();
+		return 0;
+	}
+
+	LOG(CameraSensor, Error) << "Can't generate sensor ID";
+	return -EINVAL;
+}
+
 int CameraSensor::validateSensorDriver()
 {
 	int err = 0;
@@ -256,9 +271,14 @@
 	const struct v4l2_query_ext_ctrl *hflipInfo = subdev_->controlInfo(V4L2_CID_HFLIP);
 	const struct v4l2_query_ext_ctrl *vflipInfo = subdev_->controlInfo(V4L2_CID_VFLIP);
 	if (hflipInfo && !(hflipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY) &&
-	    vflipInfo && !(vflipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY))
+	    vflipInfo && !(vflipInfo->flags & V4L2_CTRL_FLAG_READ_ONLY)) {
 		supportFlips_ = true;
 
+		if (hflipInfo->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT ||
+		    vflipInfo->flags & V4L2_CTRL_FLAG_MODIFY_LAYOUT)
+			flipsAlterBayerOrder_ = true;
+	}
+
 	if (!supportFlips_)
 		LOG(CameraSensor, Debug)
 			<< "Camera sensor does not support horizontal/vertical flip";
@@ -432,11 +452,11 @@
 
 	/* Retrieve and register properties from the kernel interface. */
 	const ControlInfoMap &controls = subdev_->controls();
-	int32_t propertyValue;
 
 	const auto &orientation = controls.find(V4L2_CID_CAMERA_ORIENTATION);
 	if (orientation != controls.end()) {
 		int32_t v4l2Orientation = orientation->second.def().get<int32_t>();
+		int32_t propertyValue;
 
 		switch (v4l2Orientation) {
 		default:
@@ -461,18 +481,27 @@
 
 	const auto &rotationControl = controls.find(V4L2_CID_CAMERA_SENSOR_ROTATION);
 	if (rotationControl != controls.end()) {
+		int32_t propertyValue = rotationControl->second.def().get<int32_t>();
+
 		/*
-		 * validateTransform() compensates for the mounting rotation.
-		 * However, as a camera sensor can only compensate rotations
-		 * by applying H/VFlips, only rotation of 180 degrees are
-		 * automatically compensated. The other valid rotations (Rot90
-		 * and Rot270) require transposition, which the camera sensor
-		 * cannot perform, so leave them untouched.
+		 * Cache the Transform associated with the camera mounting
+		 * rotation for later use in computeTransform().
 		 */
-		propertyValue = rotationControl->second.def().get<int32_t>();
-		if (propertyValue == 180 && supportFlips_)
-			propertyValue = 0;
+		bool success;
+		mountingOrientation_ = orientationFromRotation(propertyValue, &success);
+		if (!success) {
+			LOG(CameraSensor, Warning)
+				<< "Invalid rotation of " << propertyValue
+				<< " degrees - ignoring";
+			mountingOrientation_ = Orientation::Rotate0;
+		}
+
 		properties_.set(properties::Rotation, propertyValue);
+	} else {
+		LOG(CameraSensor, Warning)
+			<< "Rotation control not available, default to 0 degrees";
+		properties_.set(properties::Rotation, 0);
+		mountingOrientation_ = Orientation::Rotate0;
 	}
 
 	properties_.set(properties::PixelArraySize, pixelArraySize_);
@@ -568,6 +597,21 @@
  */
 
 /**
+ * \fn CameraSensor::device()
+ * \brief Retrieve the camera sensor device
+ * \todo Remove this function by integrating DelayedControl with CameraSensor
+ * \return The camera sensor device
+ */
+
+/**
+ * \fn CameraSensor::focusLens()
+ * \brief Retrieve the focus lens controller
+ *
+ * \return The focus lens controller. nullptr if no focus lens controller is
+ * connected to the sensor
+ */
+
+/**
  * \fn CameraSensor::mbusCodes()
  * \brief Retrieve the media bus codes supported by the camera sensor
  *
@@ -620,64 +664,6 @@
 }
 
 /**
- * \fn CameraSensor::testPatternModes()
- * \brief Retrieve all the supported test pattern modes of the camera sensor
- * The test pattern mode values correspond to the controls::TestPattern control.
- *
- * \return The list of test pattern modes
- */
-
-/**
- * \brief Set the test pattern mode for the camera sensor
- * \param[in] mode The test pattern mode
- *
- * The new \a mode is applied to the sensor if it differs from the active test
- * pattern mode. Otherwise, this function is a no-op. Setting the same test
- * pattern mode for every frame thus incurs no performance penalty.
- */
-int CameraSensor::setTestPatternMode(controls::draft::TestPatternModeEnum mode)
-{
-	if (testPatternMode_ == mode)
-		return 0;
-
-	if (testPatternModes_.empty()) {
-		LOG(CameraSensor, Error)
-			<< "Camera sensor does not support test pattern modes.";
-		return -EINVAL;
-	}
-
-	return applyTestPatternMode(mode);
-}
-
-int CameraSensor::applyTestPatternMode(controls::draft::TestPatternModeEnum mode)
-{
-	if (testPatternModes_.empty())
-		return 0;
-
-	auto it = std::find(testPatternModes_.begin(), testPatternModes_.end(),
-			    mode);
-	if (it == testPatternModes_.end()) {
-		LOG(CameraSensor, Error) << "Unsupported test pattern mode "
-					 << mode;
-		return -EINVAL;
-	}
-
-	LOG(CameraSensor, Debug) << "Apply test pattern mode " << mode;
-
-	int32_t index = staticProps_->testPatternModes.at(mode);
-	ControlList ctrls{ controls() };
-	ctrls.set(V4L2_CID_TEST_PATTERN, index);
-
-	int ret = setControls(&ctrls);
-	if (ret)
-		return ret;
-
-	testPatternMode_ = mode;
-
-	return 0;
-}
-
-/**
  * \brief Retrieve the best sensor format for a desired output
  * \param[in] mbusCodes The list of acceptable media bus codes
  * \param[in] size The desired size
@@ -757,7 +743,7 @@
 	}
 
 	V4L2SubdeviceFormat format{
-		.mbus_code = bestCode,
+		.code = bestCode,
 		.size = *bestSize,
 		.colorSpace = ColorSpace::Raw,
 	};
@@ -801,7 +787,7 @@
 	if (ret)
 		return ret;
 
-	updateControlInfo();
+	subdev_->updateControlInfo();
 	return 0;
 }
 
@@ -823,13 +809,277 @@
 }
 
 /**
+ * \brief Apply a sensor configuration to the camera sensor
+ * \param[in] config The sensor configuration
+ * \param[in] transform The transform to be applied on the sensor.
+ * Defaults to Identity
+ * \param[out] sensorFormat Format applied to the sensor (optional)
+ *
+ * Apply to the camera sensor the configuration \a config.
+ *
+ * \todo The configuration shall be fully populated and if any of the fields
+ * specified cannot be applied exactly, an error code is returned.
+ *
+ * \return 0 if \a config is applied correctly to the camera sensor, a negative
+ * error code otherwise
+ */
+int CameraSensor::applyConfiguration(const SensorConfiguration &config,
+				     Transform transform,
+				     V4L2SubdeviceFormat *sensorFormat)
+{
+	if (!config.isValid()) {
+		LOG(CameraSensor, Error) << "Invalid sensor configuration";
+		return -EINVAL;
+	}
+
+	std::vector<unsigned int> filteredCodes;
+	std::copy_if(mbusCodes_.begin(), mbusCodes_.end(),
+		     std::back_inserter(filteredCodes),
+		     [&config](unsigned int mbusCode) {
+			     BayerFormat bayer = BayerFormat::fromMbusCode(mbusCode);
+			     if (bayer.bitDepth == config.bitDepth)
+				     return true;
+			     return false;
+		     });
+	if (filteredCodes.empty()) {
+		LOG(CameraSensor, Error)
+			<< "Cannot find any format with bit depth "
+			<< config.bitDepth;
+		return -EINVAL;
+	}
+
+	/*
+	 * Compute the sensor's data frame size by applying the cropping
+	 * rectangle, subsampling and output crop to the sensor's pixel array
+	 * size.
+	 *
+	 * \todo The actual size computation is for now ignored and only the
+	 * output size is considered. This implies that resolutions obtained
+	 * with two different cropping/subsampling will look identical and
+	 * only the first found one will be considered.
+	 */
+	V4L2SubdeviceFormat subdevFormat = {};
+	for (unsigned int code : filteredCodes) {
+		for (const Size &size : sizes(code)) {
+			if (size.width != config.outputSize.width ||
+			    size.height != config.outputSize.height)
+				continue;
+
+			subdevFormat.code = code;
+			subdevFormat.size = size;
+			break;
+		}
+	}
+	if (!subdevFormat.code) {
+		LOG(CameraSensor, Error) << "Invalid output size in sensor configuration";
+		return -EINVAL;
+	}
+
+	int ret = setFormat(&subdevFormat, transform);
+	if (ret)
+		return ret;
+
+	/*
+	 * Return to the caller the format actually applied to the sensor.
+	 * This is relevant if transform has changed the bayer pattern order.
+	 */
+	if (sensorFormat)
+		*sensorFormat = subdevFormat;
+
+	/* \todo Handle AnalogCrop. Most sensors do not support set_selection */
+	/* \todo Handle scaling in the digital domain. */
+
+	return 0;
+}
+
+/**
+ * \fn CameraSensor::properties()
+ * \brief Retrieve the camera sensor properties
+ * \return The list of camera sensor properties
+ */
+
+/**
+ * \brief Assemble and return the camera sensor info
+ * \param[out] info The camera sensor info
+ *
+ * This function fills \a info with information that describes the camera sensor
+ * and its current configuration. The information combines static data (such as
+ * the the sensor model or active pixel array size) and data specific to the
+ * current sensor configuration (such as the line length and pixel rate).
+ *
+ * Sensor information is only available for raw sensors. When called for a YUV
+ * sensor, this function returns -EINVAL.
+ *
+ * \return 0 on success, a negative error code otherwise
+ */
+int CameraSensor::sensorInfo(IPACameraSensorInfo *info) const
+{
+	if (!bayerFormat_)
+		return -EINVAL;
+
+	info->model = model();
+
+	/*
+	 * The active area size is a static property, while the crop
+	 * rectangle needs to be re-read as it depends on the sensor
+	 * configuration.
+	 */
+	info->activeAreaSize = { activeArea_.width, activeArea_.height };
+
+	/*
+	 * \todo Support for retreiving the crop rectangle is scheduled to
+	 * become mandatory. For the time being use the default value if it has
+	 * been initialized at sensor driver validation time.
+	 */
+	int ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP, &info->analogCrop);
+	if (ret) {
+		info->analogCrop = activeArea_;
+		LOG(CameraSensor, Warning)
+			<< "The analogue crop rectangle has been defaulted to the active area size";
+	}
+
+	/*
+	 * IPACameraSensorInfo::analogCrop::x and IPACameraSensorInfo::analogCrop::y
+	 * are defined relatively to the active pixel area, while V4L2's
+	 * TGT_CROP target is defined in respect to the full pixel array.
+	 *
+	 * Compensate it by subtracting the active area offset.
+	 */
+	info->analogCrop.x -= activeArea_.x;
+	info->analogCrop.y -= activeArea_.y;
+
+	/* The bit depth and image size depend on the currently applied format. */
+	V4L2SubdeviceFormat format{};
+	ret = subdev_->getFormat(pad_, &format);
+	if (ret)
+		return ret;
+
+	info->bitsPerPixel = MediaBusFormatInfo::info(format.code).bitsPerPixel;
+	info->outputSize = format.size;
+
+	std::optional<int32_t> cfa = properties_.get(properties::draft::ColorFilterArrangement);
+	info->cfaPattern = cfa ? *cfa : properties::draft::RGB;
+
+	/*
+	 * Retrieve the pixel rate, line length and minimum/maximum frame
+	 * duration through V4L2 controls. Support for the V4L2_CID_PIXEL_RATE,
+	 * V4L2_CID_HBLANK and V4L2_CID_VBLANK controls is mandatory.
+	 */
+	ControlList ctrls = subdev_->getControls({ V4L2_CID_PIXEL_RATE,
+						   V4L2_CID_HBLANK,
+						   V4L2_CID_VBLANK });
+	if (ctrls.empty()) {
+		LOG(CameraSensor, Error)
+			<< "Failed to retrieve camera info controls";
+		return -EINVAL;
+	}
+
+	info->pixelRate = ctrls.get(V4L2_CID_PIXEL_RATE).get<int64_t>();
+
+	const ControlInfo hblank = ctrls.infoMap()->at(V4L2_CID_HBLANK);
+	info->minLineLength = info->outputSize.width + hblank.min().get<int32_t>();
+	info->maxLineLength = info->outputSize.width + hblank.max().get<int32_t>();
+
+	const ControlInfo vblank = ctrls.infoMap()->at(V4L2_CID_VBLANK);
+	info->minFrameLength = info->outputSize.height + vblank.min().get<int32_t>();
+	info->maxFrameLength = info->outputSize.height + vblank.max().get<int32_t>();
+
+	return 0;
+}
+
+/**
+ * \brief Compute the Transform that gives the requested \a orientation
+ * \param[inout] orientation The desired image orientation
+ *
+ * This function computes the Transform that the pipeline handler should apply
+ * to the CameraSensor to obtain the requested \a orientation.
+ *
+ * The intended caller of this function is the validate() implementation of
+ * pipeline handlers, that pass in the application requested
+ * CameraConfiguration::orientation and obtain a Transform to apply to the
+ * camera sensor, likely at configure() time.
+ *
+ * If the requested \a orientation cannot be obtained, the \a orientation
+ * parameter is adjusted to report the current image orientation and
+ * Transform::Identity is returned.
+ *
+ * If the requested \a orientation can be obtained, the function computes a
+ * Transform and does not adjust \a orientation.
+ *
+ * Pipeline handlers are expected to verify if \a orientation has been
+ * adjusted by this function and set the CameraConfiguration::status to
+ * Adjusted accordingly.
+ *
+ * \return A Transform instance that applied to the CameraSensor produces images
+ * with \a orientation
+ */
+Transform CameraSensor::computeTransform(Orientation *orientation) const
+{
+	/*
+	 * If we cannot do any flips we cannot change the native camera mounting
+	 * orientation.
+	 */
+	if (!supportFlips_) {
+		*orientation = mountingOrientation_;
+		return Transform::Identity;
+	}
+
+	/*
+	 * Now compute the required transform to obtain 'orientation' starting
+	 * from the mounting rotation.
+	 *
+	 * As a note:
+	 * 	orientation / mountingOrientation_ = transform
+	 * 	mountingOrientation_ * transform = orientation
+	 */
+	Transform transform = *orientation / mountingOrientation_;
+
+	/*
+	 * If transform contains any Transpose we cannot do it, so adjust
+	 * 'orientation' to report the image native orientation and return Identity.
+	 */
+	if (!!(transform & Transform::Transpose)) {
+		*orientation = mountingOrientation_;
+		return Transform::Identity;
+	}
+
+	return transform;
+}
+
+/**
+ * \brief Compute the Bayer order that results from the given Transform
+ * \param[in] t The Transform to apply to the sensor
+ *
+ * Some sensors change their Bayer order when they are h-flipped or v-flipped.
+ * This function computes and returns the Bayer order that would result from the
+ * given transform applied to the sensor.
+ *
+ * This function is valid only when the sensor produces raw Bayer formats.
+ *
+ * \return The Bayer order produced by the sensor when the Transform is applied
+ */
+BayerFormat::Order CameraSensor::bayerOrder(Transform t) const
+{
+	/* Return a defined by meaningless value for non-Bayer sensors. */
+	if (!bayerFormat_)
+		return BayerFormat::Order::BGGR;
+
+	if (!flipsAlterBayerOrder_)
+		return bayerFormat_->order;
+
+	/*
+	 * Apply the transform to the native (i.e. untransformed) Bayer order,
+	 * using the rest of the Bayer format supplied by the caller.
+	 */
+	return bayerFormat_->transform(t).order;
+}
+
+/**
  * \brief Retrieve the supported V4L2 controls and their information
  *
  * Control information is updated automatically to reflect the current sensor
  * configuration when the setFormat() function is called, without invalidating
- * any iterator on the ControlInfoMap. A manual update can also be forced by
- * calling the updateControlInfo() function for pipeline handlers that change
- * the sensor configuration wihtout using setFormat().
+ * any iterator on the ControlInfoMap.
  *
  * \return A map of the V4L2 controls supported by the sensor
  */
@@ -892,231 +1142,66 @@
 }
 
 /**
- * \fn CameraSensor::device()
- * \brief Retrieve the camera sensor device
- * \todo Remove this function by integrating DelayedControl with CameraSensor
- * \return The camera sensor device
+ * \fn CameraSensor::testPatternModes()
+ * \brief Retrieve all the supported test pattern modes of the camera sensor
+ * The test pattern mode values correspond to the controls::TestPattern control.
+ *
+ * \return The list of test pattern modes
  */
 
 /**
- * \fn CameraSensor::properties()
- * \brief Retrieve the camera sensor properties
- * \return The list of camera sensor properties
+ * \brief Set the test pattern mode for the camera sensor
+ * \param[in] mode The test pattern mode
+ *
+ * The new \a mode is applied to the sensor if it differs from the active test
+ * pattern mode. Otherwise, this function is a no-op. Setting the same test
+ * pattern mode for every frame thus incurs no performance penalty.
  */
-
-/**
- * \brief Assemble and return the camera sensor info
- * \param[out] info The camera sensor info
- *
- * This function fills \a info with information that describes the camera sensor
- * and its current configuration. The information combines static data (such as
- * the the sensor model or active pixel array size) and data specific to the
- * current sensor configuration (such as the line length and pixel rate).
- *
- * Sensor information is only available for raw sensors. When called for a YUV
- * sensor, this function returns -EINVAL.
- *
- * Pipeline handlers that do not change the sensor format using the setFormat()
- * function may need to call updateControlInfo() beforehand, to ensure all the
- * control ranges are up to date.
- *
- * \return 0 on success, a negative error code otherwise
- */
-int CameraSensor::sensorInfo(IPACameraSensorInfo *info) const
+int CameraSensor::setTestPatternMode(controls::draft::TestPatternModeEnum mode)
 {
-	if (!bayerFormat_)
+	if (testPatternMode_ == mode)
+		return 0;
+
+	if (testPatternModes_.empty()) {
+		LOG(CameraSensor, Error)
+			<< "Camera sensor does not support test pattern modes.";
 		return -EINVAL;
-
-	info->model = model();
-
-	/*
-	 * The active area size is a static property, while the crop
-	 * rectangle needs to be re-read as it depends on the sensor
-	 * configuration.
-	 */
-	info->activeAreaSize = { activeArea_.width, activeArea_.height };
-
-	/*
-	 * \todo Support for retreiving the crop rectangle is scheduled to
-	 * become mandatory. For the time being use the default value if it has
-	 * been initialized at sensor driver validation time.
-	 */
-	int ret = subdev_->getSelection(pad_, V4L2_SEL_TGT_CROP, &info->analogCrop);
-	if (ret) {
-		info->analogCrop = activeArea_;
-		LOG(CameraSensor, Warning)
-			<< "The analogue crop rectangle has been defaulted to the active area size";
 	}
 
-	/*
-	 * IPACameraSensorInfo::analogCrop::x and IPACameraSensorInfo::analogCrop::y
-	 * are defined relatively to the active pixel area, while V4L2's
-	 * TGT_CROP target is defined in respect to the full pixel array.
-	 *
-	 * Compensate it by subtracting the active area offset.
-	 */
-	info->analogCrop.x -= activeArea_.x;
-	info->analogCrop.y -= activeArea_.y;
+	return applyTestPatternMode(mode);
+}
 
-	/* The bit depth and image size depend on the currently applied format. */
-	V4L2SubdeviceFormat format{};
-	ret = subdev_->getFormat(pad_, &format);
+int CameraSensor::applyTestPatternMode(controls::draft::TestPatternModeEnum mode)
+{
+	if (testPatternModes_.empty())
+		return 0;
+
+	auto it = std::find(testPatternModes_.begin(), testPatternModes_.end(),
+			    mode);
+	if (it == testPatternModes_.end()) {
+		LOG(CameraSensor, Error) << "Unsupported test pattern mode "
+					 << mode;
+		return -EINVAL;
+	}
+
+	LOG(CameraSensor, Debug) << "Apply test pattern mode " << mode;
+
+	int32_t index = staticProps_->testPatternModes.at(mode);
+	ControlList ctrls{ controls() };
+	ctrls.set(V4L2_CID_TEST_PATTERN, index);
+
+	int ret = setControls(&ctrls);
 	if (ret)
 		return ret;
-	info->bitsPerPixel = format.bitsPerPixel();
-	info->outputSize = format.size;
 
-	std::optional<int32_t> cfa = properties_.get(properties::draft::ColorFilterArrangement);
-	info->cfaPattern = cfa ? *cfa : properties::draft::RGB;
-
-	/*
-	 * Retrieve the pixel rate, line length and minimum/maximum frame
-	 * duration through V4L2 controls. Support for the V4L2_CID_PIXEL_RATE,
-	 * V4L2_CID_HBLANK and V4L2_CID_VBLANK controls is mandatory.
-	 */
-	ControlList ctrls = subdev_->getControls({ V4L2_CID_PIXEL_RATE,
-						   V4L2_CID_HBLANK,
-						   V4L2_CID_VBLANK });
-	if (ctrls.empty()) {
-		LOG(CameraSensor, Error)
-			<< "Failed to retrieve camera info controls";
-		return -EINVAL;
-	}
-
-	info->pixelRate = ctrls.get(V4L2_CID_PIXEL_RATE).get<int64_t>();
-
-	const ControlInfo hblank = ctrls.infoMap()->at(V4L2_CID_HBLANK);
-	info->minLineLength = info->outputSize.width + hblank.min().get<int32_t>();
-	info->maxLineLength = info->outputSize.width + hblank.max().get<int32_t>();
-
-	const ControlInfo vblank = ctrls.infoMap()->at(V4L2_CID_VBLANK);
-	info->minFrameLength = info->outputSize.height + vblank.min().get<int32_t>();
-	info->maxFrameLength = info->outputSize.height + vblank.max().get<int32_t>();
+	testPatternMode_ = mode;
 
 	return 0;
 }
 
-/**
- * \fn void CameraSensor::updateControlInfo()
- * \brief Update the sensor's ControlInfoMap in case they have changed
- * \sa V4L2Device::updateControlInfo()
- */
-void CameraSensor::updateControlInfo()
-{
-	subdev_->updateControlInfo();
-}
-
-/**
- * \fn CameraSensor::focusLens()
- * \brief Retrieve the focus lens controller
- *
- * \return The focus lens controller. nullptr if no focus lens controller is
- * connected to the sensor
- */
-
-/**
- * \brief Validate a transform request against the sensor capabilities
- * \param[inout] transform The requested transformation, updated to match
- * the sensor capabilities
- *
- * The input \a transform is the transform that the caller wants, and it is
- * adjusted according to the capabilities of the sensor to represent the
- * "nearest" transform that can actually be delivered.
- *
- * The returned Transform is the transform applied to the sensor in order to
- * produce the input \a transform, It is also validated against the sensor's
- * ability to perform horizontal and vertical flips.
- *
- * For example, if the requested \a transform is Transform::Identity and the
- * sensor rotation is 180 degrees, the output transform will be
- * Transform::Rot180 to correct the images so that they appear to have
- * Transform::Identity, but only if the sensor can apply horizontal and vertical
- * flips.
- *
- * \return A Transform instance that represents which transformation has been
- * applied to the camera sensor
- */
-Transform CameraSensor::validateTransform(Transform *transform) const
-{
-	/* Adjust the requested transform to compensate the sensor mounting rotation. */
-	const ControlInfoMap &controls = subdev_->controls();
-	int rotation = 0;
-
-	const auto &rotationControl = controls.find(V4L2_CID_CAMERA_SENSOR_ROTATION);
-	if (rotationControl != controls.end())
-		rotation = rotationControl->second.def().get<int32_t>();
-
-	bool success;
-	Transform rotationTransform = transformFromRotation(rotation, &success);
-	if (!success)
-		LOG(CameraSensor, Warning) << "Invalid rotation of " << rotation
-					   << " degrees - ignoring";
-
-	Transform combined = *transform * rotationTransform;
-
-	/*
-	 * We combine the platform and user transform, but must "adjust away"
-	 * any combined result that includes a transposition, as we can't do
-	 * those. In this case, flipping only the transpose bit is helpful to
-	 * applications - they either get the transform they requested, or have
-	 * to do a simple transpose themselves (they don't have to worry about
-	 * the other possible cases).
-	 */
-	if (!!(combined & Transform::Transpose)) {
-		/*
-		 * Flipping the transpose bit in "transform" flips it in the
-		 * combined result too (as it's the last thing that happens),
-		 * which is of course clearing it.
-		 */
-		*transform ^= Transform::Transpose;
-		combined &= ~Transform::Transpose;
-	}
-
-	/*
-	 * We also check if the sensor doesn't do h/vflips at all, in which
-	 * case we clear them, and the application will have to do everything.
-	 */
-	if (!supportFlips_ && !!combined) {
-		/*
-		 * If the sensor can do no transforms, then combined must be
-		 * changed to the identity. The only user transform that gives
-		 * rise to this is the inverse of the rotation. (Recall that
-		 * combined = transform * rotationTransform.)
-		 */
-		*transform = -rotationTransform;
-		combined = Transform::Identity;
-	}
-
-	return combined;
-}
-
 std::string CameraSensor::logPrefix() const
 {
 	return "'" + entity_->name() + "'";
 }
 
-int CameraSensor::generateId()
-{
-	const std::string devPath = subdev_->devicePath();
-
-	/* Try to get ID from firmware description. */
-	id_ = sysfs::firmwareNodePath(devPath);
-	if (!id_.empty())
-		return 0;
-
-	/*
-	 * Virtual sensors not described in firmware
-	 *
-	 * Verify it's a platform device and construct ID from the device path
-	 * and model of sensor.
-	 */
-	if (devPath.find("/sys/devices/platform/", 0) == 0) {
-		id_ = devPath.substr(strlen("/sys/devices/")) + " " + model();
-		return 0;
-	}
-
-	LOG(CameraSensor, Error) << "Can't generate sensor ID";
-	return -EINVAL;
-}
-
 } /* namespace libcamera */
diff --git a/src/libcamera/camera_sensor_properties.cpp b/src/libcamera/sensor/camera_sensor_properties.cpp
similarity index 91%
rename from src/libcamera/camera_sensor_properties.cpp
rename to src/libcamera/sensor/camera_sensor_properties.cpp
index 27d6799..b18524d 100644
--- a/src/libcamera/camera_sensor_properties.cpp
+++ b/src/libcamera/sensor/camera_sensor_properties.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * camera_sensor_properties.cpp - Database of camera sensor properties
+ * Database of camera sensor properties
  */
 
 #include "libcamera/internal/camera_sensor_properties.h"
@@ -99,6 +99,10 @@
 				{ controls::draft::TestPatternModePn9, 4 },
 			},
 		} },
+		{ "imx283", {
+			.unitCellSize = { 2400, 2400 },
+			.testPatternModes = {},
+		} },
 		{ "imx290", {
 			.unitCellSize = { 2900, 2900 },
 			.testPatternModes = {},
@@ -111,6 +115,14 @@
 			.unitCellSize = { 2900, 2900 },
 			.testPatternModes = {},
 		} },
+		{ "imx335", {
+			.unitCellSize = { 2000, 2000 },
+			.testPatternModes = {},
+		} },
+		{ "imx415", {
+			.unitCellSize = { 1450, 1450 },
+			.testPatternModes = {},
+		} },
 		{ "imx477", {
 			.unitCellSize = { 1550, 1550 },
 			.testPatternModes = {},
@@ -208,6 +220,19 @@
 				 */
 			},
 		} },
+		{ "ov64a40", {
+			.unitCellSize = { 1008, 1008 },
+			.testPatternModes = {
+				{ controls::draft::TestPatternModeOff, 0 },
+				{ controls::draft::TestPatternModeColorBars, 1 },
+				{ controls::draft::TestPatternModeColorBarsFadeToGray, 2 },
+				/*
+				 * No corresponding test patter mode
+				 * 3: "Vertical Color Bar Type 3",
+				 * 4: "Vertical Color Bar Type 4"
+				 */
+			},
+		} },
 		{ "ov8858", {
 			.unitCellSize = { 1120, 1120 },
 			.testPatternModes = {
diff --git a/src/libcamera/sensor/meson.build b/src/libcamera/sensor/meson.build
new file mode 100644
index 0000000..bf4b131
--- /dev/null
+++ b/src/libcamera/sensor/meson.build
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: CC0-1.0
+
+libcamera_sources += files([
+    'camera_sensor.cpp',
+    'camera_sensor_properties.cpp',
+])
diff --git a/src/libcamera/shared_mem_object.cpp b/src/libcamera/shared_mem_object.cpp
new file mode 100644
index 0000000..809fbda
--- /dev/null
+++ b/src/libcamera/shared_mem_object.cpp
@@ -0,0 +1,242 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023 Raspberry Pi Ltd
+ * Copyright (C) 2024 Andrei Konovalov
+ * Copyright (C) 2024 Dennis Bonke
+ * Copyright (C) 2024 Ideas on Board Oy
+ *
+ * Helpers for shared memory allocations
+ */
+
+#include "libcamera/internal/shared_mem_object.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+/**
+ * \file shared_mem_object.cpp
+ * \brief Helpers for shared memory allocations
+ */
+
+namespace libcamera {
+
+/**
+ * \class SharedMem
+ * \brief Helper class to allocate and manage memory shareable between processes
+ *
+ * SharedMem manages memory suitable for sharing between processes. When an
+ * instance is constructed, it allocates a memory buffer of the requested size
+ * backed by an anonymous file, using the memfd API.
+ *
+ * The allocated memory is exposed by the mem() function. If memory allocation
+ * fails, the function returns an empty Span. This can be also checked using the
+ * bool() operator.
+ *
+ * The file descriptor for the backing file is exposed as a SharedFD by the fd()
+ * function. It can be shared with other processes across IPC boundaries, which
+ * can then map the memory with mmap().
+ *
+ * A single memfd is created for every SharedMem. If there is a need to allocate
+ * a large number of objects in shared memory, these objects should be grouped
+ * together and use the shared memory allocated by a single SharedMem object if
+ * possible. This will help to minimize the number of created memfd's.
+ */
+
+SharedMem::SharedMem() = default;
+
+/**
+ * \brief Construct a SharedMem with memory of the given \a size
+ * \param[in] name Name of the SharedMem
+ * \param[in] size Size of the shared memory to allocate and map
+ *
+ * The \a name is used for debugging purpose only. Multiple SharedMem instances
+ * can have the same name.
+ */
+SharedMem::SharedMem(const std::string &name, std::size_t size)
+{
+#if HAVE_MEMFD_CREATE
+	int fd = memfd_create(name.c_str(), MFD_CLOEXEC);
+#else
+	int fd = syscall(SYS_memfd_create, name.c_str(), MFD_CLOEXEC);
+#endif
+	if (fd < 0)
+		return;
+
+	fd_ = SharedFD(std::move(fd));
+	if (!fd_.isValid())
+		return;
+
+	if (ftruncate(fd_.get(), size) < 0) {
+		fd_ = SharedFD();
+		return;
+	}
+
+	void *mem = mmap(nullptr, size, PROT_READ | PROT_WRITE, MAP_SHARED,
+			 fd_.get(), 0);
+	if (mem == MAP_FAILED) {
+		fd_ = SharedFD();
+		return;
+	}
+
+	mem_ = { static_cast<uint8_t *>(mem), size };
+}
+
+/**
+ * \brief Move constructor for SharedMem
+ * \param[in] rhs The object to move
+ */
+SharedMem::SharedMem(SharedMem &&rhs)
+{
+	this->fd_ = std::move(rhs.fd_);
+	this->mem_ = rhs.mem_;
+	rhs.mem_ = {};
+}
+
+/**
+ * \brief Destroy the SharedMem instance
+ *
+ * Destroying an instance invalidates the memory mapping exposed with mem().
+ * Other mappings of the backing file, created in this or other processes with
+ * mmap(), remain valid.
+ *
+ * Similarly, other references to the backing file descriptor created by copying
+ * the SharedFD returned by fd() remain valid. The underlying memory will be
+ * freed only when all file descriptors that reference the anonymous file get
+ * closed.
+ */
+SharedMem::~SharedMem()
+{
+	if (!mem_.empty())
+		munmap(mem_.data(), mem_.size_bytes());
+}
+
+/**
+ * \brief Move assignment operator for SharedMem
+ * \param[in] rhs The object to move
+ */
+SharedMem &SharedMem::operator=(SharedMem &&rhs)
+{
+	this->fd_ = std::move(rhs.fd_);
+	this->mem_ = rhs.mem_;
+	rhs.mem_ = {};
+	return *this;
+}
+
+/**
+ * \fn const SharedFD &SharedMem::fd() const
+ * \brief Retrieve the file descriptor for the underlying shared memory
+ * \return The file descriptor, or an invalid SharedFD if allocation failed
+ */
+
+/**
+ * \fn Span<uint8_t> SharedMem::mem() const
+ * \brief Retrieve the underlying shared memory
+ * \return The memory buffer, or an empty Span if allocation failed
+ */
+
+/**
+ * \fn SharedMem::operator bool()
+ * \brief Check if the shared memory allocation succeeded
+ * \return True if allocation of the shared memory succeeded, false otherwise
+ */
+
+/**
+ * \class SharedMemObject
+ * \brief Helper class to allocate an object in shareable memory
+ * \tparam The object type
+ *
+ * The SharedMemObject class is a specialization of the SharedMem class that
+ * wraps an object of type \a T and constructs it in shareable memory. It uses
+ * the same underlying memory allocation and sharing mechanism as the SharedMem
+ * class.
+ *
+ * The wrapped object is constructed at the same time as the SharedMemObject
+ * instance, by forwarding the arguments passed to the SharedMemObject
+ * constructor. The underlying memory allocation is sized to the object \a T
+ * size. The bool() operator should be used to check the allocation was
+ * successful. The object can be accessed using the dereference operators
+ * operator*() and operator->().
+ *
+ * While no restriction on the type \a T is enforced, not all types are suitable
+ * for sharing between multiple processes. Most notably, any object type that
+ * contains pointer or reference members will likely cause issues. Even if those
+ * members refer to other members of the same object, the shared memory will be
+ * mapped at different addresses in different processes, and the pointers will
+ * not be valid.
+ *
+ * A new anonymous file is created for every SharedMemObject instance. If there
+ * is a need to share a large number of small objects, these objects should be
+ * grouped into a single larger object to limit the number of file descriptors.
+ *
+ * To share the object with other processes, see the SharedMem documentation.
+ */
+
+/**
+ * \var SharedMemObject::kSize
+ * \brief The size of the object stored in shared memory
+ */
+
+/**
+ * \fn SharedMemObject::SharedMemObject(const std::string &name, Args &&...args)
+ * \brief Construct a SharedMemObject
+ * \param[in] name Name of the SharedMemObject
+ * \param[in] args Arguments to pass to the constructor of the object T
+ *
+ * The \a name is used for debugging purpose only. Multiple SharedMem instances
+ * can have the same name.
+ */
+
+/**
+ * \fn SharedMemObject::SharedMemObject(SharedMemObject<T> &&rhs)
+ * \brief Move constructor for SharedMemObject
+ * \param[in] rhs The object to move
+ */
+
+/**
+ * \fn SharedMemObject::~SharedMemObject()
+ * \brief Destroy the SharedMemObject instance
+ *
+ * Destroying a SharedMemObject calls the wrapped T object's destructor. While
+ * the underlying memory may not be freed immediately if other mappings have
+ * been created manually (see SharedMem::~SharedMem() for more information), the
+ * stored object may be modified. Depending on the ~T() destructor, accessing
+ * the object after destruction of the SharedMemObject causes undefined
+ * behaviour. It is the responsibility of the user of this class to synchronize
+ * with other users who have access to the shared object.
+ */
+
+/**
+ * \fn SharedMemObject::operator=(SharedMemObject<T> &&rhs)
+ * \brief Move assignment operator for SharedMemObject
+ * \param[in] rhs The SharedMemObject object to take the data from
+ *
+ * Moving a SharedMemObject does not affect the stored object.
+ */
+
+/**
+ * \fn SharedMemObject::operator->()
+ * \brief Dereference the stored object
+ * \return Pointer to the stored object
+ */
+
+/**
+ * \fn const T *SharedMemObject::operator->() const
+ * \copydoc SharedMemObject::operator->
+ */
+
+/**
+ * \fn SharedMemObject::operator*()
+ * \brief Dereference the stored object
+ * \return Reference to the stored object
+ */
+
+/**
+ * \fn const T &SharedMemObject::operator*() const
+ * \copydoc SharedMemObject::operator*
+ */
+
+} /* namespace libcamera */
diff --git a/src/libcamera/software_isp/TODO b/src/libcamera/software_isp/TODO
new file mode 100644
index 0000000..6bdc590
--- /dev/null
+++ b/src/libcamera/software_isp/TODO
@@ -0,0 +1,266 @@
+1. Setting F_SEAL_SHRINK and F_SEAL_GROW after ftruncate()
+
+>> SharedMem::SharedMem(const std::string &name, std::size_t size)
+>> 	: name_(name), size_(size), mem_(nullptr)
+>>
+>> ...
+>>
+>> 	if (ftruncate(fd_.get(), size_) < 0)
+>> 		return;
+>
+> Should we set the GROW and SHRINK seals (in a separate patch) ?
+
+Yes, this can be done.
+Setting F_SEAL_SHRINK and F_SEAL_GROW after the ftruncate() call above could catch
+some potential errors related to improper access to the shared memory allocated by
+the SharedMemObject.
+
+---
+
+2. Reconsider stats sharing
+
+>>> +void SwStatsCpu::finishFrame(void)
+>>> +{
+>>> +	*sharedStats_ = stats_;
+>> 
+>> Is it more efficient to copy the stats instead of operating directly on
+>> the shared memory ?
+>
+> I inherited doing things this way from Andrey. I kept this because
+> we don't really have any synchronization with the IPA reading this.
+>
+> So the idea is to only touch this when the next set of statistics
+> is ready since we don't know when the IPA is done with accessing
+> the previous set of statistics ...
+>
+> This is both something which seems mostly a theoretic problem,
+> yet also definitely something which I think we need to fix.
+>
+> Maybe use a ringbuffer of stats buffers and pass the index into
+> the ringbuffer to the emit signal ?
+
+That would match how we deal with hardware ISPs, and I think that's a
+good idea. It will help decoupling the processing side from the IPA.
+
+---
+
+3. Remove statsReady signal
+
+> class SwStatsCpu
+> {
+> 	/**
+> 	 * \brief Signals that the statistics are ready
+> 	 */
+> 	Signal<> statsReady;
+
+But better, I wonder if the signal could be dropped completely. The
+SwStatsCpu class does not operate asynchronously. Shouldn't whoever
+calls the finishFrame() function then handle emitting the signal ?
+
+Now, the trouble is that this would be the DebayerCpu class, whose name
+doesn't indicate as a prime candidate to handle stats. However, it
+already exposes a getStatsFD() function, so we're already calling for
+trouble :-) Either that should be moved to somewhere else, or the class
+should be renamed. Considering that the class applies colour gains in
+addition to performing the interpolation, it may be more of a naming
+issue.
+
+Removing the signal and refactoring those classes doesn't have to be
+addressed now, I think it would be part of a larger refactoring
+(possibly also considering platforms that have no ISP but can produce
+stats in hardware, such as the i.MX7), but please keep it on your radar.
+
+---
+
+5. Store ISP parameters in per-frame buffers
+
+> /**
+>  * \fn void Debayer::process(FrameBuffer *input, FrameBuffer *output, DebayerParams params)
+>  * \brief Process the bayer data into the requested format.
+>  * \param[in] input The input buffer.
+>  * \param[in] output The output buffer.
+>  * \param[in] params The parameters to be used in debayering.
+>  *
+>  * \note DebayerParams is passed by value deliberately so that a copy is passed
+>  * when this is run in another thread by invokeMethod().
+>  */
+
+Possibly something to address later, by storing ISP parameters in
+per-frame buffers like we do for hardware ISPs.
+
+---
+
+6. Input buffer copying configuration
+
+> DebayerCpu::DebayerCpu(std::unique_ptr<SwStatsCpu> stats)
+> 	: stats_(std::move(stats)), gammaCorrection_(1.0)
+> {
+> 	enableInputMemcpy_ = true;
+
+Set this appropriately and/or make it configurable.
+
+---
+
+7. Performance measurement configuration
+
+> void DebayerCpu::process(FrameBuffer *input, FrameBuffer *output, DebayerParams params)
+> /* Measure before emitting signals */
+> if (measuredFrames_ < DebayerCpu::kLastFrameToMeasure &&
+>     ++measuredFrames_ > DebayerCpu::kFramesToSkip) {
+> 	timespec frameEndTime = {};
+> 	clock_gettime(CLOCK_MONOTONIC_RAW, &frameEndTime);
+> 	frameProcessTime_ += timeDiff(frameEndTime, frameStartTime);
+> 	if (measuredFrames_ == DebayerCpu::kLastFrameToMeasure) {
+> 		const unsigned int measuredFrames = DebayerCpu::kLastFrameToMeasure -
+> 						    DebayerCpu::kFramesToSkip;
+> 		LOG(Debayer, Info)
+> 			<< "Processed " << measuredFrames
+> 			<< " frames in " << frameProcessTime_ / 1000 << "us, "
+> 			<< frameProcessTime_ / (1000 * measuredFrames)
+> 			<< " us/frame";
+> 	}
+> }
+
+I wonder if there would be a way to control at runtime when/how to
+perform those measurements. Maybe that's a bit overkill.
+
+---
+
+8. DebayerCpu cleanups
+
+> >> class DebayerCpu : public Debayer, public Object
+> >>   const SharedFD &getStatsFD() { return stats_->getStatsFD(); }
+> >
+> > This,
+>
+> Note the statistics pass-through stuff is sort of a necessary evil
+> since we want one main loop going over the data line by line and
+> doing both debayering as well as stats while the line is still
+> hot in the l2 cache. And things like the process2() and process4()
+> loops are highly CPU debayering specific so I don't think we should
+> move those out of the CpuDebayer code.
+
+Yes, that I understood from the review. "necessary evil" is indeed the
+right term :-) I expect it will take quite some design skills to balance
+the need for performances and the need for a maintainable architecture.
+
+> > plus the fact that this class handles colour gains and gamma,
+> > makes me thing we have either a naming issue, or an architecture issue.
+>
+> I agree that this does a bit more then debayering, although
+> the debayering really is the main thing it does.
+>
+> I guess the calculation of the rgb lookup tables which do the
+> color gains and gamma could be moved outside of this class,
+> that might even be beneficial for GPU based debayering assuming
+> that that is going to use rgb lookup tables too (it could
+> implement actual color gains + gamma correction in some different
+> way).
+>
+> I think this falls under the lets wait until we have a GPU
+> based SoftISP MVP/POC and then do some refactoring to see which
+> bits should go where.
+
+---
+
+8. Decouple pipeline and IPA naming
+
+> The current src/ipa/meson.build assumes the IPA name to match the
+> pipeline name. For this reason "-Dipas=simple" is used for the
+> Soft IPA module.
+
+This should be addressed.
+
+---
+
+9. Doxyfile cleanup
+
+>> diff --git a/Documentation/Doxyfile.in b/Documentation/Doxyfile.in
+>> index a86ea6c1..2be8d47b 100644
+>> --- a/Documentation/Doxyfile.in
+>> +++ b/Documentation/Doxyfile.in
+>> @@ -44,6 +44,7 @@ EXCLUDE                = @TOP_SRCDIR@/include/libcamera/base/span.h \
+>>                            @TOP_SRCDIR@/src/libcamera/pipeline/ \
+>>                            @TOP_SRCDIR@/src/libcamera/tracepoints.cpp \
+>>                            @TOP_BUILDDIR@/include/libcamera/internal/tracepoints.h \
+>> +                         @TOP_BUILDDIR@/include/libcamera/ipa/soft_ipa_interface.h \
+> Why is this needed ?
+>
+>>                            @TOP_BUILDDIR@/src/libcamera/proxy/
+>>     EXCLUDE_PATTERNS       = @TOP_BUILDDIR@/include/libcamera/ipa/*_serializer.h \
+>> diff --git a/include/libcamera/ipa/meson.build b/include/libcamera/ipa/meson.build
+>> index f3b4881c..3352d08f 100644
+>> --- a/include/libcamera/ipa/meson.build
+>> +++ b/include/libcamera/ipa/meson.build
+>> @@ -65,6 +65,7 @@ pipeline_ipa_mojom_mapping = {
+>>       'ipu3': 'ipu3.mojom',
+>>       'rkisp1': 'rkisp1.mojom',
+>>       'rpi/vc4': 'raspberrypi.mojom',
+>> +    'simple': 'soft.mojom',
+>>       'vimc': 'vimc.mojom',
+>>   }
+>>   diff --git a/include/libcamera/ipa/soft.mojom b/include/libcamera/ipa/soft.mojom
+>> new file mode 100644
+>> index 00000000..c249bd75
+>> --- /dev/null
+>> +++ b/include/libcamera/ipa/soft.mojom
+>> @@ -0,0 +1,28 @@
+>> +/* SPDX-License-Identifier: LGPL-2.1-or-later */
+>> +
+>> +/*
+>> + * \todo Document the interface and remove the related EXCLUDE_PATTERNS entry.
+> Ah that's why.
+
+Yes, because, well... all the other IPAs were doing that...
+
+> It doesn't have to be done before merging, but could you
+> address this sooner than later ?
+
+---
+
+10. Switch to libipa/algorithm.h API in processStats
+
+>> void IPASoftSimple::processStats(const ControlList &sensorControls)
+>>
+> Do you envision switching to the libipa/algorithm.h API at some point ?
+
+At some point, yes.
+
+---
+
+11. Improve handling the sensor controls which take effect with a delay
+
+> void IPASoftSimple::processStats(const ControlList &sensorControls)
+> {
+>       ...
+>	/*
+>	 * AE / AGC, use 2 frames delay to make sure that the exposure and
+>	 * the gain set have applied to the camera sensor.
+>	 */
+>	if (ignore_updates_ > 0) {
+>		--ignore_updates_;
+>		return;
+>	}
+
+This could be handled better with DelayedControls.
+
+---
+
+12. Use DelayedControls class in ispStatsReady()
+
+> void SimpleCameraData::ispStatsReady()
+> {
+> 	swIsp_->processStats(sensor_->getControls({ V4L2_CID_ANALOGUE_GAIN,
+> 						    V4L2_CID_EXPOSURE }));
+
+You should use the DelayedControls class.
+
+---
+
+13. Improve black level and colour gains application
+
+I think the black level should eventually be moved before debayering, and
+ideally the colour gains as well. I understand the need for optimizations to
+lower the CPU consumption, but at the same time I don't feel comfortable
+building up on top of an implementation that may work a bit more by chance than
+by correctness, as that's not very maintainable.
diff --git a/src/libcamera/software_isp/debayer.cpp b/src/libcamera/software_isp/debayer.cpp
new file mode 100644
index 0000000..f4a299d
--- /dev/null
+++ b/src/libcamera/software_isp/debayer.cpp
@@ -0,0 +1,126 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ * Copyright (C) 2023, 2024 Red Hat Inc.
+ *
+ * Authors:
+ * Hans de Goede <hdegoede@redhat.com>
+ *
+ * debayer base class
+ */
+
+#include "debayer.h"
+
+namespace libcamera {
+
+/**
+ * \struct DebayerParams
+ * \brief Struct to hold the debayer parameters.
+ */
+
+/**
+ * \var DebayerParams::kRGBLookupSize
+ * \brief Size of a color lookup table
+ */
+
+/**
+ * \typedef DebayerParams::ColorLookupTable
+ * \brief Type of the lookup tables for red, green, blue values
+ */
+
+/**
+ * \var DebayerParams::red
+ * \brief Lookup table for red color, mapping input values to output values
+ */
+
+/**
+ * \var DebayerParams::green
+ * \brief Lookup table for green color, mapping input values to output values
+ */
+
+/**
+ * \var DebayerParams::blue
+ * \brief Lookup table for blue color, mapping input values to output values
+ */
+
+/**
+ * \class Debayer
+ * \brief Base debayering class
+ *
+ * Base class that provides functions for setting up the debayering process.
+ */
+
+LOG_DEFINE_CATEGORY(Debayer)
+
+Debayer::~Debayer()
+{
+}
+
+/**
+ * \fn int Debayer::configure(const StreamConfiguration &inputCfg, const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs)
+ * \brief Configure the debayer object according to the passed in parameters.
+ * \param[in] inputCfg The input configuration.
+ * \param[in] outputCfgs The output configurations.
+ *
+ * \return 0 on success, a negative errno on failure.
+ */
+
+/**
+ * \fn Size Debayer::patternSize(PixelFormat inputFormat)
+ * \brief Get the width and height at which the bayer pattern repeats.
+ * \param[in] inputFormat The input format.
+ *
+ * Valid sizes are: 2x2, 4x2 or 4x4.
+ *
+ * \return Pattern size or an empty size for unsupported inputFormats.
+ */
+
+/**
+ * \fn std::vector<PixelFormat> Debayer::formats(PixelFormat inputFormat)
+ * \brief Get the supported output formats.
+ * \param[in] inputFormat The input format.
+ *
+ * \return All supported output formats or an empty vector if there are none.
+ */
+
+/**
+ * \fn std::tuple<unsigned int, unsigned int> Debayer::strideAndFrameSize(const PixelFormat &outputFormat, const Size &size)
+ * \brief Get the stride and the frame size.
+ * \param[in] outputFormat The output format.
+ * \param[in] size The output size.
+ *
+ * \return A tuple of the stride and the frame size, or a tuple with 0,0 if
+ *    there is no valid output config.
+ */
+
+/**
+ * \fn void Debayer::process(FrameBuffer *input, FrameBuffer *output, DebayerParams params)
+ * \brief Process the bayer data into the requested format.
+ * \param[in] input The input buffer.
+ * \param[in] output The output buffer.
+ * \param[in] params The parameters to be used in debayering.
+ *
+ * \note DebayerParams is passed by value deliberately so that a copy is passed
+ * when this is run in another thread by invokeMethod().
+ */
+
+/**
+ * \fn virtual SizeRange Debayer::sizes(PixelFormat inputFormat, const Size &inputSize)
+ * \brief Get the supported output sizes for the given input format and size.
+ * \param[in] inputFormat The input format.
+ * \param[in] inputSize The input size.
+ *
+ * \return The valid size ranges or an empty range if there are none.
+ */
+
+/**
+ * \var Signal<FrameBuffer *> Debayer::inputBufferReady
+ * \brief Signals when the input buffer is ready.
+ */
+
+/**
+ * \var Signal<FrameBuffer *> Debayer::outputBufferReady
+ * \brief Signals when the output buffer is ready.
+ */
+
+} /* namespace libcamera */
diff --git a/src/libcamera/software_isp/debayer.h b/src/libcamera/software_isp/debayer.h
new file mode 100644
index 0000000..c151fe5
--- /dev/null
+++ b/src/libcamera/software_isp/debayer.h
@@ -0,0 +1,54 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ * Copyright (C) 2023, Red Hat Inc.
+ *
+ * Authors:
+ * Hans de Goede <hdegoede@redhat.com>
+ *
+ * debayering base class
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <libcamera/base/log.h>
+#include <libcamera/base/signal.h>
+
+#include <libcamera/geometry.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/software_isp/debayer_params.h"
+
+namespace libcamera {
+
+class FrameBuffer;
+
+LOG_DECLARE_CATEGORY(Debayer)
+
+class Debayer
+{
+public:
+	virtual ~Debayer() = 0;
+
+	virtual int configure(const StreamConfiguration &inputCfg,
+			      const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs) = 0;
+
+	virtual std::vector<PixelFormat> formats(PixelFormat inputFormat) = 0;
+
+	virtual std::tuple<unsigned int, unsigned int>
+	strideAndFrameSize(const PixelFormat &outputFormat, const Size &size) = 0;
+
+	virtual void process(FrameBuffer *input, FrameBuffer *output, DebayerParams params) = 0;
+
+	virtual SizeRange sizes(PixelFormat inputFormat, const Size &inputSize) = 0;
+
+	Signal<FrameBuffer *> inputBufferReady;
+	Signal<FrameBuffer *> outputBufferReady;
+
+private:
+	virtual Size patternSize(PixelFormat inputFormat) = 0;
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/software_isp/debayer_cpu.cpp b/src/libcamera/software_isp/debayer_cpu.cpp
new file mode 100644
index 0000000..f8d2677
--- /dev/null
+++ b/src/libcamera/software_isp/debayer_cpu.cpp
@@ -0,0 +1,821 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ * Copyright (C) 2023, Red Hat Inc.
+ *
+ * Authors:
+ * Hans de Goede <hdegoede@redhat.com>
+ *
+ * CPU based debayering class
+ */
+
+#include "debayer_cpu.h"
+
+#include <stdlib.h>
+#include <time.h>
+
+#include <libcamera/formats.h>
+
+#include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/framebuffer.h"
+#include "libcamera/internal/mapped_framebuffer.h"
+
+namespace libcamera {
+
+/**
+ * \class DebayerCpu
+ * \brief Class for debayering on the CPU
+ *
+ * Implementation for CPU based debayering
+ */
+
+/**
+ * \brief Constructs a DebayerCpu object
+ * \param[in] stats Pointer to the stats object to use
+ */
+DebayerCpu::DebayerCpu(std::unique_ptr<SwStatsCpu> stats)
+	: stats_(std::move(stats))
+{
+	/*
+	 * Reading from uncached buffers may be very slow.
+	 * In such a case, it's better to copy input buffer data to normal memory.
+	 * But in case of cached buffers, copying the data is unnecessary overhead.
+	 * enable_input_memcpy_ makes this behavior configurable.  At the moment, we
+	 * always set it to true as the safer choice but this should be changed in
+	 * future.
+	 */
+	enableInputMemcpy_ = true;
+
+	/* Initialize color lookup tables */
+	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++)
+		red_[i] = green_[i] = blue_[i] = i;
+
+	for (unsigned int i = 0; i < kMaxLineBuffers; i++)
+		lineBuffers_[i] = nullptr;
+}
+
+DebayerCpu::~DebayerCpu()
+{
+	for (unsigned int i = 0; i < kMaxLineBuffers; i++)
+		free(lineBuffers_[i]);
+}
+
+#define DECLARE_SRC_POINTERS(pixel_t)                            \
+	const pixel_t *prev = (const pixel_t *)src[0] + xShift_; \
+	const pixel_t *curr = (const pixel_t *)src[1] + xShift_; \
+	const pixel_t *next = (const pixel_t *)src[2] + xShift_;
+
+/*
+ * RGR
+ * GBG
+ * RGR
+ */
+#define BGGR_BGR888(p, n, div)                                                                \
+	*dst++ = blue_[curr[x] / (div)];                                                      \
+	*dst++ = green_[(prev[x] + curr[x - p] + curr[x + n] + next[x]) / (4 * (div))];       \
+	*dst++ = red_[(prev[x - p] + prev[x + n] + next[x - p] + next[x + n]) / (4 * (div))]; \
+	if constexpr (addAlphaByte)                                                           \
+		*dst++ = 255;                                                                 \
+	x++;
+
+/*
+ * GBG
+ * RGR
+ * GBG
+ */
+#define GRBG_BGR888(p, n, div)                                    \
+	*dst++ = blue_[(prev[x] + next[x]) / (2 * (div))];        \
+	*dst++ = green_[curr[x] / (div)];                         \
+	*dst++ = red_[(curr[x - p] + curr[x + n]) / (2 * (div))]; \
+	if constexpr (addAlphaByte)                               \
+		*dst++ = 255;                                     \
+	x++;
+
+/*
+ * GRG
+ * BGB
+ * GRG
+ */
+#define GBRG_BGR888(p, n, div)                                     \
+	*dst++ = blue_[(curr[x - p] + curr[x + n]) / (2 * (div))]; \
+	*dst++ = green_[curr[x] / (div)];                          \
+	*dst++ = red_[(prev[x] + next[x]) / (2 * (div))];          \
+	if constexpr (addAlphaByte)                                \
+		*dst++ = 255;                                      \
+	x++;
+
+/*
+ * BGB
+ * GRG
+ * BGB
+ */
+#define RGGB_BGR888(p, n, div)                                                                 \
+	*dst++ = blue_[(prev[x - p] + prev[x + n] + next[x - p] + next[x + n]) / (4 * (div))]; \
+	*dst++ = green_[(prev[x] + curr[x - p] + curr[x + n] + next[x]) / (4 * (div))];        \
+	*dst++ = red_[curr[x] / (div)];                                                        \
+	if constexpr (addAlphaByte)                                                            \
+		*dst++ = 255;                                                                  \
+	x++;
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer8_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	DECLARE_SRC_POINTERS(uint8_t)
+
+	for (int x = 0; x < (int)window_.width;) {
+		BGGR_BGR888(1, 1, 1)
+		GBRG_BGR888(1, 1, 1)
+	}
+}
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer8_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	DECLARE_SRC_POINTERS(uint8_t)
+
+	for (int x = 0; x < (int)window_.width;) {
+		GRBG_BGR888(1, 1, 1)
+		RGGB_BGR888(1, 1, 1)
+	}
+}
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer10_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	DECLARE_SRC_POINTERS(uint16_t)
+
+	for (int x = 0; x < (int)window_.width;) {
+		/* divide values by 4 for 10 -> 8 bpp value */
+		BGGR_BGR888(1, 1, 4)
+		GBRG_BGR888(1, 1, 4)
+	}
+}
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer10_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	DECLARE_SRC_POINTERS(uint16_t)
+
+	for (int x = 0; x < (int)window_.width;) {
+		/* divide values by 4 for 10 -> 8 bpp value */
+		GRBG_BGR888(1, 1, 4)
+		RGGB_BGR888(1, 1, 4)
+	}
+}
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer12_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	DECLARE_SRC_POINTERS(uint16_t)
+
+	for (int x = 0; x < (int)window_.width;) {
+		/* divide values by 16 for 12 -> 8 bpp value */
+		BGGR_BGR888(1, 1, 16)
+		GBRG_BGR888(1, 1, 16)
+	}
+}
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer12_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	DECLARE_SRC_POINTERS(uint16_t)
+
+	for (int x = 0; x < (int)window_.width;) {
+		/* divide values by 16 for 12 -> 8 bpp value */
+		GRBG_BGR888(1, 1, 16)
+		RGGB_BGR888(1, 1, 16)
+	}
+}
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer10P_BGBG_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	const int widthInBytes = window_.width * 5 / 4;
+	const uint8_t *prev = src[0];
+	const uint8_t *curr = src[1];
+	const uint8_t *next = src[2];
+
+	/*
+	 * For the first pixel getting a pixel from the previous column uses
+	 * x - 2 to skip the 5th byte with least-significant bits for 4 pixels.
+	 * Same for last pixel (uses x + 2) and looking at the next column.
+	 */
+	for (int x = 0; x < widthInBytes;) {
+		/* First pixel */
+		BGGR_BGR888(2, 1, 1)
+		/* Second pixel BGGR -> GBRG */
+		GBRG_BGR888(1, 1, 1)
+		/* Same thing for third and fourth pixels */
+		BGGR_BGR888(1, 1, 1)
+		GBRG_BGR888(1, 2, 1)
+		/* Skip 5th src byte with 4 x 2 least-significant-bits */
+		x++;
+	}
+}
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer10P_GRGR_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	const int widthInBytes = window_.width * 5 / 4;
+	const uint8_t *prev = src[0];
+	const uint8_t *curr = src[1];
+	const uint8_t *next = src[2];
+
+	for (int x = 0; x < widthInBytes;) {
+		/* First pixel */
+		GRBG_BGR888(2, 1, 1)
+		/* Second pixel GRBG -> RGGB */
+		RGGB_BGR888(1, 1, 1)
+		/* Same thing for third and fourth pixels */
+		GRBG_BGR888(1, 1, 1)
+		RGGB_BGR888(1, 2, 1)
+		/* Skip 5th src byte with 4 x 2 least-significant-bits */
+		x++;
+	}
+}
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer10P_GBGB_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	const int widthInBytes = window_.width * 5 / 4;
+	const uint8_t *prev = src[0];
+	const uint8_t *curr = src[1];
+	const uint8_t *next = src[2];
+
+	for (int x = 0; x < widthInBytes;) {
+		/* Even pixel */
+		GBRG_BGR888(2, 1, 1)
+		/* Odd pixel GBGR -> BGGR */
+		BGGR_BGR888(1, 1, 1)
+		/* Same thing for next 2 pixels */
+		GBRG_BGR888(1, 1, 1)
+		BGGR_BGR888(1, 2, 1)
+		/* Skip 5th src byte with 4 x 2 least-significant-bits */
+		x++;
+	}
+}
+
+template<bool addAlphaByte>
+void DebayerCpu::debayer10P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[])
+{
+	const int widthInBytes = window_.width * 5 / 4;
+	const uint8_t *prev = src[0];
+	const uint8_t *curr = src[1];
+	const uint8_t *next = src[2];
+
+	for (int x = 0; x < widthInBytes;) {
+		/* Even pixel */
+		RGGB_BGR888(2, 1, 1)
+		/* Odd pixel RGGB -> GRBG */
+		GRBG_BGR888(1, 1, 1)
+		/* Same thing for next 2 pixels */
+		RGGB_BGR888(1, 1, 1)
+		GRBG_BGR888(1, 2, 1)
+		/* Skip 5th src byte with 4 x 2 least-significant-bits */
+		x++;
+	}
+}
+
+static bool isStandardBayerOrder(BayerFormat::Order order)
+{
+	return order == BayerFormat::BGGR || order == BayerFormat::GBRG ||
+	       order == BayerFormat::GRBG || order == BayerFormat::RGGB;
+}
+
+/*
+ * Setup the Debayer object according to the passed in parameters.
+ * Return 0 on success, a negative errno value on failure
+ * (unsupported parameters).
+ */
+int DebayerCpu::getInputConfig(PixelFormat inputFormat, DebayerInputConfig &config)
+{
+	BayerFormat bayerFormat =
+		BayerFormat::fromPixelFormat(inputFormat);
+
+	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10 || bayerFormat.bitDepth == 12) &&
+	    bayerFormat.packing == BayerFormat::Packing::None &&
+	    isStandardBayerOrder(bayerFormat.order)) {
+		config.bpp = (bayerFormat.bitDepth + 7) & ~7;
+		config.patternSize.width = 2;
+		config.patternSize.height = 2;
+		config.outputFormats = std::vector<PixelFormat>({ formats::RGB888,
+								  formats::XRGB8888,
+								  formats::ARGB8888,
+								  formats::BGR888,
+								  formats::XBGR8888,
+								  formats::ABGR8888 });
+		return 0;
+	}
+
+	if (bayerFormat.bitDepth == 10 &&
+	    bayerFormat.packing == BayerFormat::Packing::CSI2 &&
+	    isStandardBayerOrder(bayerFormat.order)) {
+		config.bpp = 10;
+		config.patternSize.width = 4; /* 5 bytes per *4* pixels */
+		config.patternSize.height = 2;
+		config.outputFormats = std::vector<PixelFormat>({ formats::RGB888,
+								  formats::XRGB8888,
+								  formats::ARGB8888,
+								  formats::BGR888,
+								  formats::XBGR8888,
+								  formats::ABGR8888 });
+		return 0;
+	}
+
+	LOG(Debayer, Info)
+		<< "Unsupported input format " << inputFormat.toString();
+	return -EINVAL;
+}
+
+int DebayerCpu::getOutputConfig(PixelFormat outputFormat, DebayerOutputConfig &config)
+{
+	if (outputFormat == formats::RGB888 || outputFormat == formats::BGR888) {
+		config.bpp = 24;
+		return 0;
+	}
+
+	if (outputFormat == formats::XRGB8888 || outputFormat == formats::ARGB8888 ||
+	    outputFormat == formats::XBGR8888 || outputFormat == formats::ABGR8888) {
+		config.bpp = 32;
+		return 0;
+	}
+
+	LOG(Debayer, Info)
+		<< "Unsupported output format " << outputFormat.toString();
+	return -EINVAL;
+}
+
+/*
+ * Check for standard Bayer orders and set xShift_ and swap debayer0/1, so that
+ * a single pair of BGGR debayer functions can be used for all 4 standard orders.
+ */
+int DebayerCpu::setupStandardBayerOrder(BayerFormat::Order order)
+{
+	switch (order) {
+	case BayerFormat::BGGR:
+		break;
+	case BayerFormat::GBRG:
+		xShift_ = 1; /* BGGR -> GBRG */
+		break;
+	case BayerFormat::GRBG:
+		std::swap(debayer0_, debayer1_); /* BGGR -> GRBG */
+		break;
+	case BayerFormat::RGGB:
+		xShift_ = 1; /* BGGR -> GBRG */
+		std::swap(debayer0_, debayer1_); /* GBRG -> RGGB */
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	return 0;
+}
+
+int DebayerCpu::setDebayerFunctions(PixelFormat inputFormat, PixelFormat outputFormat)
+{
+	BayerFormat bayerFormat =
+		BayerFormat::fromPixelFormat(inputFormat);
+	bool addAlphaByte = false;
+
+	xShift_ = 0;
+	swapRedBlueGains_ = false;
+
+	auto invalidFmt = []() -> int {
+		LOG(Debayer, Error) << "Unsupported input output format combination";
+		return -EINVAL;
+	};
+
+	switch (outputFormat) {
+	case formats::XRGB8888:
+	case formats::ARGB8888:
+		addAlphaByte = true;
+		[[fallthrough]];
+	case formats::RGB888:
+		break;
+	case formats::XBGR8888:
+	case formats::ABGR8888:
+		addAlphaByte = true;
+		[[fallthrough]];
+	case formats::BGR888:
+		/* Swap R and B in bayer order to generate BGR888 instead of RGB888 */
+		swapRedBlueGains_ = true;
+
+		switch (bayerFormat.order) {
+		case BayerFormat::BGGR:
+			bayerFormat.order = BayerFormat::RGGB;
+			break;
+		case BayerFormat::GBRG:
+			bayerFormat.order = BayerFormat::GRBG;
+			break;
+		case BayerFormat::GRBG:
+			bayerFormat.order = BayerFormat::GBRG;
+			break;
+		case BayerFormat::RGGB:
+			bayerFormat.order = BayerFormat::BGGR;
+			break;
+		default:
+			return invalidFmt();
+		}
+		break;
+	default:
+		return invalidFmt();
+	}
+
+	if ((bayerFormat.bitDepth == 8 || bayerFormat.bitDepth == 10 || bayerFormat.bitDepth == 12) &&
+	    bayerFormat.packing == BayerFormat::Packing::None &&
+	    isStandardBayerOrder(bayerFormat.order)) {
+		switch (bayerFormat.bitDepth) {
+		case 8:
+			debayer0_ = addAlphaByte ? &DebayerCpu::debayer8_BGBG_BGR888<true> : &DebayerCpu::debayer8_BGBG_BGR888<false>;
+			debayer1_ = addAlphaByte ? &DebayerCpu::debayer8_GRGR_BGR888<true> : &DebayerCpu::debayer8_GRGR_BGR888<false>;
+			break;
+		case 10:
+			debayer0_ = addAlphaByte ? &DebayerCpu::debayer10_BGBG_BGR888<true> : &DebayerCpu::debayer10_BGBG_BGR888<false>;
+			debayer1_ = addAlphaByte ? &DebayerCpu::debayer10_GRGR_BGR888<true> : &DebayerCpu::debayer10_GRGR_BGR888<false>;
+			break;
+		case 12:
+			debayer0_ = addAlphaByte ? &DebayerCpu::debayer12_BGBG_BGR888<true> : &DebayerCpu::debayer12_BGBG_BGR888<false>;
+			debayer1_ = addAlphaByte ? &DebayerCpu::debayer12_GRGR_BGR888<true> : &DebayerCpu::debayer12_GRGR_BGR888<false>;
+			break;
+		}
+		setupStandardBayerOrder(bayerFormat.order);
+		return 0;
+	}
+
+	if (bayerFormat.bitDepth == 10 &&
+	    bayerFormat.packing == BayerFormat::Packing::CSI2) {
+		switch (bayerFormat.order) {
+		case BayerFormat::BGGR:
+			debayer0_ = addAlphaByte ? &DebayerCpu::debayer10P_BGBG_BGR888<true> : &DebayerCpu::debayer10P_BGBG_BGR888<false>;
+			debayer1_ = addAlphaByte ? &DebayerCpu::debayer10P_GRGR_BGR888<true> : &DebayerCpu::debayer10P_GRGR_BGR888<false>;
+			return 0;
+		case BayerFormat::GBRG:
+			debayer0_ = addAlphaByte ? &DebayerCpu::debayer10P_GBGB_BGR888<true> : &DebayerCpu::debayer10P_GBGB_BGR888<false>;
+			debayer1_ = addAlphaByte ? &DebayerCpu::debayer10P_RGRG_BGR888<true> : &DebayerCpu::debayer10P_RGRG_BGR888<false>;
+			return 0;
+		case BayerFormat::GRBG:
+			debayer0_ = addAlphaByte ? &DebayerCpu::debayer10P_GRGR_BGR888<true> : &DebayerCpu::debayer10P_GRGR_BGR888<false>;
+			debayer1_ = addAlphaByte ? &DebayerCpu::debayer10P_BGBG_BGR888<true> : &DebayerCpu::debayer10P_BGBG_BGR888<false>;
+			return 0;
+		case BayerFormat::RGGB:
+			debayer0_ = addAlphaByte ? &DebayerCpu::debayer10P_RGRG_BGR888<true> : &DebayerCpu::debayer10P_RGRG_BGR888<false>;
+			debayer1_ = addAlphaByte ? &DebayerCpu::debayer10P_GBGB_BGR888<true> : &DebayerCpu::debayer10P_GBGB_BGR888<false>;
+			return 0;
+		default:
+			break;
+		}
+	}
+
+	return invalidFmt();
+}
+
+int DebayerCpu::configure(const StreamConfiguration &inputCfg,
+			  const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs)
+{
+	if (getInputConfig(inputCfg.pixelFormat, inputConfig_) != 0)
+		return -EINVAL;
+
+	if (stats_->configure(inputCfg) != 0)
+		return -EINVAL;
+
+	const Size &statsPatternSize = stats_->patternSize();
+	if (inputConfig_.patternSize.width != statsPatternSize.width ||
+	    inputConfig_.patternSize.height != statsPatternSize.height) {
+		LOG(Debayer, Error)
+			<< "mismatching stats and debayer pattern sizes for "
+			<< inputCfg.pixelFormat.toString();
+		return -EINVAL;
+	}
+
+	inputConfig_.stride = inputCfg.stride;
+
+	if (outputCfgs.size() != 1) {
+		LOG(Debayer, Error)
+			<< "Unsupported number of output streams: "
+			<< outputCfgs.size();
+		return -EINVAL;
+	}
+
+	const StreamConfiguration &outputCfg = outputCfgs[0];
+	SizeRange outSizeRange = sizes(inputCfg.pixelFormat, inputCfg.size);
+	std::tie(outputConfig_.stride, outputConfig_.frameSize) =
+		strideAndFrameSize(outputCfg.pixelFormat, outputCfg.size);
+
+	if (!outSizeRange.contains(outputCfg.size) || outputConfig_.stride != outputCfg.stride) {
+		LOG(Debayer, Error)
+			<< "Invalid output size/stride: "
+			<< "\n  " << outputCfg.size << " (" << outSizeRange << ")"
+			<< "\n  " << outputCfg.stride << " (" << outputConfig_.stride << ")";
+		return -EINVAL;
+	}
+
+	if (setDebayerFunctions(inputCfg.pixelFormat, outputCfg.pixelFormat) != 0)
+		return -EINVAL;
+
+	window_.x = ((inputCfg.size.width - outputCfg.size.width) / 2) &
+		    ~(inputConfig_.patternSize.width - 1);
+	window_.y = ((inputCfg.size.height - outputCfg.size.height) / 2) &
+		    ~(inputConfig_.patternSize.height - 1);
+	window_.width = outputCfg.size.width;
+	window_.height = outputCfg.size.height;
+
+	/* Don't pass x,y since process() already adjusts src before passing it */
+	stats_->setWindow(Rectangle(window_.size()));
+
+	/* pad with patternSize.Width on both left and right side */
+	lineBufferPadding_ = inputConfig_.patternSize.width * inputConfig_.bpp / 8;
+	lineBufferLength_ = window_.width * inputConfig_.bpp / 8 +
+			    2 * lineBufferPadding_;
+	for (unsigned int i = 0;
+	     i < (inputConfig_.patternSize.height + 1) && enableInputMemcpy_;
+	     i++) {
+		free(lineBuffers_[i]);
+		lineBuffers_[i] = (uint8_t *)malloc(lineBufferLength_);
+		if (!lineBuffers_[i])
+			return -ENOMEM;
+	}
+
+	measuredFrames_ = 0;
+	frameProcessTime_ = 0;
+
+	return 0;
+}
+
+/*
+ * Get width and height at which the bayer-pattern repeats.
+ * Return pattern-size or an empty Size for an unsupported inputFormat.
+ */
+Size DebayerCpu::patternSize(PixelFormat inputFormat)
+{
+	DebayerCpu::DebayerInputConfig config;
+
+	if (getInputConfig(inputFormat, config) != 0)
+		return {};
+
+	return config.patternSize;
+}
+
+std::vector<PixelFormat> DebayerCpu::formats(PixelFormat inputFormat)
+{
+	DebayerCpu::DebayerInputConfig config;
+
+	if (getInputConfig(inputFormat, config) != 0)
+		return std::vector<PixelFormat>();
+
+	return config.outputFormats;
+}
+
+std::tuple<unsigned int, unsigned int>
+DebayerCpu::strideAndFrameSize(const PixelFormat &outputFormat, const Size &size)
+{
+	DebayerCpu::DebayerOutputConfig config;
+
+	if (getOutputConfig(outputFormat, config) != 0)
+		return std::make_tuple(0, 0);
+
+	/* round up to multiple of 8 for 64 bits alignment */
+	unsigned int stride = (size.width * config.bpp / 8 + 7) & ~7;
+
+	return std::make_tuple(stride, stride * size.height);
+}
+
+void DebayerCpu::setupInputMemcpy(const uint8_t *linePointers[])
+{
+	const unsigned int patternHeight = inputConfig_.patternSize.height;
+
+	if (!enableInputMemcpy_)
+		return;
+
+	for (unsigned int i = 0; i < patternHeight; i++) {
+		memcpy(lineBuffers_[i], linePointers[i + 1] - lineBufferPadding_,
+		       lineBufferLength_);
+		linePointers[i + 1] = lineBuffers_[i] + lineBufferPadding_;
+	}
+
+	/* Point lineBufferIndex_ to first unused lineBuffer */
+	lineBufferIndex_ = patternHeight;
+}
+
+void DebayerCpu::shiftLinePointers(const uint8_t *linePointers[], const uint8_t *src)
+{
+	const unsigned int patternHeight = inputConfig_.patternSize.height;
+
+	for (unsigned int i = 0; i < patternHeight; i++)
+		linePointers[i] = linePointers[i + 1];
+
+	linePointers[patternHeight] = src +
+				      (patternHeight / 2) * (int)inputConfig_.stride;
+}
+
+void DebayerCpu::memcpyNextLine(const uint8_t *linePointers[])
+{
+	const unsigned int patternHeight = inputConfig_.patternSize.height;
+
+	if (!enableInputMemcpy_)
+		return;
+
+	memcpy(lineBuffers_[lineBufferIndex_], linePointers[patternHeight] - lineBufferPadding_,
+	       lineBufferLength_);
+	linePointers[patternHeight] = lineBuffers_[lineBufferIndex_] + lineBufferPadding_;
+
+	lineBufferIndex_ = (lineBufferIndex_ + 1) % (patternHeight + 1);
+}
+
+void DebayerCpu::process2(const uint8_t *src, uint8_t *dst)
+{
+	unsigned int yEnd = window_.y + window_.height;
+	/* Holds [0] previous- [1] current- [2] next-line */
+	const uint8_t *linePointers[3];
+
+	/* Adjust src to top left corner of the window */
+	src += window_.y * inputConfig_.stride + window_.x * inputConfig_.bpp / 8;
+
+	/* [x] becomes [x - 1] after initial shiftLinePointers() call */
+	if (window_.y) {
+		linePointers[1] = src - inputConfig_.stride; /* previous-line */
+		linePointers[2] = src;
+	} else {
+		/* window_.y == 0, use the next line as prev line */
+		linePointers[1] = src + inputConfig_.stride;
+		linePointers[2] = src;
+		/* Last 2 lines also need special handling */
+		yEnd -= 2;
+	}
+
+	setupInputMemcpy(linePointers);
+
+	for (unsigned int y = window_.y; y < yEnd; y += 2) {
+		shiftLinePointers(linePointers, src);
+		memcpyNextLine(linePointers);
+		stats_->processLine0(y, linePointers);
+		(this->*debayer0_)(dst, linePointers);
+		src += inputConfig_.stride;
+		dst += outputConfig_.stride;
+
+		shiftLinePointers(linePointers, src);
+		memcpyNextLine(linePointers);
+		(this->*debayer1_)(dst, linePointers);
+		src += inputConfig_.stride;
+		dst += outputConfig_.stride;
+	}
+
+	if (window_.y == 0) {
+		shiftLinePointers(linePointers, src);
+		memcpyNextLine(linePointers);
+		stats_->processLine0(yEnd, linePointers);
+		(this->*debayer0_)(dst, linePointers);
+		src += inputConfig_.stride;
+		dst += outputConfig_.stride;
+
+		shiftLinePointers(linePointers, src);
+		/* next line may point outside of src, use prev. */
+		linePointers[2] = linePointers[0];
+		(this->*debayer1_)(dst, linePointers);
+		src += inputConfig_.stride;
+		dst += outputConfig_.stride;
+	}
+}
+
+void DebayerCpu::process4(const uint8_t *src, uint8_t *dst)
+{
+	const unsigned int yEnd = window_.y + window_.height;
+	/*
+	 * This holds pointers to [0] 2-lines-up [1] 1-line-up [2] current-line
+	 * [3] 1-line-down [4] 2-lines-down.
+	 */
+	const uint8_t *linePointers[5];
+
+	/* Adjust src to top left corner of the window */
+	src += window_.y * inputConfig_.stride + window_.x * inputConfig_.bpp / 8;
+
+	/* [x] becomes [x - 1] after initial shiftLinePointers() call */
+	linePointers[1] = src - 2 * inputConfig_.stride;
+	linePointers[2] = src - inputConfig_.stride;
+	linePointers[3] = src;
+	linePointers[4] = src + inputConfig_.stride;
+
+	setupInputMemcpy(linePointers);
+
+	for (unsigned int y = window_.y; y < yEnd; y += 4) {
+		shiftLinePointers(linePointers, src);
+		memcpyNextLine(linePointers);
+		stats_->processLine0(y, linePointers);
+		(this->*debayer0_)(dst, linePointers);
+		src += inputConfig_.stride;
+		dst += outputConfig_.stride;
+
+		shiftLinePointers(linePointers, src);
+		memcpyNextLine(linePointers);
+		(this->*debayer1_)(dst, linePointers);
+		src += inputConfig_.stride;
+		dst += outputConfig_.stride;
+
+		shiftLinePointers(linePointers, src);
+		memcpyNextLine(linePointers);
+		stats_->processLine2(y, linePointers);
+		(this->*debayer2_)(dst, linePointers);
+		src += inputConfig_.stride;
+		dst += outputConfig_.stride;
+
+		shiftLinePointers(linePointers, src);
+		memcpyNextLine(linePointers);
+		(this->*debayer3_)(dst, linePointers);
+		src += inputConfig_.stride;
+		dst += outputConfig_.stride;
+	}
+}
+
+static inline int64_t timeDiff(timespec &after, timespec &before)
+{
+	return (after.tv_sec - before.tv_sec) * 1000000000LL +
+	       (int64_t)after.tv_nsec - (int64_t)before.tv_nsec;
+}
+
+void DebayerCpu::process(FrameBuffer *input, FrameBuffer *output, DebayerParams params)
+{
+	timespec frameStartTime;
+
+	if (measuredFrames_ < DebayerCpu::kLastFrameToMeasure) {
+		frameStartTime = {};
+		clock_gettime(CLOCK_MONOTONIC_RAW, &frameStartTime);
+	}
+
+	green_ = params.green;
+	red_ = swapRedBlueGains_ ? params.blue : params.red;
+	blue_ = swapRedBlueGains_ ? params.red : params.blue;
+
+	/* Copy metadata from the input buffer */
+	FrameMetadata &metadata = output->_d()->metadata();
+	metadata.status = input->metadata().status;
+	metadata.sequence = input->metadata().sequence;
+	metadata.timestamp = input->metadata().timestamp;
+
+	MappedFrameBuffer in(input, MappedFrameBuffer::MapFlag::Read);
+	MappedFrameBuffer out(output, MappedFrameBuffer::MapFlag::Write);
+	if (!in.isValid() || !out.isValid()) {
+		LOG(Debayer, Error) << "mmap-ing buffer(s) failed";
+		metadata.status = FrameMetadata::FrameError;
+		return;
+	}
+
+	stats_->startFrame();
+
+	if (inputConfig_.patternSize.height == 2)
+		process2(in.planes()[0].data(), out.planes()[0].data());
+	else
+		process4(in.planes()[0].data(), out.planes()[0].data());
+
+	metadata.planes()[0].bytesused = out.planes()[0].size();
+
+	/* Measure before emitting signals */
+	if (measuredFrames_ < DebayerCpu::kLastFrameToMeasure &&
+	    ++measuredFrames_ > DebayerCpu::kFramesToSkip) {
+		timespec frameEndTime = {};
+		clock_gettime(CLOCK_MONOTONIC_RAW, &frameEndTime);
+		frameProcessTime_ += timeDiff(frameEndTime, frameStartTime);
+		if (measuredFrames_ == DebayerCpu::kLastFrameToMeasure) {
+			const unsigned int measuredFrames = DebayerCpu::kLastFrameToMeasure -
+							    DebayerCpu::kFramesToSkip;
+			LOG(Debayer, Info)
+				<< "Processed " << measuredFrames
+				<< " frames in " << frameProcessTime_ / 1000 << "us, "
+				<< frameProcessTime_ / (1000 * measuredFrames)
+				<< " us/frame";
+		}
+	}
+
+	stats_->finishFrame();
+	outputBufferReady.emit(output);
+	inputBufferReady.emit(input);
+}
+
+SizeRange DebayerCpu::sizes(PixelFormat inputFormat, const Size &inputSize)
+{
+	Size patternSize = this->patternSize(inputFormat);
+	unsigned int borderHeight = patternSize.height;
+
+	if (patternSize.isNull())
+		return {};
+
+	/* No need for top/bottom border with a pattern height of 2 */
+	if (patternSize.height == 2)
+		borderHeight = 0;
+
+	/*
+	 * For debayer interpolation a border is kept around the entire image
+	 * and the minimum output size is pattern-height x pattern-width.
+	 */
+	if (inputSize.width < (3 * patternSize.width) ||
+	    inputSize.height < (2 * borderHeight + patternSize.height)) {
+		LOG(Debayer, Warning)
+			<< "Input format size too small: " << inputSize.toString();
+		return {};
+	}
+
+	return SizeRange(Size(patternSize.width, patternSize.height),
+			 Size((inputSize.width - 2 * patternSize.width) & ~(patternSize.width - 1),
+			      (inputSize.height - 2 * borderHeight) & ~(patternSize.height - 1)),
+			 patternSize.width, patternSize.height);
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/software_isp/debayer_cpu.h b/src/libcamera/software_isp/debayer_cpu.h
new file mode 100644
index 0000000..1dac643
--- /dev/null
+++ b/src/libcamera/software_isp/debayer_cpu.h
@@ -0,0 +1,163 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ * Copyright (C) 2023, Red Hat Inc.
+ *
+ * Authors:
+ * Hans de Goede <hdegoede@redhat.com>
+ *
+ * CPU based debayering header
+ */
+
+#pragma once
+
+#include <memory>
+#include <stdint.h>
+#include <vector>
+
+#include <libcamera/base/object.h>
+
+#include "libcamera/internal/bayer_format.h"
+
+#include "debayer.h"
+#include "swstats_cpu.h"
+
+namespace libcamera {
+
+class DebayerCpu : public Debayer, public Object
+{
+public:
+	DebayerCpu(std::unique_ptr<SwStatsCpu> stats);
+	~DebayerCpu();
+
+	int configure(const StreamConfiguration &inputCfg,
+		      const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs);
+	Size patternSize(PixelFormat inputFormat);
+	std::vector<PixelFormat> formats(PixelFormat input);
+	std::tuple<unsigned int, unsigned int>
+	strideAndFrameSize(const PixelFormat &outputFormat, const Size &size);
+	void process(FrameBuffer *input, FrameBuffer *output, DebayerParams params);
+	SizeRange sizes(PixelFormat inputFormat, const Size &inputSize);
+
+	/**
+	 * \brief Get the file descriptor for the statistics
+	 *
+	 * \return the file descriptor pointing to the statistics
+	 */
+	const SharedFD &getStatsFD() { return stats_->getStatsFD(); }
+
+	/**
+	 * \brief Get the output frame size
+	 *
+	 * \return The output frame size
+	 */
+	unsigned int frameSize() { return outputConfig_.frameSize; }
+
+private:
+	/**
+	 * \brief Called to debayer 1 line of Bayer input data to output format
+	 * \param[out] dst Pointer to the start of the output line to write
+	 * \param[in] src The input data
+	 *
+	 * Input data is an array of (patternSize_.height + 1) src
+	 * pointers each pointing to a line in the Bayer source. The middle
+	 * element of the array will point to the actual line being processed.
+	 * Earlier element(s) will point to the previous line(s) and later
+	 * element(s) to the next line(s).
+	 *
+	 * These functions take an array of src pointers, rather than
+	 * a single src pointer + a stride for the source, so that when the src
+	 * is slow uncached memory it can be copied to faster memory before
+	 * debayering. Debayering a standard 2x2 Bayer pattern requires access
+	 * to the previous and next src lines for interpolating the missing
+	 * colors. To allow copying the src lines only once 3 temporary buffers
+	 * each holding a single line are used, re-using the oldest buffer for
+	 * the next line and the pointers are swizzled so that:
+	 * src[0] = previous-line, src[1] = currrent-line, src[2] = next-line.
+	 * This way the 3 pointers passed to the debayer functions form
+	 * a sliding window over the src avoiding the need to copy each
+	 * line more than once.
+	 *
+	 * Similarly for bayer patterns which repeat every 4 lines, 5 src
+	 * pointers are passed holding: src[0] = 2-lines-up, src[1] = 1-line-up
+	 * src[2] = current-line, src[3] = 1-line-down, src[4] = 2-lines-down.
+	 */
+	using debayerFn = void (DebayerCpu::*)(uint8_t *dst, const uint8_t *src[]);
+
+	/* 8-bit raw bayer format */
+	template<bool addAlphaByte>
+	void debayer8_BGBG_BGR888(uint8_t *dst, const uint8_t *src[]);
+	template<bool addAlphaByte>
+	void debayer8_GRGR_BGR888(uint8_t *dst, const uint8_t *src[]);
+	/* unpacked 10-bit raw bayer format */
+	template<bool addAlphaByte>
+	void debayer10_BGBG_BGR888(uint8_t *dst, const uint8_t *src[]);
+	template<bool addAlphaByte>
+	void debayer10_GRGR_BGR888(uint8_t *dst, const uint8_t *src[]);
+	/* unpacked 12-bit raw bayer format */
+	template<bool addAlphaByte>
+	void debayer12_BGBG_BGR888(uint8_t *dst, const uint8_t *src[]);
+	template<bool addAlphaByte>
+	void debayer12_GRGR_BGR888(uint8_t *dst, const uint8_t *src[]);
+	/* CSI-2 packed 10-bit raw bayer format (all the 4 orders) */
+	template<bool addAlphaByte>
+	void debayer10P_BGBG_BGR888(uint8_t *dst, const uint8_t *src[]);
+	template<bool addAlphaByte>
+	void debayer10P_GRGR_BGR888(uint8_t *dst, const uint8_t *src[]);
+	template<bool addAlphaByte>
+	void debayer10P_GBGB_BGR888(uint8_t *dst, const uint8_t *src[]);
+	template<bool addAlphaByte>
+	void debayer10P_RGRG_BGR888(uint8_t *dst, const uint8_t *src[]);
+
+	struct DebayerInputConfig {
+		Size patternSize;
+		unsigned int bpp; /* Memory used per pixel, not precision */
+		unsigned int stride;
+		std::vector<PixelFormat> outputFormats;
+	};
+
+	struct DebayerOutputConfig {
+		unsigned int bpp; /* Memory used per pixel, not precision */
+		unsigned int stride;
+		unsigned int frameSize;
+	};
+
+	int getInputConfig(PixelFormat inputFormat, DebayerInputConfig &config);
+	int getOutputConfig(PixelFormat outputFormat, DebayerOutputConfig &config);
+	int setupStandardBayerOrder(BayerFormat::Order order);
+	int setDebayerFunctions(PixelFormat inputFormat, PixelFormat outputFormat);
+	void setupInputMemcpy(const uint8_t *linePointers[]);
+	void shiftLinePointers(const uint8_t *linePointers[], const uint8_t *src);
+	void memcpyNextLine(const uint8_t *linePointers[]);
+	void process2(const uint8_t *src, uint8_t *dst);
+	void process4(const uint8_t *src, uint8_t *dst);
+
+	/* Max. supported Bayer pattern height is 4, debayering this requires 5 lines */
+	static constexpr unsigned int kMaxLineBuffers = 5;
+
+	DebayerParams::ColorLookupTable red_;
+	DebayerParams::ColorLookupTable green_;
+	DebayerParams::ColorLookupTable blue_;
+	debayerFn debayer0_;
+	debayerFn debayer1_;
+	debayerFn debayer2_;
+	debayerFn debayer3_;
+	Rectangle window_;
+	DebayerInputConfig inputConfig_;
+	DebayerOutputConfig outputConfig_;
+	std::unique_ptr<SwStatsCpu> stats_;
+	uint8_t *lineBuffers_[kMaxLineBuffers];
+	unsigned int lineBufferLength_;
+	unsigned int lineBufferPadding_;
+	unsigned int lineBufferIndex_;
+	unsigned int xShift_; /* Offset of 0/1 applied to window_.x */
+	bool enableInputMemcpy_;
+	bool swapRedBlueGains_;
+	unsigned int measuredFrames_;
+	int64_t frameProcessTime_;
+	/* Skip 30 frames for things to stabilize then measure 30 frames */
+	static constexpr unsigned int kFramesToSkip = 30;
+	static constexpr unsigned int kLastFrameToMeasure = 60;
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/software_isp/meson.build b/src/libcamera/software_isp/meson.build
new file mode 100644
index 0000000..f7c66e2
--- /dev/null
+++ b/src/libcamera/software_isp/meson.build
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: CC0-1.0
+
+softisp_enabled = pipelines.contains('simple')
+summary({'SoftISP support' : softisp_enabled}, section : 'Configuration')
+
+if not softisp_enabled
+    subdir_done()
+endif
+
+libcamera_sources += files([
+    'debayer.cpp',
+    'debayer_cpu.cpp',
+    'software_isp.cpp',
+    'swstats_cpu.cpp',
+])
diff --git a/src/libcamera/software_isp/software_isp.cpp b/src/libcamera/software_isp/software_isp.cpp
new file mode 100644
index 0000000..20fb6f4
--- /dev/null
+++ b/src/libcamera/software_isp/software_isp.cpp
@@ -0,0 +1,377 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ *
+ * Simple software ISP implementation
+ */
+
+#include "libcamera/internal/software_isp/software_isp.h"
+
+#include <cmath>
+#include <stdint.h>
+#include <sys/mman.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <libcamera/formats.h>
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/framebuffer.h"
+#include "libcamera/internal/ipa_manager.h"
+#include "libcamera/internal/mapped_framebuffer.h"
+#include "libcamera/internal/software_isp/debayer_params.h"
+
+#include "debayer_cpu.h"
+
+/**
+ * \file software_isp.cpp
+ * \brief Simple software ISP implementation
+ */
+
+namespace libcamera {
+
+LOG_DEFINE_CATEGORY(SoftwareIsp)
+
+/**
+ * \class SoftwareIsp
+ * \brief Class for the Software ISP
+ */
+
+/**
+ * \var SoftwareIsp::inputBufferReady
+ * \brief A signal emitted when the input frame buffer completes
+ */
+
+/**
+ * \var SoftwareIsp::outputBufferReady
+ * \brief A signal emitted when the output frame buffer completes
+ */
+
+/**
+ * \var SoftwareIsp::ispStatsReady
+ * \brief A signal emitted when the statistics for IPA are ready
+ */
+
+/**
+ * \var SoftwareIsp::setSensorControls
+ * \brief A signal emitted when the values to write to the sensor controls are
+ * ready
+ */
+
+/**
+ * \brief Constructs SoftwareIsp object
+ * \param[in] pipe The pipeline handler in use
+ * \param[in] sensor Pointer to the CameraSensor instance owned by the pipeline
+ * handler
+ */
+SoftwareIsp::SoftwareIsp(PipelineHandler *pipe, const CameraSensor *sensor)
+	: dmaHeap_(DmaBufAllocator::DmaBufAllocatorFlag::CmaHeap |
+		   DmaBufAllocator::DmaBufAllocatorFlag::SystemHeap |
+		   DmaBufAllocator::DmaBufAllocatorFlag::UDmaBuf)
+{
+	/*
+	 * debayerParams_ must be initialized because the initial value is used for
+	 * the first two frames, i.e. until stats processing starts providing its
+	 * own parameters.
+	 *
+	 * \todo This should be handled in the same place as the related
+	 * operations, in the IPA module.
+	 */
+	std::array<uint8_t, 256> gammaTable;
+	for (unsigned int i = 0; i < 256; i++)
+		gammaTable[i] = UINT8_MAX * std::pow(i / 256.0, 0.5);
+	for (unsigned int i = 0; i < DebayerParams::kRGBLookupSize; i++) {
+		debayerParams_.red[i] = gammaTable[i];
+		debayerParams_.green[i] = gammaTable[i];
+		debayerParams_.blue[i] = gammaTable[i];
+	}
+
+	if (!dmaHeap_.isValid()) {
+		LOG(SoftwareIsp, Error) << "Failed to create DmaBufAllocator object";
+		return;
+	}
+
+	sharedParams_ = SharedMemObject<DebayerParams>("softIsp_params");
+	if (!sharedParams_) {
+		LOG(SoftwareIsp, Error) << "Failed to create shared memory for parameters";
+		return;
+	}
+
+	auto stats = std::make_unique<SwStatsCpu>();
+	if (!stats->isValid()) {
+		LOG(SoftwareIsp, Error) << "Failed to create SwStatsCpu object";
+		return;
+	}
+	stats->statsReady.connect(this, &SoftwareIsp::statsReady);
+
+	debayer_ = std::make_unique<DebayerCpu>(std::move(stats));
+	debayer_->inputBufferReady.connect(this, &SoftwareIsp::inputReady);
+	debayer_->outputBufferReady.connect(this, &SoftwareIsp::outputReady);
+
+	ipa_ = IPAManager::createIPA<ipa::soft::IPAProxySoft>(pipe, 0, 0);
+	if (!ipa_) {
+		LOG(SoftwareIsp, Error)
+			<< "Creating IPA for software ISP failed";
+		debayer_.reset();
+		return;
+	}
+
+	/*
+	 * The API tuning file is made from the sensor name. If the tuning file
+	 * isn't found, fall back to the 'uncalibrated' file.
+	 */
+	std::string ipaTuningFile = ipa_->configurationFile(sensor->model() + ".yaml");
+	if (ipaTuningFile.empty())
+		ipaTuningFile = ipa_->configurationFile("uncalibrated.yaml");
+
+	int ret = ipa_->init(IPASettings{ ipaTuningFile, sensor->model() },
+			     debayer_->getStatsFD(),
+			     sharedParams_.fd(),
+			     sensor->controls());
+	if (ret) {
+		LOG(SoftwareIsp, Error) << "IPA init failed";
+		debayer_.reset();
+		return;
+	}
+
+	ipa_->setIspParams.connect(this, &SoftwareIsp::saveIspParams);
+	ipa_->setSensorControls.connect(this, &SoftwareIsp::setSensorCtrls);
+
+	debayer_->moveToThread(&ispWorkerThread_);
+}
+
+SoftwareIsp::~SoftwareIsp()
+{
+	/* make sure to destroy the DebayerCpu before the ispWorkerThread_ is gone */
+	debayer_.reset();
+}
+
+/**
+ * \fn int SoftwareIsp::loadConfiguration([[maybe_unused]] const std::string &filename)
+ * \brief Load a configuration from a file
+ * \param[in] filename The file to load the configuration data from
+ *
+ * Currently is a stub doing nothing and always returning "success".
+ *
+ * \return 0 on success
+ */
+
+/**
+ * \brief Process the statistics gathered
+ * \param[in] sensorControls The sensor controls
+ *
+ * Requests the IPA to calculate new parameters for ISP and new control
+ * values for the sensor.
+ */
+void SoftwareIsp::processStats(const ControlList &sensorControls)
+{
+	ASSERT(ipa_);
+	ipa_->processStats(sensorControls);
+}
+
+/**
+ * \brief Check the validity of Software Isp object
+ * \return True if Software Isp is valid, false otherwise
+ */
+bool SoftwareIsp::isValid() const
+{
+	return !!debayer_;
+}
+
+/**
+  * \brief Get the output formats supported for the given input format
+  * \param[in] inputFormat The input format
+  * \return All the supported output formats or an empty vector if there are none
+  */
+std::vector<PixelFormat> SoftwareIsp::formats(PixelFormat inputFormat)
+{
+	ASSERT(debayer_);
+
+	return debayer_->formats(inputFormat);
+}
+
+/**
+ * \brief Get the supported output sizes for the given input format and size
+ * \param[in] inputFormat The input format
+ * \param[in] inputSize The input frame size
+ * \return The valid size range or an empty range if there are none
+ */
+SizeRange SoftwareIsp::sizes(PixelFormat inputFormat, const Size &inputSize)
+{
+	ASSERT(debayer_);
+
+	return debayer_->sizes(inputFormat, inputSize);
+}
+
+/**
+ * Get the output stride and the frame size in bytes for the given output format and size
+ * \param[in] outputFormat The output format
+ * \param[in] size The output size (width and height in pixels)
+ * \return A tuple of the stride and the frame size in bytes, or a tuple of 0,0
+ * if there is no valid output config
+ */
+std::tuple<unsigned int, unsigned int>
+SoftwareIsp::strideAndFrameSize(const PixelFormat &outputFormat, const Size &size)
+{
+	ASSERT(debayer_);
+
+	return debayer_->strideAndFrameSize(outputFormat, size);
+}
+
+/**
+ * \brief Configure the SoftwareIsp object according to the passed in parameters
+ * \param[in] inputCfg The input configuration
+ * \param[in] outputCfgs The output configurations
+ * \param[in] sensorControls ControlInfoMap of the controls supported by the sensor
+ * \return 0 on success, a negative errno on failure
+ */
+int SoftwareIsp::configure(const StreamConfiguration &inputCfg,
+			   const std::vector<std::reference_wrapper<StreamConfiguration>> &outputCfgs,
+			   const ControlInfoMap &sensorControls)
+{
+	ASSERT(ipa_ && debayer_);
+
+	int ret = ipa_->configure(sensorControls);
+	if (ret < 0)
+		return ret;
+
+	return debayer_->configure(inputCfg, outputCfgs);
+}
+
+/**
+ * \brief Export the buffers from the Software ISP
+ * \param[in] output Output stream index exporting the buffers
+ * \param[in] count Number of buffers to allocate
+ * \param[out] buffers Vector to store the allocated buffers
+ * \return The number of allocated buffers on success or a negative error code
+ * otherwise
+ */
+int SoftwareIsp::exportBuffers(unsigned int output, unsigned int count,
+			       std::vector<std::unique_ptr<FrameBuffer>> *buffers)
+{
+	ASSERT(debayer_ != nullptr);
+
+	/* single output for now */
+	if (output >= 1)
+		return -EINVAL;
+
+	for (unsigned int i = 0; i < count; i++) {
+		const std::string name = "frame-" + std::to_string(i);
+		const size_t frameSize = debayer_->frameSize();
+
+		FrameBuffer::Plane outPlane;
+		outPlane.fd = SharedFD(dmaHeap_.alloc(name.c_str(), frameSize));
+		if (!outPlane.fd.isValid()) {
+			LOG(SoftwareIsp, Error)
+				<< "failed to allocate a dma_buf";
+			return -ENOMEM;
+		}
+		outPlane.offset = 0;
+		outPlane.length = frameSize;
+
+		std::vector<FrameBuffer::Plane> planes{ outPlane };
+		buffers->emplace_back(std::make_unique<FrameBuffer>(std::move(planes)));
+	}
+
+	return count;
+}
+
+/**
+ * \brief Queue buffers to Software ISP
+ * \param[in] input The input framebuffer
+ * \param[in] outputs The container holding the output stream indexes and
+ * their respective frame buffer outputs
+ * \return 0 on success, a negative errno on failure
+ */
+int SoftwareIsp::queueBuffers(FrameBuffer *input,
+			      const std::map<unsigned int, FrameBuffer *> &outputs)
+{
+	unsigned int mask = 0;
+
+	/*
+	 * Validate the outputs as a sanity check: at least one output is
+	 * required, all outputs must reference a valid stream and no two
+	 * outputs can reference the same stream.
+	 */
+	if (outputs.empty())
+		return -EINVAL;
+
+	for (auto [index, buffer] : outputs) {
+		if (!buffer)
+			return -EINVAL;
+		if (index >= 1) /* only single stream atm */
+			return -EINVAL;
+		if (mask & (1 << index))
+			return -EINVAL;
+
+		mask |= 1 << index;
+	}
+
+	process(input, outputs.at(0));
+
+	return 0;
+}
+
+/**
+ * \brief Starts the Software ISP streaming operation
+ * \return 0 on success, any other value indicates an error
+ */
+int SoftwareIsp::start()
+{
+	int ret = ipa_->start();
+	if (ret)
+		return ret;
+
+	ispWorkerThread_.start();
+	return 0;
+}
+
+/**
+ * \brief Stops the Software ISP streaming operation
+ */
+void SoftwareIsp::stop()
+{
+	ispWorkerThread_.exit();
+	ispWorkerThread_.wait();
+
+	ipa_->stop();
+}
+
+/**
+ * \brief Passes the input framebuffer to the ISP worker to process
+ * \param[in] input The input framebuffer
+ * \param[out] output The framebuffer to write the processed frame to
+ */
+void SoftwareIsp::process(FrameBuffer *input, FrameBuffer *output)
+{
+	debayer_->invokeMethod(&DebayerCpu::process,
+			       ConnectionTypeQueued, input, output, debayerParams_);
+}
+
+void SoftwareIsp::saveIspParams()
+{
+	debayerParams_ = *sharedParams_;
+}
+
+void SoftwareIsp::setSensorCtrls(const ControlList &sensorControls)
+{
+	setSensorControls.emit(sensorControls);
+}
+
+void SoftwareIsp::statsReady()
+{
+	ispStatsReady.emit();
+}
+
+void SoftwareIsp::inputReady(FrameBuffer *input)
+{
+	inputBufferReady.emit(input);
+}
+
+void SoftwareIsp::outputReady(FrameBuffer *output)
+{
+	outputBufferReady.emit(output);
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/software_isp/swstats_cpu.cpp b/src/libcamera/software_isp/swstats_cpu.cpp
new file mode 100644
index 0000000..815c4d4
--- /dev/null
+++ b/src/libcamera/software_isp/swstats_cpu.cpp
@@ -0,0 +1,432 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ * Copyright (C) 2023, Red Hat Inc.
+ *
+ * Authors:
+ * Hans de Goede <hdegoede@redhat.com>
+ *
+ * CPU based software statistics implementation
+ */
+
+#include "swstats_cpu.h"
+
+#include <libcamera/base/log.h>
+
+#include <libcamera/stream.h>
+
+#include "libcamera/internal/bayer_format.h"
+
+namespace libcamera {
+
+/**
+ * \class SwStatsCpu
+ * \brief Class for gathering statistics on the CPU
+ *
+ * CPU based software ISP statistics implementation.
+ *
+ * This class offers a configure function + functions to gather statistics on a
+ * line by line basis. This allows CPU based software debayering to interleave
+ * debayering and statistics gathering on a line by line basis while the input
+ * data is still hot in the cache.
+ *
+ * It is also possible to specify a window over which to gather statistics
+ * instead of processing the whole frame.
+ */
+
+/**
+ * \fn bool SwStatsCpu::isValid() const
+ * \brief Gets whether the statistics object is valid
+ *
+ * \return True if it's valid, false otherwise
+ */
+
+/**
+ * \fn const SharedFD &SwStatsCpu::getStatsFD()
+ * \brief Get the file descriptor for the statistics
+ *
+ * \return The file descriptor
+ */
+
+/**
+ * \fn const Size &SwStatsCpu::patternSize()
+ * \brief Get the pattern size
+ *
+ * For some input-formats, e.g. Bayer data, processing is done multiple lines
+ * and/or columns at a time. Get width and height at which the (bayer) pattern
+ * repeats. Window values are rounded down to a multiple of this and the height
+ * also indicates if processLine2() should be called or not.
+ * This may only be called after a successful configure() call.
+ *
+ * \return The pattern size
+ */
+
+/**
+ * \fn void SwStatsCpu::processLine0(unsigned int y, const uint8_t *src[])
+ * \brief Process line 0
+ * \param[in] y The y coordinate.
+ * \param[in] src The input data.
+ *
+ * This function processes line 0 for input formats with
+ * patternSize height == 1.
+ * It'll process line 0 and 1 for input formats with patternSize height >= 2.
+ * This function may only be called after a successful setWindow() call.
+ */
+
+/**
+ * \fn void SwStatsCpu::processLine2(unsigned int y, const uint8_t *src[])
+ * \brief Process line 2 and 3
+ * \param[in] y The y coordinate.
+ * \param[in] src The input data.
+ *
+ * This function processes line 2 and 3 for input formats with
+ * patternSize height == 4.
+ * This function may only be called after a successful setWindow() call.
+ */
+
+/**
+ * \var Signal<> SwStatsCpu::statsReady
+ * \brief Signals that the statistics are ready
+ */
+
+/**
+ * \typedef SwStatsCpu::statsProcessFn
+ * \brief Called when there is data to get statistics from
+ * \param[in] src The input data
+ *
+ * These functions take an array of (patternSize_.height + 1) src
+ * pointers each pointing to a line in the source image. The middle
+ * element of the array will point to the actual line being processed.
+ * Earlier element(s) will point to the previous line(s) and later
+ * element(s) to the next line(s).
+ *
+ * See the documentation of DebayerCpu::debayerFn for more details.
+ */
+
+/**
+ * \var unsigned int SwStatsCpu::ySkipMask_
+ * \brief Skip lines where this bitmask is set in y
+ */
+
+/**
+ * \var Rectangle SwStatsCpu::window_
+ * \brief Statistics window, set by setWindow(), used every line
+ */
+
+/**
+ * \var Size SwStatsCpu::patternSize_
+ * \brief The size of the bayer pattern
+ *
+ * Valid sizes are: 2x2, 4x2 or 4x4.
+ */
+
+/**
+ * \var unsigned int SwStatsCpu::xShift_
+ * \brief The offset of x, applied to window_.x for bayer variants
+ *
+ * This can either be 0 or 1.
+ */
+
+LOG_DEFINE_CATEGORY(SwStatsCpu)
+
+SwStatsCpu::SwStatsCpu()
+	: sharedStats_("softIsp_stats")
+{
+	if (!sharedStats_)
+		LOG(SwStatsCpu, Error)
+			<< "Failed to create shared memory for statistics";
+}
+
+static constexpr unsigned int kRedYMul = 77; /* 0.299 * 256 */
+static constexpr unsigned int kGreenYMul = 150; /* 0.587 * 256 */
+static constexpr unsigned int kBlueYMul = 29; /* 0.114 * 256 */
+
+#define SWSTATS_START_LINE_STATS(pixel_t) \
+	pixel_t r, g, g2, b;              \
+	uint64_t yVal;                    \
+                                          \
+	uint64_t sumR = 0;                \
+	uint64_t sumG = 0;                \
+	uint64_t sumB = 0;
+
+#define SWSTATS_ACCUMULATE_LINE_STATS(div) \
+	sumR += r;                         \
+	sumG += g;                         \
+	sumB += b;                         \
+                                           \
+	yVal = r * kRedYMul;               \
+	yVal += g * kGreenYMul;            \
+	yVal += b * kBlueYMul;             \
+	stats_.yHistogram[yVal * SwIspStats::kYHistogramSize / (256 * 256 * (div))]++;
+
+#define SWSTATS_FINISH_LINE_STATS() \
+	stats_.sumR_ += sumR;       \
+	stats_.sumG_ += sumG;       \
+	stats_.sumB_ += sumB;
+
+void SwStatsCpu::statsBGGR8Line0(const uint8_t *src[])
+{
+	const uint8_t *src0 = src[1] + window_.x;
+	const uint8_t *src1 = src[2] + window_.x;
+
+	SWSTATS_START_LINE_STATS(uint8_t)
+
+	if (swapLines_)
+		std::swap(src0, src1);
+
+	/* x += 4 sample every other 2x2 block */
+	for (int x = 0; x < (int)window_.width; x += 4) {
+		b = src0[x];
+		g = src0[x + 1];
+		g2 = src1[x];
+		r = src1[x + 1];
+
+		g = (g + g2) / 2;
+
+		SWSTATS_ACCUMULATE_LINE_STATS(1)
+	}
+
+	SWSTATS_FINISH_LINE_STATS()
+}
+
+void SwStatsCpu::statsBGGR10Line0(const uint8_t *src[])
+{
+	const uint16_t *src0 = (const uint16_t *)src[1] + window_.x;
+	const uint16_t *src1 = (const uint16_t *)src[2] + window_.x;
+
+	SWSTATS_START_LINE_STATS(uint16_t)
+
+	if (swapLines_)
+		std::swap(src0, src1);
+
+	/* x += 4 sample every other 2x2 block */
+	for (int x = 0; x < (int)window_.width; x += 4) {
+		b = src0[x];
+		g = src0[x + 1];
+		g2 = src1[x];
+		r = src1[x + 1];
+
+		g = (g + g2) / 2;
+
+		/* divide Y by 4 for 10 -> 8 bpp value */
+		SWSTATS_ACCUMULATE_LINE_STATS(4)
+	}
+
+	SWSTATS_FINISH_LINE_STATS()
+}
+
+void SwStatsCpu::statsBGGR12Line0(const uint8_t *src[])
+{
+	const uint16_t *src0 = (const uint16_t *)src[1] + window_.x;
+	const uint16_t *src1 = (const uint16_t *)src[2] + window_.x;
+
+	SWSTATS_START_LINE_STATS(uint16_t)
+
+	if (swapLines_)
+		std::swap(src0, src1);
+
+	/* x += 4 sample every other 2x2 block */
+	for (int x = 0; x < (int)window_.width; x += 4) {
+		b = src0[x];
+		g = src0[x + 1];
+		g2 = src1[x];
+		r = src1[x + 1];
+
+		g = (g + g2) / 2;
+
+		/* divide Y by 16 for 12 -> 8 bpp value */
+		SWSTATS_ACCUMULATE_LINE_STATS(16)
+	}
+
+	SWSTATS_FINISH_LINE_STATS()
+}
+
+void SwStatsCpu::statsBGGR10PLine0(const uint8_t *src[])
+{
+	const uint8_t *src0 = src[1] + window_.x * 5 / 4;
+	const uint8_t *src1 = src[2] + window_.x * 5 / 4;
+	const int widthInBytes = window_.width * 5 / 4;
+
+	if (swapLines_)
+		std::swap(src0, src1);
+
+	SWSTATS_START_LINE_STATS(uint8_t)
+
+	/* x += 5 sample every other 2x2 block */
+	for (int x = 0; x < widthInBytes; x += 5) {
+		/* BGGR */
+		b = src0[x];
+		g = src0[x + 1];
+		g2 = src1[x];
+		r = src1[x + 1];
+		g = (g + g2) / 2;
+		/* Data is already 8 bits, divide by 1 */
+		SWSTATS_ACCUMULATE_LINE_STATS(1)
+	}
+
+	SWSTATS_FINISH_LINE_STATS()
+}
+
+void SwStatsCpu::statsGBRG10PLine0(const uint8_t *src[])
+{
+	const uint8_t *src0 = src[1] + window_.x * 5 / 4;
+	const uint8_t *src1 = src[2] + window_.x * 5 / 4;
+	const int widthInBytes = window_.width * 5 / 4;
+
+	if (swapLines_)
+		std::swap(src0, src1);
+
+	SWSTATS_START_LINE_STATS(uint8_t)
+
+	/* x += 5 sample every other 2x2 block */
+	for (int x = 0; x < widthInBytes; x += 5) {
+		/* GBRG */
+		g = src0[x];
+		b = src0[x + 1];
+		r = src1[x];
+		g2 = src1[x + 1];
+		g = (g + g2) / 2;
+		/* Data is already 8 bits, divide by 1 */
+		SWSTATS_ACCUMULATE_LINE_STATS(1)
+	}
+
+	SWSTATS_FINISH_LINE_STATS()
+}
+
+/**
+ * \brief Reset state to start statistics gathering for a new frame
+ *
+ * This may only be called after a successful setWindow() call.
+ */
+void SwStatsCpu::startFrame(void)
+{
+	if (window_.width == 0)
+		LOG(SwStatsCpu, Error) << "Calling startFrame() without setWindow()";
+
+	stats_.sumR_ = 0;
+	stats_.sumB_ = 0;
+	stats_.sumG_ = 0;
+	stats_.yHistogram.fill(0);
+}
+
+/**
+ * \brief Finish statistics calculation for the current frame
+ *
+ * This may only be called after a successful setWindow() call.
+ */
+void SwStatsCpu::finishFrame(void)
+{
+	*sharedStats_ = stats_;
+	statsReady.emit();
+}
+
+/**
+ * \brief Setup SwStatsCpu object for standard Bayer orders
+ * \param[in] order The Bayer order
+ *
+ * Check if order is a standard Bayer order and setup xShift_ and swapLines_
+ * so that a single BGGR stats function can be used for all 4 standard orders.
+ */
+int SwStatsCpu::setupStandardBayerOrder(BayerFormat::Order order)
+{
+	switch (order) {
+	case BayerFormat::BGGR:
+		xShift_ = 0;
+		swapLines_ = false;
+		break;
+	case BayerFormat::GBRG:
+		xShift_ = 1; /* BGGR -> GBRG */
+		swapLines_ = false;
+		break;
+	case BayerFormat::GRBG:
+		xShift_ = 0;
+		swapLines_ = true; /* BGGR -> GRBG */
+		break;
+	case BayerFormat::RGGB:
+		xShift_ = 1; /* BGGR -> GBRG */
+		swapLines_ = true; /* GBRG -> RGGB */
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	patternSize_.height = 2;
+	patternSize_.width = 2;
+	ySkipMask_ = 0x02; /* Skip every 3th and 4th line */
+	return 0;
+}
+
+/**
+ * \brief Configure the statistics object for the passed in input format
+ * \param[in] inputCfg The input format
+ *
+ * \return 0 on success, a negative errno value on failure
+ */
+int SwStatsCpu::configure(const StreamConfiguration &inputCfg)
+{
+	BayerFormat bayerFormat =
+		BayerFormat::fromPixelFormat(inputCfg.pixelFormat);
+
+	if (bayerFormat.packing == BayerFormat::Packing::None &&
+	    setupStandardBayerOrder(bayerFormat.order) == 0) {
+		switch (bayerFormat.bitDepth) {
+		case 8:
+			stats0_ = &SwStatsCpu::statsBGGR8Line0;
+			return 0;
+		case 10:
+			stats0_ = &SwStatsCpu::statsBGGR10Line0;
+			return 0;
+		case 12:
+			stats0_ = &SwStatsCpu::statsBGGR12Line0;
+			return 0;
+		}
+	}
+
+	if (bayerFormat.bitDepth == 10 &&
+	    bayerFormat.packing == BayerFormat::Packing::CSI2) {
+		patternSize_.height = 2;
+		patternSize_.width = 4; /* 5 bytes per *4* pixels */
+		/* Skip every 3th and 4th line, sample every other 2x2 block */
+		ySkipMask_ = 0x02;
+		xShift_ = 0;
+
+		switch (bayerFormat.order) {
+		case BayerFormat::BGGR:
+		case BayerFormat::GRBG:
+			stats0_ = &SwStatsCpu::statsBGGR10PLine0;
+			swapLines_ = bayerFormat.order == BayerFormat::GRBG;
+			return 0;
+		case BayerFormat::GBRG:
+		case BayerFormat::RGGB:
+			stats0_ = &SwStatsCpu::statsGBRG10PLine0;
+			swapLines_ = bayerFormat.order == BayerFormat::RGGB;
+			return 0;
+		default:
+			break;
+		}
+	}
+
+	LOG(SwStatsCpu, Info)
+		<< "Unsupported input format " << inputCfg.pixelFormat.toString();
+	return -EINVAL;
+}
+
+/**
+ * \brief Specify window coordinates over which to gather statistics
+ * \param[in] window The window object.
+ */
+void SwStatsCpu::setWindow(const Rectangle &window)
+{
+	window_ = window;
+
+	window_.x &= ~(patternSize_.width - 1);
+	window_.x += xShift_;
+	window_.y &= ~(patternSize_.height - 1);
+
+	/* width_ - xShift_ to make sure the window fits */
+	window_.width -= xShift_;
+	window_.width &= ~(patternSize_.width - 1);
+	window_.height &= ~(patternSize_.height - 1);
+}
+
+} /* namespace libcamera */
diff --git a/src/libcamera/software_isp/swstats_cpu.h b/src/libcamera/software_isp/swstats_cpu.h
new file mode 100644
index 0000000..363e326
--- /dev/null
+++ b/src/libcamera/software_isp/swstats_cpu.h
@@ -0,0 +1,97 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+/*
+ * Copyright (C) 2023, Linaro Ltd
+ * Copyright (C) 2023, Red Hat Inc.
+ *
+ * Authors:
+ * Hans de Goede <hdegoede@redhat.com>
+ *
+ * CPU based software statistics implementation
+ */
+
+#pragma once
+
+#include <stdint.h>
+
+#include <libcamera/base/signal.h>
+
+#include <libcamera/geometry.h>
+
+#include "libcamera/internal/bayer_format.h"
+#include "libcamera/internal/shared_mem_object.h"
+#include "libcamera/internal/software_isp/swisp_stats.h"
+
+namespace libcamera {
+
+class PixelFormat;
+struct StreamConfiguration;
+
+class SwStatsCpu
+{
+public:
+	SwStatsCpu();
+	~SwStatsCpu() = default;
+
+	bool isValid() const { return sharedStats_.fd().isValid(); }
+
+	const SharedFD &getStatsFD() { return sharedStats_.fd(); }
+
+	const Size &patternSize() { return patternSize_; }
+
+	int configure(const StreamConfiguration &inputCfg);
+	void setWindow(const Rectangle &window);
+	void startFrame();
+	void finishFrame();
+
+	void processLine0(unsigned int y, const uint8_t *src[])
+	{
+		if ((y & ySkipMask_) || y < static_cast<unsigned int>(window_.y) ||
+		    y >= (window_.y + window_.height))
+			return;
+
+		(this->*stats0_)(src);
+	}
+
+	void processLine2(unsigned int y, const uint8_t *src[])
+	{
+		if ((y & ySkipMask_) || y < static_cast<unsigned int>(window_.y) ||
+		    y >= (window_.y + window_.height))
+			return;
+
+		(this->*stats2_)(src);
+	}
+
+	Signal<> statsReady;
+
+private:
+	using statsProcessFn = void (SwStatsCpu::*)(const uint8_t *src[]);
+
+	int setupStandardBayerOrder(BayerFormat::Order order);
+	/* Bayer 8 bpp unpacked */
+	void statsBGGR8Line0(const uint8_t *src[]);
+	/* Bayer 10 bpp unpacked */
+	void statsBGGR10Line0(const uint8_t *src[]);
+	/* Bayer 12 bpp unpacked */
+	void statsBGGR12Line0(const uint8_t *src[]);
+	/* Bayer 10 bpp packed */
+	void statsBGGR10PLine0(const uint8_t *src[]);
+	void statsGBRG10PLine0(const uint8_t *src[]);
+
+	/* Variables set by configure(), used every line */
+	statsProcessFn stats0_;
+	statsProcessFn stats2_;
+	bool swapLines_;
+
+	unsigned int ySkipMask_;
+
+	Rectangle window_;
+
+	Size patternSize_;
+
+	unsigned int xShift_;
+
+	SharedMemObject<SwIspStats> sharedStats_;
+	SwIspStats stats_;
+};
+
+} /* namespace libcamera */
diff --git a/src/libcamera/source_paths.cpp b/src/libcamera/source_paths.cpp
index 1968958..1af5386 100644
--- a/src/libcamera/source_paths.cpp
+++ b/src/libcamera/source_paths.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * source_paths.cpp - Identify libcamera source and build paths
+ * Identify libcamera source and build paths
  */
 
 #include "libcamera/internal/source_paths.h"
diff --git a/src/libcamera/stream.cpp b/src/libcamera/stream.cpp
index f3e00ea..053cc4b 100644
--- a/src/libcamera/stream.cpp
+++ b/src/libcamera/stream.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * stream.cpp - Video stream for a Camera
+ * Video stream for a Camera
  */
 
 #include <libcamera/stream.h>
@@ -433,7 +433,7 @@
 		"Viewfinder",
 	};
 
-	out << names[static_cast<std::underlying_type_t<StreamRole>>(role)];
+	out << names[utils::to_underlying(role)];
 	return out;
 }
 
diff --git a/src/libcamera/sysfs.cpp b/src/libcamera/sysfs.cpp
index 44c3331..3d9885b 100644
--- a/src/libcamera/sysfs.cpp
+++ b/src/libcamera/sysfs.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * sysfs.cpp - Miscellaneous utility functions to access sysfs
+ * Miscellaneous utility functions to access sysfs
  */
 
 #include "libcamera/internal/sysfs.h"
diff --git a/src/libcamera/tracepoints.cpp b/src/libcamera/tracepoints.cpp
index 0173b75..90662d1 100644
--- a/src/libcamera/tracepoints.cpp
+++ b/src/libcamera/tracepoints.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * tracepoints.cpp - Tracepoints with lttng
+ * Tracepoints with lttng
  */
 #define TRACEPOINT_CREATE_PROBES
 #define TRACEPOINT_DEFINE
diff --git a/src/libcamera/transform.cpp b/src/libcamera/transform.cpp
index 4668303..9fe8b56 100644
--- a/src/libcamera/transform.cpp
+++ b/src/libcamera/transform.cpp
@@ -2,11 +2,13 @@
 /*
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * transform.cpp - 2D plane transforms.
+ * 2D plane transforms.
  */
 
 #include <libcamera/transform.h>
 
+#include <libcamera/orientation.h>
+
 /**
  * \file transform.h
  * \brief Enum to represent and manipulate 2D plane transforms
@@ -187,24 +189,24 @@
  */
 
 /**
- * \brief Compose two transforms together
- * \param[in] t1 The second transform
- * \param[in] t0 The first transform
+ * \brief Compose two transforms by applying \a t0 first then \a t1
+ * \param[in] t0 The first transform to apply
+ * \param[in] t1 The second transform to apply
  *
- * Composing transforms follows the usual mathematical convention for
- * composing functions. That is, when performing `t1 * t0`, \a t0 is applied
- * first, and then \a t1.
- * For example, `Transpose * HFlip` performs `HFlip` first and then the
- * `Transpose` yielding `Rot270`, as shown below.
+ * Compose two transforms into a transform that is equivalent to first applying
+ * \a t0 and then applying \a t1. For example, `HFlip * Transpose` performs
+ * `HFlip` first and then the `Transpose` yielding `Rot270`, as shown below.
 ~~~
              A-B                 B-A                     B-D
 Input image  | |   -> HFLip ->   | |   -> Transpose ->   | |   = Rot270
              C-D                 D-C                     A-C
 ~~~
- * Note that composition is generally non-commutative for Transforms,
- * and not the same as XOR-ing the underlying bit representations.
+ * Note that composition is generally non-commutative for Transforms, and not
+ * the same as XOR-ing the underlying bit representations.
+ *
+ * \return A Transform equivalent to applying \a t0 and then \a t1
  */
-Transform operator*(Transform t1, Transform t0)
+Transform operator*(Transform t0, Transform t1)
 {
 	/*
 	 * Reorder the operations so that we imagine doing t0's transpose
@@ -299,6 +301,91 @@
 	return Transform::Identity;
 }
 
+namespace {
+
+/**
+ * \brief Return the transform representing \a orientation
+ * \param[in] orientation The orientation to convert
+ * \return The transform corresponding to \a orientation
+ */
+Transform transformFromOrientation(const Orientation &orientation)
+{
+	switch (orientation) {
+	case Orientation::Rotate0:
+		return Transform::Identity;
+	case Orientation::Rotate0Mirror:
+		return Transform::HFlip;
+	case Orientation::Rotate180:
+		return Transform::Rot180;
+	case Orientation::Rotate180Mirror:
+		return Transform::VFlip;
+	case Orientation::Rotate90Mirror:
+		return Transform::Transpose;
+	case Orientation::Rotate90:
+		return Transform::Rot90;
+	case Orientation::Rotate270Mirror:
+		return Transform::Rot180Transpose;
+	case Orientation::Rotate270:
+		return Transform::Rot270;
+	}
+
+	return Transform::Identity;
+}
+
+} /* namespace */
+
+/**
+ * \brief Return the Transform that applied to \a o2 gives \a o1
+ * \param o1 The Orientation to obtain
+ * \param o2 The base Orientation
+ *
+ * This operation can be used to easily compute the Transform to apply to a
+ * base orientation \a o2 to get the desired orientation \a o1.
+ *
+ * \return A Transform that applied to \a o2 gives \a o1
+ */
+Transform operator/(const Orientation &o1, const Orientation &o2)
+{
+	Transform t1 = transformFromOrientation(o1);
+	Transform t2 = transformFromOrientation(o2);
+
+	return -t2 * t1;
+}
+
+/**
+ * \brief Apply the Transform \a t on the orientation \a o
+ * \param o The orientation
+ * \param t The transform to apply on \a o
+ * \return The Orientation resulting from applying \a t on \a o
+ */
+Orientation operator*(const Orientation &o, const Transform &t)
+{
+	/*
+	 * Apply a Transform corresponding to the orientation first and
+	 * then apply \a t to it.
+	 */
+	switch (transformFromOrientation(o) * t) {
+	case Transform::Identity:
+		return Orientation::Rotate0;
+	case Transform::HFlip:
+		return Orientation::Rotate0Mirror;
+	case Transform::VFlip:
+		return Orientation::Rotate180Mirror;
+	case Transform::Rot180:
+		return Orientation::Rotate180;
+	case Transform::Transpose:
+		return Orientation::Rotate90Mirror;
+	case Transform::Rot270:
+		return Orientation::Rotate270;
+	case Transform::Rot90:
+		return Orientation::Rotate90;
+	case Transform::Rot180Transpose:
+		return Orientation::Rotate270Mirror;
+	}
+
+	return Orientation::Rotate0;
+}
+
 /**
  * \brief Return a character string describing the transform
  * \param[in] t The transform to be described.
diff --git a/src/libcamera/v4l2_device.cpp b/src/libcamera/v4l2_device.cpp
index 24d208e..4a2048c 100644
--- a/src/libcamera/v4l2_device.cpp
+++ b/src/libcamera/v4l2_device.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_device.cpp - Common base for V4L2 video devices and subdevices
+ * Common base for V4L2 video devices and subdevices
  */
 
 #include "libcamera/internal/v4l2_device.h"
diff --git a/src/libcamera/v4l2_pixelformat.cpp b/src/libcamera/v4l2_pixelformat.cpp
index a7ae69b..7056833 100644
--- a/src/libcamera/v4l2_pixelformat.cpp
+++ b/src/libcamera/v4l2_pixelformat.cpp
@@ -3,7 +3,7 @@
  * Copyright (C) 2019, Google Inc.
  * Copyright (C) 2020, Raspberry Pi Ltd
  *
- * v4l2_pixelformat.cpp - V4L2 Pixel Format
+ * V4L2 Pixel Format
  */
 
 #include "libcamera/internal/v4l2_pixelformat.h"
@@ -71,6 +71,10 @@
 		{ formats::BGRA8888, "32-bit ARGB 8-8-8-8" } },
 	{ V4L2PixelFormat(V4L2_PIX_FMT_BGRA32),
 		{ formats::RGBA8888, "32-bit ABGR 8-8-8-8" } },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_RGB48),
+		{ formats::BGR161616, "48-bit RGB 16-16-16" } },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_BGR48),
+		{ formats::RGB161616, "48-bit BGR 16-16-16" } },
 
 	/* YUV packed formats. */
 	{ V4L2PixelFormat(V4L2_PIX_FMT_YUYV),
@@ -123,7 +127,7 @@
 		{ formats::YVU422, "Planar YVU 4:2:2 (N-C)" } },
 	{ V4L2PixelFormat(V4L2_PIX_FMT_YUV444M),
 		{ formats::YUV444, "Planar YUV 4:4:4 (N-C)" } },
-	{ V4L2PixelFormat(V4L2_PIX_FMT_YUV444M),
+	{ V4L2PixelFormat(V4L2_PIX_FMT_YVU444M),
 		{ formats::YVU444, "Planar YVU 4:4:4 (N-C)" } },
 
 	/* Greyscale formats. */
@@ -135,6 +139,8 @@
 		{ formats::R10_CSI2P, "10-bit Greyscale Packed" } },
 	{ V4L2PixelFormat(V4L2_PIX_FMT_Y12),
 		{ formats::R12, "12-bit Greyscale" } },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_Y16),
+		{ formats::R16, "16-bit Greyscale" } },
 
 	/* Bayer formats. */
 	{ V4L2PixelFormat(V4L2_PIX_FMT_SBGGR8),
@@ -201,6 +207,16 @@
 		{ formats::SGRBG16, "16-bit Bayer GRGR/BGBG" } },
 	{ V4L2PixelFormat(V4L2_PIX_FMT_SRGGB16),
 		{ formats::SRGGB16, "16-bit Bayer RGRG/GBGB" } },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_BGGR),
+		{ formats::BGGR_PISP_COMP1, "16-bit Bayer BGBG/GRGR PiSP Compress Mode 1" } },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GBRG),
+		{ formats::GBRG_PISP_COMP1, "16-bit Bayer GBGB/RGRG PiSP Compress Mode 1" } },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_GRBG),
+		{ formats::GRBG_PISP_COMP1, "16-bit Bayer GRGR/BGBG PiSP Compress Mode 1" } },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_RGGB),
+		{ formats::RGGB_PISP_COMP1, "16-bit Bayer RGRG/GBGB PiSP Compress Mode 1" } },
+	{ V4L2PixelFormat(V4L2_PIX_FMT_PISP_COMP1_MONO),
+		{ formats::MONO_PISP_COMP1, "16-bit Mono PiSP Compress Mode 1" } },
 
 	/* Compressed formats. */
 	{ V4L2PixelFormat(V4L2_PIX_FMT_MJPEG),
diff --git a/src/libcamera/v4l2_subdevice.cpp b/src/libcamera/v4l2_subdevice.cpp
index 15e8206..8282443 100644
--- a/src/libcamera/v4l2_subdevice.cpp
+++ b/src/libcamera/v4l2_subdevice.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_subdevice.cpp - V4L2 Subdevice
+ * V4L2 Subdevice
  */
 
 #include "libcamera/internal/v4l2_subdevice.h"
@@ -36,110 +36,703 @@
 
 LOG_DECLARE_CATEGORY(V4L2)
 
+/**
+ * \class MediaBusFormatInfo
+ * \brief Information about media bus formats
+ *
+ * The MediaBusFormatInfo class groups together information describing a media
+ * bus format. It facilitates handling of media bus formats by providing data
+ * commonly used in pipeline handlers.
+ *
+ * \var MediaBusFormatInfo::name
+ * \brief The format name as a human-readable string, used as the text
+ * representation of the format
+ *
+ * \var MediaBusFormatInfo::code
+ * \brief The media bus format code described by this instance (MEDIA_BUS_FMT_*)
+ *
+ * \var MediaBusFormatInfo::type
+ * \brief The media bus format type
+ *
+ * \var MediaBusFormatInfo::bitsPerPixel
+ * \brief The average number of bits per pixel
+ *
+ * The number of bits per pixel averages the total number of bits for all
+ * colour components over the whole image, excluding any padding bits or
+ * padding pixels.
+ *
+ * For formats that transmit multiple or fractional pixels per sample, the
+ * value will differ from the bus width.
+ *
+ * Formats that don't have a fixed number of bits per pixel, such as compressed
+ * formats, or device-specific embedded data formats, report 0 in this field.
+ *
+ * \var MediaBusFormatInfo::colourEncoding
+ * \brief The colour encoding type
+ *
+ * This field is valid for Type::Image formats only.
+ */
+
+/**
+ * \enum MediaBusFormatInfo::Type
+ * \brief The format type
+ *
+ * \var MediaBusFormatInfo::Type::Image
+ * \brief The format describes image data
+ *
+ * \var MediaBusFormatInfo::Type::Metadata
+ * \brief The format describes generic metadata
+ *
+ * \var MediaBusFormatInfo::Type::EmbeddedData
+ * \brief The format describes sensor embedded data
+ */
+
 namespace {
 
-/*
- * \struct V4L2SubdeviceFormatInfo
- * \brief Information about media bus formats
- * \param bitsPerPixel Bits per pixel
- * \param name Name of MBUS format
- * \param colourEncoding Type of colour encoding
- */
-struct V4L2SubdeviceFormatInfo {
-	unsigned int bitsPerPixel;
-	const char *name;
-	PixelFormatInfo::ColourEncoding colourEncoding;
-};
-
-/*
- * \var formatInfoMap
- * \brief A map that associates V4L2SubdeviceFormatInfo struct to V4L2 media
- * bus codes
- */
-const std::map<uint32_t, V4L2SubdeviceFormatInfo> formatInfoMap = {
-	{ MEDIA_BUS_FMT_RGB444_2X8_PADHI_BE, { 16, "RGB444_2X8_PADHI_BE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB444_2X8_PADHI_LE, { 16, "RGB444_2X8_PADHI_LE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB555_2X8_PADHI_BE, { 16, "RGB555_2X8_PADHI_BE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB555_2X8_PADHI_LE, { 16, "RGB555_2X8_PADHI_LE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB565_1X16, { 16, "RGB565_1X16", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_BGR565_2X8_BE, { 16, "BGR565_2X8_BE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_BGR565_2X8_LE, { 16, "BGR565_2X8_LE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB565_2X8_BE, { 16, "RGB565_2X8_BE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB565_2X8_LE, { 16, "RGB565_2X8_LE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB666_1X18, { 18, "RGB666_1X18", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_BGR888_1X24, { 24, "BGR888_1X24", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB888_1X24, { 24, "RGB888_1X24", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB888_2X12_BE, { 24, "RGB888_2X12_BE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_RGB888_2X12_LE, { 24, "RGB888_2X12_LE", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_ARGB8888_1X32, { 32, "ARGB8888_1X32", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_Y8_1X8, { 8, "Y8_1X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_UV8_1X8, { 8, "UV8_1X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_UYVY8_1_5X8, { 12, "UYVY8_1_5X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_VYUY8_1_5X8, { 12, "VYUY8_1_5X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YUYV8_1_5X8, { 12, "YUYV8_1_5X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YVYU8_1_5X8, { 12, "YVYU8_1_5X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_UYVY8_2X8, { 16, "UYVY8_2X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_VYUY8_2X8, { 16, "VYUY8_2X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YUYV8_2X8, { 16, "YUYV8_2X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YVYU8_2X8, { 16, "YVYU8_2X8", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_Y10_1X10, { 10, "Y10_1X10", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_UYVY10_2X10, { 20, "UYVY10_2X10", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_VYUY10_2X10, { 20, "VYUY10_2X10", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YUYV10_2X10, { 20, "YUYV10_2X10", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YVYU10_2X10, { 20, "YVYU10_2X10", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_Y12_1X12, { 12, "Y12_1X12", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_UYVY8_1X16, { 16, "UYVY8_1X16", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_VYUY8_1X16, { 16, "VYUY8_1X16", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YUYV8_1X16, { 16, "YUYV8_1X16", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YVYU8_1X16, { 16, "YVYU8_1X16", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YDYUYDYV8_1X16, { 16, "YDYUYDYV8_1X16", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_UYVY10_1X20, { 20, "UYVY10_1X20", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_VYUY10_1X20, { 20, "VYUY10_1X20", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YUYV10_1X20, { 20, "YUYV10_1X20", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YVYU10_1X20, { 20, "YVYU10_1X20", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YUV8_1X24, { 24, "YUV8_1X24", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YUV10_1X30, { 30, "YUV10_1X30", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_AYUV8_1X32, { 32, "AYUV8_1X32", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_UYVY12_2X12, { 24, "UYVY12_2X12", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_VYUY12_2X12, { 24, "VYUY12_2X12", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YUYV12_2X12, { 24, "YUYV12_2X12", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YVYU12_2X12, { 24, "YVYU12_2X12", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_UYVY12_1X24, { 24, "UYVY12_1X24", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_VYUY12_1X24, { 24, "VYUY12_1X24", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YUYV12_1X24, { 24, "YUYV12_1X24", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_YVYU12_1X24, { 24, "YVYU12_1X24", PixelFormatInfo::ColourEncodingYUV } },
-	{ MEDIA_BUS_FMT_SBGGR8_1X8, { 8, "SBGGR8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGBRG8_1X8, { 8, "SGBRG8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGRBG8_1X8, { 8, "SGRBG8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SRGGB8_1X8, { 8, "SRGGB8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SBGGR10_ALAW8_1X8, { 8, "SBGGR10_ALAW8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGBRG10_ALAW8_1X8, { 8, "SGBRG10_ALAW8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGRBG10_ALAW8_1X8, { 8, "SGRBG10_ALAW8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SRGGB10_ALAW8_1X8, { 8, "SRGGB10_ALAW8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SBGGR10_DPCM8_1X8, { 8, "SBGGR10_DPCM8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGBRG10_DPCM8_1X8, { 8, "SGBRG10_DPCM8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGRBG10_DPCM8_1X8, { 8, "SGRBG10_DPCM8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SRGGB10_DPCM8_1X8, { 8, "SRGGB10_DPCM8_1X8", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_BE, { 16, "SBGGR10_2X8_PADHI_BE", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_LE, { 16, "SBGGR10_2X8_PADHI_LE", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_BE, { 16, "SBGGR10_2X8_PADLO_BE", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_LE, { 16, "SBGGR10_2X8_PADLO_LE", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SBGGR10_1X10, { 10, "SBGGR10_1X10", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGBRG10_1X10, { 10, "SGBRG10_1X10", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGRBG10_1X10, { 10, "SGRBG10_1X10", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SRGGB10_1X10, { 10, "SRGGB10_1X10", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SBGGR12_1X12, { 12, "SBGGR12_1X12", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGBRG12_1X12, { 12, "SGBRG12_1X12", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SGRBG12_1X12, { 12, "SGRBG12_1X12", PixelFormatInfo::ColourEncodingRAW } },
-	{ MEDIA_BUS_FMT_SRGGB12_1X12, { 12, "SRGGB12_1X12", PixelFormatInfo::ColourEncodingRAW } },
+const std::map<uint32_t, MediaBusFormatInfo> mediaBusFormatInfo{
+	/* This table is sorted to match the order in linux/media-bus-format.h */
+	{ MEDIA_BUS_FMT_RGB444_2X8_PADHI_BE, {
+		.name = "RGB444_2X8_PADHI_BE",
+		.code = MEDIA_BUS_FMT_RGB444_2X8_PADHI_BE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB444_2X8_PADHI_LE, {
+		.name = "RGB444_2X8_PADHI_LE",
+		.code = MEDIA_BUS_FMT_RGB444_2X8_PADHI_LE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB555_2X8_PADHI_BE, {
+		.name = "RGB555_2X8_PADHI_BE",
+		.code = MEDIA_BUS_FMT_RGB555_2X8_PADHI_BE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB555_2X8_PADHI_LE, {
+		.name = "RGB555_2X8_PADHI_LE",
+		.code = MEDIA_BUS_FMT_RGB555_2X8_PADHI_LE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB565_1X16, {
+		.name = "RGB565_1X16",
+		.code = MEDIA_BUS_FMT_RGB565_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_BGR565_2X8_BE, {
+		.name = "BGR565_2X8_BE",
+		.code = MEDIA_BUS_FMT_BGR565_2X8_BE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_BGR565_2X8_LE, {
+		.name = "BGR565_2X8_LE",
+		.code = MEDIA_BUS_FMT_BGR565_2X8_LE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB565_2X8_BE, {
+		.name = "RGB565_2X8_BE",
+		.code = MEDIA_BUS_FMT_RGB565_2X8_BE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB565_2X8_LE, {
+		.name = "RGB565_2X8_LE",
+		.code = MEDIA_BUS_FMT_RGB565_2X8_LE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB666_1X18, {
+		.name = "RGB666_1X18",
+		.code = MEDIA_BUS_FMT_RGB666_1X18,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 18,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_BGR888_1X24, {
+		.name = "BGR888_1X24",
+		.code = MEDIA_BUS_FMT_BGR888_1X24,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB888_1X24, {
+		.name = "RGB888_1X24",
+		.code = MEDIA_BUS_FMT_RGB888_1X24,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB888_2X12_BE, {
+		.name = "RGB888_2X12_BE",
+		.code = MEDIA_BUS_FMT_RGB888_2X12_BE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_RGB888_2X12_LE, {
+		.name = "RGB888_2X12_LE",
+		.code = MEDIA_BUS_FMT_RGB888_2X12_LE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_ARGB8888_1X32, {
+		.name = "ARGB8888_1X32",
+		.code = MEDIA_BUS_FMT_ARGB8888_1X32,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 32,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_Y8_1X8, {
+		.name = "Y8_1X8",
+		.code = MEDIA_BUS_FMT_Y8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_UV8_1X8, {
+		.name = "UV8_1X8",
+		.code = MEDIA_BUS_FMT_UV8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_UYVY8_1_5X8, {
+		.name = "UYVY8_1_5X8",
+		.code = MEDIA_BUS_FMT_UYVY8_1_5X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 12,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_VYUY8_1_5X8, {
+		.name = "VYUY8_1_5X8",
+		.code = MEDIA_BUS_FMT_VYUY8_1_5X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 12,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YUYV8_1_5X8, {
+		.name = "YUYV8_1_5X8",
+		.code = MEDIA_BUS_FMT_YUYV8_1_5X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 12,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YVYU8_1_5X8, {
+		.name = "YVYU8_1_5X8",
+		.code = MEDIA_BUS_FMT_YVYU8_1_5X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 12,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_UYVY8_2X8, {
+		.name = "UYVY8_2X8",
+		.code = MEDIA_BUS_FMT_UYVY8_2X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_VYUY8_2X8, {
+		.name = "VYUY8_2X8",
+		.code = MEDIA_BUS_FMT_VYUY8_2X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YUYV8_2X8, {
+		.name = "YUYV8_2X8",
+		.code = MEDIA_BUS_FMT_YUYV8_2X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YVYU8_2X8, {
+		.name = "YVYU8_2X8",
+		.code = MEDIA_BUS_FMT_YVYU8_2X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_Y10_1X10, {
+		.name = "Y10_1X10",
+		.code = MEDIA_BUS_FMT_Y10_1X10,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 10,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_UYVY10_2X10, {
+		.name = "UYVY10_2X10",
+		.code = MEDIA_BUS_FMT_UYVY10_2X10,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 20,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_VYUY10_2X10, {
+		.name = "VYUY10_2X10",
+		.code = MEDIA_BUS_FMT_VYUY10_2X10,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 20,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YUYV10_2X10, {
+		.name = "YUYV10_2X10",
+		.code = MEDIA_BUS_FMT_YUYV10_2X10,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 20,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YVYU10_2X10, {
+		.name = "YVYU10_2X10",
+		.code = MEDIA_BUS_FMT_YVYU10_2X10,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 20,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_Y12_1X12, {
+		.name = "Y12_1X12",
+		.code = MEDIA_BUS_FMT_Y12_1X12,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 12,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_Y16_1X16, {
+		.name = "Y16_1X16",
+		.code = MEDIA_BUS_FMT_Y16_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_UYVY8_1X16, {
+		.name = "UYVY8_1X16",
+		.code = MEDIA_BUS_FMT_UYVY8_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_VYUY8_1X16, {
+		.name = "VYUY8_1X16",
+		.code = MEDIA_BUS_FMT_VYUY8_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YUYV8_1X16, {
+		.name = "YUYV8_1X16",
+		.code = MEDIA_BUS_FMT_YUYV8_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YVYU8_1X16, {
+		.name = "YVYU8_1X16",
+		.code = MEDIA_BUS_FMT_YVYU8_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YDYUYDYV8_1X16, {
+		.name = "YDYUYDYV8_1X16",
+		.code = MEDIA_BUS_FMT_YDYUYDYV8_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_UYVY10_1X20, {
+		.name = "UYVY10_1X20",
+		.code = MEDIA_BUS_FMT_UYVY10_1X20,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 20,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_VYUY10_1X20, {
+		.name = "VYUY10_1X20",
+		.code = MEDIA_BUS_FMT_VYUY10_1X20,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 20,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YUYV10_1X20, {
+		.name = "YUYV10_1X20",
+		.code = MEDIA_BUS_FMT_YUYV10_1X20,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 20,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YVYU10_1X20, {
+		.name = "YVYU10_1X20",
+		.code = MEDIA_BUS_FMT_YVYU10_1X20,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 20,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YUV8_1X24, {
+		.name = "YUV8_1X24",
+		.code = MEDIA_BUS_FMT_YUV8_1X24,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YUV10_1X30, {
+		.name = "YUV10_1X30",
+		.code = MEDIA_BUS_FMT_YUV10_1X30,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 30,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_AYUV8_1X32, {
+		.name = "AYUV8_1X32",
+		.code = MEDIA_BUS_FMT_AYUV8_1X32,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 32,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_UYVY12_2X12, {
+		.name = "UYVY12_2X12",
+		.code = MEDIA_BUS_FMT_UYVY12_2X12,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_VYUY12_2X12, {
+		.name = "VYUY12_2X12",
+		.code = MEDIA_BUS_FMT_VYUY12_2X12,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YUYV12_2X12, {
+		.name = "YUYV12_2X12",
+		.code = MEDIA_BUS_FMT_YUYV12_2X12,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YVYU12_2X12, {
+		.name = "YVYU12_2X12",
+		.code = MEDIA_BUS_FMT_YVYU12_2X12,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_UYVY12_1X24, {
+		.name = "UYVY12_1X24",
+		.code = MEDIA_BUS_FMT_UYVY12_1X24,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_VYUY12_1X24, {
+		.name = "VYUY12_1X24",
+		.code = MEDIA_BUS_FMT_VYUY12_1X24,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YUYV12_1X24, {
+		.name = "YUYV12_1X24",
+		.code = MEDIA_BUS_FMT_YUYV12_1X24,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_YVYU12_1X24, {
+		.name = "YVYU12_1X24",
+		.code = MEDIA_BUS_FMT_YVYU12_1X24,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 24,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR8_1X8, {
+		.name = "SBGGR8_1X8",
+		.code = MEDIA_BUS_FMT_SBGGR8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGBRG8_1X8, {
+		.name = "SGBRG8_1X8",
+		.code = MEDIA_BUS_FMT_SGBRG8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGRBG8_1X8, {
+		.name = "SGRBG8_1X8",
+		.code = MEDIA_BUS_FMT_SGRBG8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SRGGB8_1X8, {
+		.name = "SRGGB8_1X8",
+		.code = MEDIA_BUS_FMT_SRGGB8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR10_ALAW8_1X8, {
+		.name = "SBGGR10_ALAW8_1X8",
+		.code = MEDIA_BUS_FMT_SBGGR10_ALAW8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGBRG10_ALAW8_1X8, {
+		.name = "SGBRG10_ALAW8_1X8",
+		.code = MEDIA_BUS_FMT_SGBRG10_ALAW8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGRBG10_ALAW8_1X8, {
+		.name = "SGRBG10_ALAW8_1X8",
+		.code = MEDIA_BUS_FMT_SGRBG10_ALAW8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SRGGB10_ALAW8_1X8, {
+		.name = "SRGGB10_ALAW8_1X8",
+		.code = MEDIA_BUS_FMT_SRGGB10_ALAW8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR10_DPCM8_1X8, {
+		.name = "SBGGR10_DPCM8_1X8",
+		.code = MEDIA_BUS_FMT_SBGGR10_DPCM8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGBRG10_DPCM8_1X8, {
+		.name = "SGBRG10_DPCM8_1X8",
+		.code = MEDIA_BUS_FMT_SGBRG10_DPCM8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGRBG10_DPCM8_1X8, {
+		.name = "SGRBG10_DPCM8_1X8",
+		.code = MEDIA_BUS_FMT_SGRBG10_DPCM8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SRGGB10_DPCM8_1X8, {
+		.name = "SRGGB10_DPCM8_1X8",
+		.code = MEDIA_BUS_FMT_SRGGB10_DPCM8_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_BE, {
+		.name = "SBGGR10_2X8_PADHI_BE",
+		.code = MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_BE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_LE, {
+		.name = "SBGGR10_2X8_PADHI_LE",
+		.code = MEDIA_BUS_FMT_SBGGR10_2X8_PADHI_LE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_BE, {
+		.name = "SBGGR10_2X8_PADLO_BE",
+		.code = MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_BE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_LE, {
+		.name = "SBGGR10_2X8_PADLO_LE",
+		.code = MEDIA_BUS_FMT_SBGGR10_2X8_PADLO_LE,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR10_1X10, {
+		.name = "SBGGR10_1X10",
+		.code = MEDIA_BUS_FMT_SBGGR10_1X10,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 10,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGBRG10_1X10, {
+		.name = "SGBRG10_1X10",
+		.code = MEDIA_BUS_FMT_SGBRG10_1X10,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 10,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGRBG10_1X10, {
+		.name = "SGRBG10_1X10",
+		.code = MEDIA_BUS_FMT_SGRBG10_1X10,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 10,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SRGGB10_1X10, {
+		.name = "SRGGB10_1X10",
+		.code = MEDIA_BUS_FMT_SRGGB10_1X10,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 10,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR12_1X12, {
+		.name = "SBGGR12_1X12",
+		.code = MEDIA_BUS_FMT_SBGGR12_1X12,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 12,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGBRG12_1X12, {
+		.name = "SGBRG12_1X12",
+		.code = MEDIA_BUS_FMT_SGBRG12_1X12,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 12,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGRBG12_1X12, {
+		.name = "SGRBG12_1X12",
+		.code = MEDIA_BUS_FMT_SGRBG12_1X12,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 12,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SRGGB12_1X12, {
+		.name = "SRGGB12_1X12",
+		.code = MEDIA_BUS_FMT_SRGGB12_1X12,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 12,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR14_1X14, {
+		.name = "SBGGR14_1X14",
+		.code = MEDIA_BUS_FMT_SBGGR14_1X14,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 14,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGBRG14_1X14, {
+		.name = "SGBRG14_1X14",
+		.code = MEDIA_BUS_FMT_SGBRG14_1X14,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 14,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SGRBG14_1X14, {
+		.name = "SGRBG14_1X14",
+		.code = MEDIA_BUS_FMT_SGRBG14_1X14,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 14,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SRGGB14_1X14, {
+		.name = "SRGGB14_1X14",
+		.code = MEDIA_BUS_FMT_SRGGB14_1X14,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 14,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
+	{ MEDIA_BUS_FMT_SBGGR16_1X16, {
+		.name = "SBGGR16_1X16",
+		.code = MEDIA_BUS_FMT_SBGGR16_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW
+	} },
+	{ MEDIA_BUS_FMT_SGBRG16_1X16, {
+		.name = "SGBRG16_1X16",
+		.code = MEDIA_BUS_FMT_SGBRG16_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW
+	} },
+	{ MEDIA_BUS_FMT_SGRBG16_1X16, {
+		.name = "SGRBG16_1X16",
+		.code = MEDIA_BUS_FMT_SGRBG16_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW
+	} },
+	{ MEDIA_BUS_FMT_SRGGB16_1X16, {
+		.name = "SRGGB16_1X16",
+		.code = MEDIA_BUS_FMT_SRGGB16_1X16,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 16,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW
+	} },
 	/* \todo Clarify colour encoding for HSV formats */
-	{ MEDIA_BUS_FMT_AHSV8888_1X32, { 32, "AHSV8888_1X32", PixelFormatInfo::ColourEncodingRGB } },
-	{ MEDIA_BUS_FMT_JPEG_1X8, { 8, "JPEG_1X8", PixelFormatInfo::ColourEncodingYUV } },
+	{ MEDIA_BUS_FMT_AHSV8888_1X32, {
+		.name = "AHSV8888_1X32",
+		.code = MEDIA_BUS_FMT_AHSV8888_1X32,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 32,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRGB,
+	} },
+	{ MEDIA_BUS_FMT_JPEG_1X8, {
+		.name = "JPEG_1X8",
+		.code = MEDIA_BUS_FMT_JPEG_1X8,
+		.type = MediaBusFormatInfo::Type::Image,
+		.bitsPerPixel = 8,
+		.colourEncoding = PixelFormatInfo::ColourEncodingYUV,
+	} },
+	{ MEDIA_BUS_FMT_METADATA_FIXED, {
+		.name = "METADATA_FIXED",
+		.code = MEDIA_BUS_FMT_METADATA_FIXED,
+		.type = MediaBusFormatInfo::Type::Metadata,
+		.bitsPerPixel = 0,
+		.colourEncoding = PixelFormatInfo::ColourEncodingRAW,
+	} },
 };
 
 } /* namespace */
 
 /**
+ * \fn bool MediaBusFormatInfo::isValid() const
+ * \brief Check if the media bus format info is valid
+ * \return True if the media bus format info is valid, false otherwise
+ */
+
+/**
+ * \brief Retrieve information about a media bus format
+ * \param[in] code The media bus format code
+ * \return The MediaBusFormatInfo describing the \a code if known, or an invalid
+ * MediaBusFormatInfo otherwise
+ */
+const MediaBusFormatInfo &MediaBusFormatInfo::info(uint32_t code)
+{
+	static const MediaBusFormatInfo invalid{};
+
+	const auto it = mediaBusFormatInfo.find(code);
+	if (it == mediaBusFormatInfo.end()) {
+		LOG(V4L2, Warning)
+			<< "Unsupported media bus format "
+			<< utils::hex(code, 4);
+		return invalid;
+	}
+
+	return it->second;
+}
+
+/**
  * \struct V4L2SubdeviceCapability
  * \brief struct v4l2_subdev_capability object wrapper and helpers
  *
@@ -192,7 +785,7 @@
  */
 
 /**
- * \var V4L2SubdeviceFormat::mbus_code
+ * \var V4L2SubdeviceFormat::code
  * \brief The image format bus code
  */
 
@@ -229,23 +822,6 @@
 }
 
 /**
- * \brief Retrieve the number of bits per pixel for the V4L2 subdevice format
- * \return The number of bits per pixel for the format, or 0 if the format is
- * not supported
- */
-uint8_t V4L2SubdeviceFormat::bitsPerPixel() const
-{
-	const auto it = formatInfoMap.find(mbus_code);
-	if (it == formatInfoMap.end()) {
-		LOG(V4L2, Error) << "No information available for format '"
-				 << *this << "'";
-		return 0;
-	}
-
-	return it->second.bitsPerPixel;
-}
-
-/**
  * \brief Insert a text representation of a V4L2SubdeviceFormat into an output
  * stream
  * \param[in] out The output stream
@@ -256,10 +832,10 @@
 {
 	out << f.size << "-";
 
-	const auto it = formatInfoMap.find(f.mbus_code);
+	const auto it = mediaBusFormatInfo.find(f.code);
 
-	if (it == formatInfoMap.end())
-		out << utils::hex(f.mbus_code, 4);
+	if (it == mediaBusFormatInfo.end())
+		out << utils::hex(f.code, 4);
 	else
 		out << it->second.name;
 
@@ -295,30 +871,131 @@
  */
 
 /**
- * \class V4L2Subdevice::Routing
+ * \class V4L2Subdevice::Stream
+ * \brief V4L2 subdevice stream
+ *
+ * This class identifies a subdev stream, by bundling the pad number with the
+ * stream number. It is used in all stream-aware functions of the V4L2Subdevice
+ * class to identify the stream the functions operate on.
+ *
+ * \var V4L2Subdevice::Stream::pad
+ * \brief The 0-indexed pad number
+ *
+ * \var V4L2Subdevice::Stream::stream
+ * \brief The stream number
+ */
+
+/**
+ * \fn V4L2Subdevice::Stream::Stream()
+ * \brief Construct a Stream with pad and stream set to 0
+ */
+
+/**
+ * \fn V4L2Subdevice::Stream::Stream(unsigned int pad, unsigned int stream)
+ * \brief Construct a Stream with a given \a pad and \a stream number
+ * \param[in] pad The indexed pad number
+ * \param[in] stream The stream number
+ */
+
+/**
+ * \brief Compare streams for equality
+ * \return True if the two streams are equal, false otherwise
+ */
+bool operator==(const V4L2Subdevice::Stream &lhs, const V4L2Subdevice::Stream &rhs)
+{
+	return lhs.pad == rhs.pad && lhs.stream == rhs.stream;
+}
+
+/**
+ * \fn bool operator!=(const V4L2Subdevice::Stream &lhs, const V4L2Subdevice::Stream &rhs)
+ * \brief Compare streams for inequality
+ * \return True if the two streams are not equal, false otherwise
+ */
+
+/**
+ * \brief Insert a text representation of a V4L2Subdevice::Stream into an
+ * output stream
+ * \param[in] out The output stream
+ * \param[in] stream The V4L2Subdevice::Stream
+ * \return The output stream \a out
+ */
+std::ostream &operator<<(std::ostream &out, const V4L2Subdevice::Stream &stream)
+{
+	out << stream.pad << "/" << stream.stream;
+
+	return out;
+}
+
+/**
+ * \class V4L2Subdevice::Route
+ * \brief V4L2 subdevice routing table entry
+ *
+ * This class models a route in the subdevice routing table. It is similar to
+ * the v4l2_subdev_route structure, but uses the V4L2Subdevice::Stream class
+ * for easier usage with the V4L2Subdevice stream-aware functions.
+ *
+ * \var V4L2Subdevice::Route::sink
+ * \brief The sink stream of the route
+ *
+ * \var V4L2Subdevice::Route::source
+ * \brief The source stream of the route
+ *
+ * \var V4L2Subdevice::Route::flags
+ * \brief The route flags (V4L2_SUBDEV_ROUTE_FL_*)
+ */
+
+/**
+ * \fn V4L2Subdevice::Route::Route()
+ * \brief Construct a Route with default streams
+ */
+
+/**
+ * \fn V4L2Subdevice::Route::Route(const Stream &sink, const Stream &source,
+ * uint32_t flags)
+ * \brief Construct a Route from \a sink to \a source
+ * \param[in] sink The sink stream
+ * \param[in] source The source stream
+ * \param[in] flags The route flags
+ */
+
+/**
+ * \brief Insert a text representation of a V4L2Subdevice::Route into an
+ * output stream
+ * \param[in] out The output stream
+ * \param[in] route The V4L2Subdevice::Route
+ * \return The output stream \a out
+ */
+std::ostream &operator<<(std::ostream &out, const V4L2Subdevice::Route &route)
+{
+	out << route.sink << " -> " << route.source
+	    << " (" << utils::hex(route.flags) << ")";
+
+	return out;
+}
+
+/**
+ * \typedef V4L2Subdevice::Routing
  * \brief V4L2 subdevice routing table
  *
  * This class stores a subdevice routing table as a vector of routes.
  */
 
 /**
- * \brief Assemble and return a string describing the routing table
- * \return A string describing the routing table
+ * \brief Insert a text representation of a V4L2Subdevice::Routing into an
+ * output stream
+ * \param[in] out The output stream
+ * \param[in] routing The V4L2Subdevice::Routing
+ * \return The output stream \a out
  */
-std::string V4L2Subdevice::Routing::toString() const
+std::ostream &operator<<(std::ostream &out, const V4L2Subdevice::Routing &routing)
 {
-	std::stringstream routing;
-
-	for (const auto &[i, route] : utils::enumerate(*this)) {
-		routing << "[" << i << "] "
-			<< route.sink_pad << "/" << route.sink_stream << " -> "
-			<< route.source_pad << "/" << route.source_stream
-			<< " (" << utils::hex(route.flags) << ")";
-		if (i != size() - 1)
-			routing << ", ";
+	for (const auto &[i, route] : utils::enumerate(routing)) {
+		out << "[" << i << "] " << route;
+		if (i != routing.size() - 1)
+			out << ", ";
 	}
 
-	return routing.str();
+	return out;
 }
 
 /**
@@ -359,6 +1036,21 @@
 		return ret;
 	}
 
+	/* If the subdev supports streams, enable the streams API. */
+	if (caps_.hasStreams()) {
+		struct v4l2_subdev_client_capability clientCaps{};
+		clientCaps.capabilities = V4L2_SUBDEV_CLIENT_CAP_STREAMS;
+
+		ret = ioctl(VIDIOC_SUBDEV_S_CLIENT_CAP, &clientCaps);
+		if (ret < 0) {
+			ret = -errno;
+			LOG(V4L2, Error)
+				<< "Unable to set client capabilities: "
+				<< strerror(-ret);
+			return ret;
+		}
+	}
+
 	return 0;
 }
 
@@ -370,7 +1062,7 @@
 
 /**
  * \brief Get selection rectangle \a rect for \a target
- * \param[in] pad The 0-indexed pad number the rectangle is retrieved from
+ * \param[in] stream The stream the rectangle is retrieved from
  * \param[in] target The selection target defined by the V4L2_SEL_TGT_* flags
  * \param[out] rect The retrieved selection rectangle
  *
@@ -378,13 +1070,14 @@
  *
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Subdevice::getSelection(unsigned int pad, unsigned int target,
+int V4L2Subdevice::getSelection(const Stream &stream, unsigned int target,
 				Rectangle *rect)
 {
 	struct v4l2_subdev_selection sel = {};
 
 	sel.which = V4L2_SUBDEV_FORMAT_ACTIVE;
-	sel.pad = pad;
+	sel.pad = stream.pad;
+	sel.stream = stream.stream;
 	sel.target = target;
 	sel.flags = 0;
 
@@ -392,7 +1085,7 @@
 	if (ret < 0) {
 		LOG(V4L2, Error)
 			<< "Unable to get rectangle " << target << " on pad "
-			<< pad << ": " << strerror(-ret);
+			<< stream << ": " << strerror(-ret);
 		return ret;
 	}
 
@@ -405,8 +1098,19 @@
 }
 
 /**
+ * \fn V4L2Subdevice::getSelection(unsigned int pad, unsigned int target,
+ * Rectangle *rect)
+ * \brief Get selection rectangle \a rect for \a target
+ * \param[in] pad The 0-indexed pad number the rectangle is retrieved from
+ * \param[in] target The selection target defined by the V4L2_SEL_TGT_* flags
+ * \param[out] rect The retrieved selection rectangle
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+
+/**
  * \brief Set selection rectangle \a rect for \a target
- * \param[in] pad The 0-indexed pad number the rectangle is to be applied to
+ * \param[in] stream The stream the rectangle is to be applied to
  * \param[in] target The selection target defined by the V4L2_SEL_TGT_* flags
  * \param[inout] rect The selection rectangle to be applied
  *
@@ -414,13 +1118,14 @@
  *
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Subdevice::setSelection(unsigned int pad, unsigned int target,
+int V4L2Subdevice::setSelection(const Stream &stream, unsigned int target,
 				Rectangle *rect)
 {
 	struct v4l2_subdev_selection sel = {};
 
 	sel.which = V4L2_SUBDEV_FORMAT_ACTIVE;
-	sel.pad = pad;
+	sel.pad = stream.pad;
+	sel.stream = stream.stream;
 	sel.target = target;
 	sel.flags = 0;
 
@@ -433,7 +1138,7 @@
 	if (ret < 0) {
 		LOG(V4L2, Error)
 			<< "Unable to set rectangle " << target << " on pad "
-			<< pad << ": " << strerror(-ret);
+			<< stream << ": " << strerror(-ret);
 		return ret;
 	}
 
@@ -444,26 +1149,40 @@
 
 	return 0;
 }
+
 /**
- * \brief Enumerate all media bus codes and frame sizes on a \a pad
- * \param[in] pad The 0-indexed pad number to enumerate formats on
+ * \fn V4L2Subdevice::setSelection(unsigned int pad, unsigned int target,
+ * Rectangle *rect)
+ * \brief Set selection rectangle \a rect for \a target
+ * \param[in] pad The 0-indexed pad number the rectangle is to be applied to
+ * \param[in] target The selection target defined by the V4L2_SEL_TGT_* flags
+ * \param[inout] rect The selection rectangle to be applied
+ *
+ * \todo Define a V4L2SelectionTarget enum for the selection target
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+
+/**
+ * \brief Enumerate all media bus codes and frame sizes on a \a stream
+ * \param[in] stream The stream to enumerate formats for
  *
  * Enumerate all media bus codes and frame sizes supported by the subdevice on
- * a \a pad.
+ * a \a stream.
  *
  * \return A list of the supported device formats
  */
-V4L2Subdevice::Formats V4L2Subdevice::formats(unsigned int pad)
+V4L2Subdevice::Formats V4L2Subdevice::formats(const Stream &stream)
 {
 	Formats formats;
 
-	if (pad >= entity_->pads().size()) {
-		LOG(V4L2, Error) << "Invalid pad: " << pad;
+	if (stream.pad >= entity_->pads().size()) {
+		LOG(V4L2, Error) << "Invalid pad: " << stream.pad;
 		return {};
 	}
 
-	for (unsigned int code : enumPadCodes(pad)) {
-		std::vector<SizeRange> sizes = enumPadSizes(pad, code);
+	for (unsigned int code : enumPadCodes(stream)) {
+		std::vector<SizeRange> sizes = enumPadSizes(stream, code);
 		if (sizes.empty())
 			return {};
 
@@ -471,7 +1190,7 @@
 		if (!inserted.second) {
 			LOG(V4L2, Error)
 				<< "Could not add sizes for media bus code "
-				<< code << " on pad " << pad;
+				<< code << " on pad " << stream.pad;
 			return {};
 		}
 	}
@@ -479,6 +1198,17 @@
 	return formats;
 }
 
+/**
+ * \fn V4L2Subdevice::formats(unsigned int pad)
+ * \brief Enumerate all media bus codes and frame sizes on a \a pad
+ * \param[in] pad The 0-indexed pad number to enumerate formats on
+ *
+ * Enumerate all media bus codes and frame sizes supported by the subdevice on
+ * a \a pad
+ *
+ * \return A list of the supported device formats
+ */
+
 std::optional<ColorSpace> V4L2Subdevice::toColorSpace(const v4l2_mbus_framefmt &format) const
 {
 	/*
@@ -494,9 +1224,9 @@
 		return std::nullopt;
 
 	PixelFormatInfo::ColourEncoding colourEncoding;
-	auto iter = formatInfoMap.find(format.code);
-	if (iter != formatInfoMap.end()) {
-		colourEncoding = iter->second.colourEncoding;
+	const MediaBusFormatInfo &info = MediaBusFormatInfo::info(format.code);
+	if (info.isValid()) {
+		colourEncoding = info.colourEncoding;
 	} else {
 		LOG(V4L2, Warning)
 			<< "Unknown subdev format "
@@ -510,6 +1240,40 @@
 }
 
 /**
+ * \brief Retrieve the image format set on one of the V4L2 subdevice streams
+ * \param[in] stream The stream the format is to be retrieved from
+ * \param[out] format The image bus format
+ * \param[in] whence The format to get, \ref V4L2Subdevice::ActiveFormat
+ * "ActiveFormat" or \ref V4L2Subdevice::TryFormat "TryFormat"
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Subdevice::getFormat(const Stream &stream, V4L2SubdeviceFormat *format,
+			     Whence whence)
+{
+	struct v4l2_subdev_format subdevFmt = {};
+	subdevFmt.which = whence;
+	subdevFmt.pad = stream.pad;
+	subdevFmt.stream = stream.stream;
+
+	int ret = ioctl(VIDIOC_SUBDEV_G_FMT, &subdevFmt);
+	if (ret) {
+		LOG(V4L2, Error)
+			<< "Unable to get format on pad " << stream << ": "
+			<< strerror(-ret);
+		return ret;
+	}
+
+	format->size.width = subdevFmt.format.width;
+	format->size.height = subdevFmt.format.height;
+	format->code = subdevFmt.format.code;
+	format->colorSpace = toColorSpace(subdevFmt.format);
+
+	return 0;
+}
+
+/**
+ * \fn V4L2Subdevice::getFormat(unsigned int pad, V4L2SubdeviceFormat *format,
+ * Whence whence)
  * \brief Retrieve the image format set on one of the V4L2 subdevice pads
  * \param[in] pad The 0-indexed pad number the format is to be retrieved from
  * \param[out] format The image bus format
@@ -517,30 +1281,57 @@
  * "ActiveFormat" or \ref V4L2Subdevice::TryFormat "TryFormat"
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Subdevice::getFormat(unsigned int pad, V4L2SubdeviceFormat *format,
+
+/**
+ * \brief Set an image format on one of the V4L2 subdevice pads
+ * \param[in] stream The stream the format is to be applied to
+ * \param[inout] format The image bus format to apply to the stream
+ * \param[in] whence The format to set, \ref V4L2Subdevice::ActiveFormat
+ * "ActiveFormat" or \ref V4L2Subdevice::TryFormat "TryFormat"
+ *
+ * Apply the requested image format to the desired stream and return the
+ * actually applied format parameters, as getFormat() would do.
+ *
+ * \return 0 on success or a negative error code otherwise
+ */
+int V4L2Subdevice::setFormat(const Stream &stream, V4L2SubdeviceFormat *format,
 			     Whence whence)
 {
 	struct v4l2_subdev_format subdevFmt = {};
 	subdevFmt.which = whence;
-	subdevFmt.pad = pad;
+	subdevFmt.pad = stream.pad;
+	subdevFmt.stream = stream.stream;
+	subdevFmt.format.width = format->size.width;
+	subdevFmt.format.height = format->size.height;
+	subdevFmt.format.code = format->code;
+	subdevFmt.format.field = V4L2_FIELD_NONE;
+	if (format->colorSpace) {
+		fromColorSpace(format->colorSpace, subdevFmt.format);
 
-	int ret = ioctl(VIDIOC_SUBDEV_G_FMT, &subdevFmt);
+		/* The CSC flag is only applicable to source pads. */
+		if (entity_->pads()[stream.pad]->flags() & MEDIA_PAD_FL_SOURCE)
+			subdevFmt.format.flags |= V4L2_MBUS_FRAMEFMT_SET_CSC;
+	}
+
+	int ret = ioctl(VIDIOC_SUBDEV_S_FMT, &subdevFmt);
 	if (ret) {
 		LOG(V4L2, Error)
-			<< "Unable to get format on pad " << pad
-			<< ": " << strerror(-ret);
+			<< "Unable to set format on pad " << stream << ": "
+			<< strerror(-ret);
 		return ret;
 	}
 
 	format->size.width = subdevFmt.format.width;
 	format->size.height = subdevFmt.format.height;
-	format->mbus_code = subdevFmt.format.code;
+	format->code = subdevFmt.format.code;
 	format->colorSpace = toColorSpace(subdevFmt.format);
 
 	return 0;
 }
 
 /**
+ * \fn V4L2Subdevice::setFormat(unsigned int pad, V4L2SubdeviceFormat *format,
+ * Whence whence)
  * \brief Set an image format on one of the V4L2 subdevice pads
  * \param[in] pad The 0-indexed pad number the format is to be applied to
  * \param[inout] format The image bus format to apply to the subdevice's pad
@@ -552,36 +1343,81 @@
  *
  * \return 0 on success or a negative error code otherwise
  */
-int V4L2Subdevice::setFormat(unsigned int pad, V4L2SubdeviceFormat *format,
-			     Whence whence)
+
+namespace {
+
+void routeFromKernel(V4L2Subdevice::Route &route,
+		     const struct v4l2_subdev_route &kroute)
 {
-	struct v4l2_subdev_format subdevFmt = {};
-	subdevFmt.which = whence;
-	subdevFmt.pad = pad;
-	subdevFmt.format.width = format->size.width;
-	subdevFmt.format.height = format->size.height;
-	subdevFmt.format.code = format->mbus_code;
-	subdevFmt.format.field = V4L2_FIELD_NONE;
-	if (format->colorSpace) {
-		fromColorSpace(format->colorSpace, subdevFmt.format);
+	route.sink.pad = kroute.sink_pad;
+	route.sink.stream = kroute.sink_stream;
+	route.source.pad = kroute.source_pad;
+	route.source.stream = kroute.source_stream;
+	route.flags = kroute.flags;
+}
 
-		/* The CSC flag is only applicable to source pads. */
-		if (entity_->pads()[pad]->flags() & MEDIA_PAD_FL_SOURCE)
-			subdevFmt.format.flags |= V4L2_MBUS_FRAMEFMT_SET_CSC;
-	}
+void routeToKernel(const V4L2Subdevice::Route &route,
+		   struct v4l2_subdev_route &kroute)
+{
+	kroute.sink_pad = route.sink.pad;
+	kroute.sink_stream = route.sink.stream;
+	kroute.source_pad = route.source.pad;
+	kroute.source_stream = route.source.stream;
+	kroute.flags = route.flags;
+}
 
-	int ret = ioctl(VIDIOC_SUBDEV_S_FMT, &subdevFmt);
-	if (ret) {
+/*
+ * Legacy routing support for pre-v6.10-rc1 kernels. Drop when v6.12-rc1 gets
+ * released.
+ */
+struct v4l2_subdev_routing_legacy {
+	__u32 which;
+	__u32 num_routes;
+	__u64 routes;
+	__u32 reserved[6];
+};
+
+#define VIDIOC_SUBDEV_G_ROUTING_LEGACY	_IOWR('V', 38, struct v4l2_subdev_routing_legacy)
+#define VIDIOC_SUBDEV_S_ROUTING_LEGACY	_IOWR('V', 39, struct v4l2_subdev_routing_legacy)
+
+} /* namespace */
+
+int V4L2Subdevice::getRoutingLegacy(Routing *routing, Whence whence)
+{
+	struct v4l2_subdev_routing_legacy rt = {};
+
+	rt.which = whence;
+
+	int ret = ioctl(VIDIOC_SUBDEV_G_ROUTING_LEGACY, &rt);
+	if (ret == 0 || ret == -ENOTTY)
+		return ret;
+
+	if (ret != -ENOSPC) {
 		LOG(V4L2, Error)
-			<< "Unable to set format on pad " << pad
-			<< ": " << strerror(-ret);
+			<< "Failed to retrieve number of routes: "
+			<< strerror(-ret);
 		return ret;
 	}
 
-	format->size.width = subdevFmt.format.width;
-	format->size.height = subdevFmt.format.height;
-	format->mbus_code = subdevFmt.format.code;
-	format->colorSpace = toColorSpace(subdevFmt.format);
+	std::vector<struct v4l2_subdev_route> routes{ rt.num_routes };
+	rt.routes = reinterpret_cast<uintptr_t>(routes.data());
+
+	ret = ioctl(VIDIOC_SUBDEV_G_ROUTING_LEGACY, &rt);
+	if (ret) {
+		LOG(V4L2, Error)
+			<< "Failed to retrieve routes: " << strerror(-ret);
+		return ret;
+	}
+
+	if (rt.num_routes != routes.size()) {
+		LOG(V4L2, Error) << "Invalid number of routes";
+		return -EINVAL;
+	}
+
+	routing->resize(rt.num_routes);
+
+	for (const auto &[i, route] : utils::enumerate(routes))
+		routeFromKernel((*routing)[i], route);
 
 	return 0;
 }
@@ -596,6 +1432,8 @@
  */
 int V4L2Subdevice::getRouting(Routing *routing, Whence whence)
 {
+	routing->clear();
+
 	if (!caps_.hasStreams())
 		return 0;
 
@@ -604,18 +1442,24 @@
 	rt.which = whence;
 
 	int ret = ioctl(VIDIOC_SUBDEV_G_ROUTING, &rt);
-	if (ret == 0 || ret == -ENOTTY)
-		return ret;
+	if (ret == -ENOTTY)
+		return V4L2Subdevice::getRoutingLegacy(routing, whence);
 
-	if (ret != -ENOSPC) {
+	if (ret) {
 		LOG(V4L2, Error)
 			<< "Failed to retrieve number of routes: "
 			<< strerror(-ret);
 		return ret;
 	}
 
-	routing->resize(rt.num_routes);
-	rt.routes = reinterpret_cast<uintptr_t>(routing->data());
+	if (!rt.num_routes)
+		return 0;
+
+	std::vector<struct v4l2_subdev_route> routes{ rt.num_routes };
+	rt.routes = reinterpret_cast<uintptr_t>(routes.data());
+
+	rt.len_routes = rt.num_routes;
+	rt.num_routes = 0;
 
 	ret = ioctl(VIDIOC_SUBDEV_G_ROUTING, &rt);
 	if (ret) {
@@ -624,11 +1468,43 @@
 		return ret;
 	}
 
-	if (rt.num_routes != routing->size()) {
+	if (rt.num_routes != routes.size()) {
 		LOG(V4L2, Error) << "Invalid number of routes";
 		return -EINVAL;
 	}
 
+	routing->resize(rt.num_routes);
+
+	for (const auto &[i, route] : utils::enumerate(routes))
+		routeFromKernel((*routing)[i], route);
+
+	return 0;
+}
+
+int V4L2Subdevice::setRoutingLegacy(Routing *routing, Whence whence)
+{
+	std::vector<struct v4l2_subdev_route> routes{ routing->size() };
+
+	for (const auto &[i, route] : utils::enumerate(*routing))
+		routeToKernel(route, routes[i]);
+
+	struct v4l2_subdev_routing_legacy rt = {};
+	rt.which = whence;
+	rt.num_routes = routes.size();
+	rt.routes = reinterpret_cast<uintptr_t>(routes.data());
+
+	int ret = ioctl(VIDIOC_SUBDEV_S_ROUTING_LEGACY, &rt);
+	if (ret) {
+		LOG(V4L2, Error) << "Failed to set routes: " << strerror(-ret);
+		return ret;
+	}
+
+	routes.resize(rt.num_routes);
+	routing->resize(rt.num_routes);
+
+	for (const auto &[i, route] : utils::enumerate(routes))
+		routeFromKernel((*routing)[i], route);
+
 	return 0;
 }
 
@@ -646,22 +1522,60 @@
  */
 int V4L2Subdevice::setRouting(Routing *routing, Whence whence)
 {
-	if (!caps_.hasStreams())
+	if (!caps_.hasStreams()) {
+		routing->clear();
 		return 0;
+	}
+
+	std::vector<struct v4l2_subdev_route> routes{ routing->size() };
+
+	for (const auto &[i, route] : utils::enumerate(*routing))
+		routeToKernel(route, routes[i]);
 
 	struct v4l2_subdev_routing rt = {};
 	rt.which = whence;
-	rt.num_routes = routing->size();
-	rt.routes = reinterpret_cast<uintptr_t>(routing->data());
+	rt.len_routes = routes.size();
+	rt.num_routes = routes.size();
+	rt.routes = reinterpret_cast<uintptr_t>(routes.data());
 
 	int ret = ioctl(VIDIOC_SUBDEV_S_ROUTING, &rt);
+	if (ret == -ENOTTY)
+		return setRoutingLegacy(routing, whence);
+
 	if (ret) {
 		LOG(V4L2, Error) << "Failed to set routes: " << strerror(-ret);
 		return ret;
 	}
 
+	/*
+	 * The kernel may want to return more routes than we have space for. In
+	 * that event, we must issue a VIDIOC_SUBDEV_G_ROUTING call to retrieve
+	 * the additional routes.
+	 */
+	if (rt.num_routes > routes.size()) {
+		routes.resize(rt.num_routes);
+
+		rt.len_routes = rt.num_routes;
+		rt.num_routes = 0;
+
+		ret = ioctl(VIDIOC_SUBDEV_G_ROUTING, &rt);
+		if (ret) {
+			LOG(V4L2, Error)
+				<< "Failed to retrieve routes: " << strerror(-ret);
+			return ret;
+		}
+	}
+
+	if (rt.num_routes != routes.size()) {
+		LOG(V4L2, Error) << "Invalid number of routes";
+		return -EINVAL;
+	}
+
 	routing->resize(rt.num_routes);
 
+	for (const auto &[i, route] : utils::enumerate(routes))
+		routeFromKernel((*routing)[i], route);
+
 	return 0;
 }
 
@@ -747,14 +1661,15 @@
 	return "'" + entity_->name() + "'";
 }
 
-std::vector<unsigned int> V4L2Subdevice::enumPadCodes(unsigned int pad)
+std::vector<unsigned int> V4L2Subdevice::enumPadCodes(const Stream &stream)
 {
 	std::vector<unsigned int> codes;
 	int ret;
 
 	for (unsigned int index = 0; ; index++) {
 		struct v4l2_subdev_mbus_code_enum mbusEnum = {};
-		mbusEnum.pad = pad;
+		mbusEnum.pad = stream.pad;
+		mbusEnum.stream = stream.stream;
 		mbusEnum.index = index;
 		mbusEnum.which = V4L2_SUBDEV_FORMAT_ACTIVE;
 
@@ -767,7 +1682,7 @@
 
 	if (ret < 0 && ret != -EINVAL) {
 		LOG(V4L2, Error)
-			<< "Unable to enumerate formats on pad " << pad
+			<< "Unable to enumerate formats on pad " << stream
 			<< ": " << strerror(-ret);
 		return {};
 	}
@@ -775,7 +1690,7 @@
 	return codes;
 }
 
-std::vector<SizeRange> V4L2Subdevice::enumPadSizes(unsigned int pad,
+std::vector<SizeRange> V4L2Subdevice::enumPadSizes(const Stream &stream,
 						   unsigned int code)
 {
 	std::vector<SizeRange> sizes;
@@ -784,7 +1699,8 @@
 	for (unsigned int index = 0;; index++) {
 		struct v4l2_subdev_frame_size_enum sizeEnum = {};
 		sizeEnum.index = index;
-		sizeEnum.pad = pad;
+		sizeEnum.pad = stream.pad;
+		sizeEnum.stream = stream.stream;
 		sizeEnum.code = code;
 		sizeEnum.which = V4L2_SUBDEV_FORMAT_ACTIVE;
 
@@ -798,7 +1714,7 @@
 
 	if (ret < 0 && ret != -EINVAL && ret != -ENOTTY) {
 		LOG(V4L2, Error)
-			<< "Unable to enumerate sizes on pad " << pad
+			<< "Unable to enumerate sizes on pad " << stream
 			<< ": " << strerror(-ret);
 		return {};
 	}
diff --git a/src/libcamera/v4l2_videodevice.cpp b/src/libcamera/v4l2_videodevice.cpp
index a72ef64..4947aa3 100644
--- a/src/libcamera/v4l2_videodevice.cpp
+++ b/src/libcamera/v4l2_videodevice.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_videodevice.cpp - V4L2 Video Device
+ * V4L2 Video Device
  */
 
 #include "libcamera/internal/v4l2_videodevice.h"
diff --git a/src/libcamera/version.cpp.in b/src/libcamera/version.cpp.in
index 5aec08a..bf5a2c3 100644
--- a/src/libcamera/version.cpp.in
+++ b/src/libcamera/version.cpp.in
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * version.cpp - libcamera version
+ * libcamera version
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/src/libcamera/yaml_parser.cpp b/src/libcamera/yaml_parser.cpp
index bf21141..025006b 100644
--- a/src/libcamera/yaml_parser.cpp
+++ b/src/libcamera/yaml_parser.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * yaml_parser.cpp - libcamera YAML parsing helper
+ * libcamera YAML parsing helper
  */
 
 #include "libcamera/internal/yaml_parser.h"
@@ -104,7 +104,7 @@
  */
 
 /**
- * \fn template<typename T> YamlObject::get<T>(const T &defaultValue) const
+ * \fn template<typename T, typename U> YamlObject::get<T>(U &&defaultValue) const
  * \brief Parse the YamlObject as a \a T value
  * \param[in] defaultValue The default value when failing to parse
  *
@@ -118,14 +118,15 @@
 #ifndef __DOXYGEN__
 
 template<>
-std::optional<bool> YamlObject::get() const
+std::optional<bool>
+YamlObject::Getter<bool>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::Value)
+	if (obj.type_ != Type::Value)
 		return std::nullopt;
 
-	if (value_ == "true")
+	if (obj.value_ == "true")
 		return true;
-	else if (value_ == "false")
+	else if (obj.value_ == "false")
 		return false;
 
 	return std::nullopt;
@@ -182,14 +183,15 @@
 } /* namespace */
 
 template<>
-std::optional<int8_t> YamlObject::get() const
+std::optional<int8_t>
+YamlObject::Getter<int8_t>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::Value)
+	if (obj.type_ != Type::Value)
 		return std::nullopt;
 
 	long value;
 
-	if (!parseSignedInteger(value_, std::numeric_limits<int8_t>::min(),
+	if (!parseSignedInteger(obj.value_, std::numeric_limits<int8_t>::min(),
 				std::numeric_limits<int8_t>::max(), &value))
 		return std::nullopt;
 
@@ -197,14 +199,15 @@
 }
 
 template<>
-std::optional<uint8_t> YamlObject::get() const
+std::optional<uint8_t>
+YamlObject::Getter<uint8_t>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::Value)
+	if (obj.type_ != Type::Value)
 		return std::nullopt;
 
 	unsigned long value;
 
-	if (!parseUnsignedInteger(value_, std::numeric_limits<uint8_t>::max(),
+	if (!parseUnsignedInteger(obj.value_, std::numeric_limits<uint8_t>::max(),
 				  &value))
 		return std::nullopt;
 
@@ -212,14 +215,15 @@
 }
 
 template<>
-std::optional<int16_t> YamlObject::get() const
+std::optional<int16_t>
+YamlObject::Getter<int16_t>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::Value)
+	if (obj.type_ != Type::Value)
 		return std::nullopt;
 
 	long value;
 
-	if (!parseSignedInteger(value_, std::numeric_limits<int16_t>::min(),
+	if (!parseSignedInteger(obj.value_, std::numeric_limits<int16_t>::min(),
 				std::numeric_limits<int16_t>::max(), &value))
 		return std::nullopt;
 
@@ -227,14 +231,15 @@
 }
 
 template<>
-std::optional<uint16_t> YamlObject::get() const
+std::optional<uint16_t>
+YamlObject::Getter<uint16_t>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::Value)
+	if (obj.type_ != Type::Value)
 		return std::nullopt;
 
 	unsigned long value;
 
-	if (!parseUnsignedInteger(value_, std::numeric_limits<uint16_t>::max(),
+	if (!parseUnsignedInteger(obj.value_, std::numeric_limits<uint16_t>::max(),
 				  &value))
 		return std::nullopt;
 
@@ -242,14 +247,15 @@
 }
 
 template<>
-std::optional<int32_t> YamlObject::get() const
+std::optional<int32_t>
+YamlObject::Getter<int32_t>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::Value)
+	if (obj.type_ != Type::Value)
 		return std::nullopt;
 
 	long value;
 
-	if (!parseSignedInteger(value_, std::numeric_limits<int32_t>::min(),
+	if (!parseSignedInteger(obj.value_, std::numeric_limits<int32_t>::min(),
 				std::numeric_limits<int32_t>::max(), &value))
 		return std::nullopt;
 
@@ -257,14 +263,15 @@
 }
 
 template<>
-std::optional<uint32_t> YamlObject::get() const
+std::optional<uint32_t>
+YamlObject::Getter<uint32_t>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::Value)
+	if (obj.type_ != Type::Value)
 		return std::nullopt;
 
 	unsigned long value;
 
-	if (!parseUnsignedInteger(value_, std::numeric_limits<uint32_t>::max(),
+	if (!parseUnsignedInteger(obj.value_, std::numeric_limits<uint32_t>::max(),
 				  &value))
 		return std::nullopt;
 
@@ -272,18 +279,26 @@
 }
 
 template<>
-std::optional<double> YamlObject::get() const
+std::optional<float>
+YamlObject::Getter<float>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::Value)
+	return obj.get<double>();
+}
+
+template<>
+std::optional<double>
+YamlObject::Getter<double>::get(const YamlObject &obj) const
+{
+	if (obj.type_ != Type::Value)
 		return std::nullopt;
 
-	if (value_ == "")
+	if (obj.value_ == "")
 		return std::nullopt;
 
 	char *end;
 
 	errno = 0;
-	double value = utils::strtod(value_.c_str(), &end);
+	double value = utils::strtod(obj.value_.c_str(), &end);
 
 	if ('\0' != *end || errno == ERANGE)
 		return std::nullopt;
@@ -292,28 +307,30 @@
 }
 
 template<>
-std::optional<std::string> YamlObject::get() const
+std::optional<std::string>
+YamlObject::Getter<std::string>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::Value)
+	if (obj.type_ != Type::Value)
 		return std::nullopt;
 
-	return value_;
+	return obj.value_;
 }
 
 template<>
-std::optional<Size> YamlObject::get() const
+std::optional<Size>
+YamlObject::Getter<Size>::get(const YamlObject &obj) const
 {
-	if (type_ != Type::List)
+	if (obj.type_ != Type::List)
 		return std::nullopt;
 
-	if (list_.size() != 2)
+	if (obj.list_.size() != 2)
 		return std::nullopt;
 
-	auto width = list_[0].value->get<uint32_t>();
+	auto width = obj.list_[0].value->get<uint32_t>();
 	if (!width)
 		return std::nullopt;
 
-	auto height = list_[1].value->get<uint32_t>();
+	auto height = obj.list_[1].value->get<uint32_t>();
 	if (!height)
 		return std::nullopt;
 
@@ -339,6 +356,7 @@
 template<typename T,
 	 std::enable_if_t<
 		 std::is_same_v<bool, T> ||
+		 std::is_same_v<float, T> ||
 		 std::is_same_v<double, T> ||
 		 std::is_same_v<int8_t, T> ||
 		 std::is_same_v<uint8_t, T> ||
@@ -367,6 +385,7 @@
 }
 
 template std::optional<std::vector<bool>> YamlObject::getList<bool>() const;
+template std::optional<std::vector<float>> YamlObject::getList<float>() const;
 template std::optional<std::vector<double>> YamlObject::getList<double>() const;
 template std::optional<std::vector<int8_t>> YamlObject::getList<int8_t>() const;
 template std::optional<std::vector<uint8_t>> YamlObject::getList<uint8_t>() const;
@@ -468,10 +487,13 @@
  */
 const YamlObject &YamlObject::operator[](const std::string &key) const
 {
-	if (type_ != Type::Dictionary || !contains(key))
+	if (type_ != Type::Dictionary)
 		return empty;
 
 	auto iter = dictionary_.find(key);
+	if (iter == dictionary_.end())
+		return empty;
+
 	return *iter->second;
 }
 
diff --git a/src/py/cam/cam.py b/src/py/cam/cam.py
index a2a115c..ff4b7f6 100755
--- a/src/py/cam/cam.py
+++ b/src/py/cam/cam.py
@@ -23,6 +23,7 @@
     opt_metadata: bool
     opt_save_frames: bool
     opt_capture: int
+    opt_orientation: str
 
     stream_names: dict[libcam.Stream, str]
     streams: list[libcam.Stream]
@@ -146,6 +147,21 @@
             if 'pixelformat' in stream_opts:
                 stream_config.pixel_format = libcam.PixelFormat(stream_opts['pixelformat'])
 
+        if self.opt_orientation is not None:
+            orientation_map = {
+                'rot0': libcam.Orientation.Rotate0,
+                'rot180': libcam.Orientation.Rotate180,
+                'mirror': libcam.Orientation.Rotate0Mirror,
+                'flip': libcam.Orientation.Rotate180Mirror,
+            }
+
+            orient = orientation_map.get(self.opt_orientation, None)
+            if orient is None:
+                print('Bad orientation: ', self.opt_orientation)
+                sys.exit(-1)
+
+            camconfig.orientation = orient
+
         stat = camconfig.validate()
 
         if stat == libcam.CameraConfiguration.Status.Invalid:
@@ -385,6 +401,7 @@
     parser.add_argument('--metadata', nargs=0, type=bool, action=CustomAction, help='Print the metadata for completed requests')
     parser.add_argument('--strict-formats', type=bool, nargs=0, action=CustomAction, help='Do not allow requested stream format(s) to be adjusted')
     parser.add_argument('-s', '--stream', nargs='+', action=CustomAction)
+    parser.add_argument('-o', '--orientation', help='Desired image orientation (rot0, rot180, mirror, flip)')
     args = parser.parse_args()
 
     cm = libcam.CameraManager.singleton()
@@ -408,6 +425,7 @@
         ctx.opt_metadata = args.metadata.get(cam_idx, False)
         ctx.opt_strict_formats = args.strict_formats.get(cam_idx, False)
         ctx.opt_stream = args.stream.get(cam_idx, ['role=viewfinder'])
+        ctx.opt_orientation = args.orientation
         contexts.append(ctx)
 
     for ctx in contexts:
diff --git a/src/py/libcamera/gen-py-controls.py b/src/py/libcamera/gen-py-controls.py
index 99f3bbc..8efbf95 100755
--- a/src/py/libcamera/gen-py-controls.py
+++ b/src/py/libcamera/gen-py-controls.py
@@ -24,48 +24,57 @@
 def generate_py(controls, mode):
     out = ''
 
-    for ctrl in controls:
-        name, ctrl = ctrl.popitem()
+    vendors_class_def = []
+    vendor_defs = []
+    vendors = []
+    for vendor, ctrl_list in controls.items():
+        for ctrls in ctrl_list:
+            name, ctrl = ctrls.popitem()
 
-        if ctrl.get('draft'):
-            ns = 'libcamera::{}::draft::'.format(mode)
-            container = 'draft'
-        else:
-            ns = 'libcamera::{}::'.format(mode)
-            container = 'controls'
+            if vendor not in vendors and vendor != 'libcamera':
+                vendor_mode_str = f'{vendor.capitalize()}{mode.capitalize()}'
+                vendors_class_def.append('class Py{}\n{{\n}};\n'.format(vendor_mode_str))
+                vendor_defs.append('\tauto {} = py::class_<Py{}>(controls, \"{}\");'.format(vendor, vendor_mode_str, vendor))
+                vendors.append(vendor)
 
-        out += f'\t{container}.def_readonly_static("{name}", static_cast<const libcamera::ControlId *>(&{ns}{name}));\n\n'
-
-        enum = ctrl.get('enum')
-        if not enum:
-            continue
-
-        cpp_enum = name + 'Enum'
-
-        out += '\tpy::enum_<{}{}>({}, \"{}\")\n'.format(ns, cpp_enum, container, cpp_enum)
-
-        if mode == 'controls':
-            # Adjustments for controls
-            if name == 'LensShadingMapMode':
-                prefix = 'LensShadingMapMode'
-            elif name == 'SceneFlicker':
-                # If we strip the prefix, we would get '50Hz', which is illegal name
-                prefix = ''
+            if vendor != 'libcamera':
+                ns = 'libcamera::{}::{}::'.format(mode, vendor)
+                container = vendor
             else:
+                ns = 'libcamera::{}::'.format(mode)
+                container = 'controls'
+
+            out += f'\t{container}.def_readonly_static("{name}", static_cast<const libcamera::ControlId *>(&{ns}{name}));\n\n'
+
+            enum = ctrl.get('enum')
+            if not enum:
+                continue
+
+            cpp_enum = name + 'Enum'
+
+            out += '\tpy::enum_<{}{}>({}, \"{}\")\n'.format(ns, cpp_enum, container, cpp_enum)
+
+            if mode == 'controls':
+                # Adjustments for controls
+                if name == 'LensShadingMapMode':
+                    prefix = 'LensShadingMapMode'
+                else:
+                    prefix = find_common_prefix([e['name'] for e in enum])
+            else:
+                # Adjustments for properties
                 prefix = find_common_prefix([e['name'] for e in enum])
-        else:
-            # Adjustments for properties
-            prefix = find_common_prefix([e['name'] for e in enum])
 
-        for entry in enum:
-            cpp_enum = entry['name']
-            py_enum = entry['name'][len(prefix):]
+            for entry in enum:
+                cpp_enum = entry['name']
+                py_enum = entry['name'][len(prefix):]
 
-            out += '\t\t.value(\"{}\", {}{})\n'.format(py_enum, ns, cpp_enum)
+                out += '\t\t.value(\"{}\", {}{})\n'.format(py_enum, ns, cpp_enum)
 
-        out += '\t;\n\n'
+            out += '\t;\n\n'
 
-    return {'controls': out}
+    return {'controls': out,
+            'vendors_class_def': '\n'.join(vendors_class_def),
+            'vendors_defs': '\n'.join(vendor_defs)}
 
 
 def fill_template(template, data):
@@ -78,22 +87,25 @@
 def main(argv):
     # Parse command line arguments
     parser = argparse.ArgumentParser()
-    parser.add_argument('-o', dest='output', metavar='file', type=str,
-                        help='Output file name. Defaults to standard output if not specified.')
-    parser.add_argument('input', type=str,
-                        help='Input file name.')
-    parser.add_argument('template', type=str,
-                        help='Template file name.')
-    parser.add_argument('--mode', type=str, required=True,
+    parser.add_argument('--mode', '-m', type=str, required=True,
                         help='Mode is either "controls" or "properties"')
+    parser.add_argument('--output', '-o', metavar='file', type=str,
+                        help='Output file name. Defaults to standard output if not specified.')
+    parser.add_argument('--template', '-t', type=str, required=True,
+                        help='Template file name.')
+    parser.add_argument('input', type=str, nargs='+',
+                        help='Input file name.')
     args = parser.parse_args(argv[1:])
 
     if args.mode not in ['controls', 'properties']:
         print(f'Invalid mode option "{args.mode}"', file=sys.stderr)
         return -1
 
-    data = open(args.input, 'rb').read()
-    controls = yaml.safe_load(data)['controls']
+    controls = {}
+    for input in args.input:
+        data = open(input, 'rb').read()
+        vendor = yaml.safe_load(data)['vendor']
+        controls[vendor] = yaml.safe_load(data)['controls']
 
     data = generate_py(controls, args.mode)
 
diff --git a/src/py/libcamera/meson.build b/src/py/libcamera/meson.build
index f58c719..4807ca7 100644
--- a/src/py/libcamera/meson.build
+++ b/src/py/libcamera/meson.build
@@ -28,29 +28,35 @@
 
 # Generate controls
 
-gen_py_controls_input_files = files([
-    '../../libcamera/control_ids.yaml',
-    'py_controls_generated.cpp.in',
-])
+gen_py_controls_input_files = []
+gen_py_controls_template = files('py_controls_generated.cpp.in')
 
 gen_py_controls = files('gen-py-controls.py')
 
+foreach file : controls_files
+    gen_py_controls_input_files += files('../../libcamera/' + file)
+endforeach
+
 pycamera_sources += custom_target('py_gen_controls',
                                   input : gen_py_controls_input_files,
                                   output : ['py_controls_generated.cpp'],
-                                  command : [gen_py_controls, '--mode', 'controls', '-o', '@OUTPUT@', '@INPUT@'])
+                                  command : [gen_py_controls, '--mode', 'controls', '-o', '@OUTPUT@',
+                                             '-t', gen_py_controls_template, '@INPUT@'])
 
 # Generate properties
 
-gen_py_property_enums_input_files = files([
-    '../../libcamera/property_ids.yaml',
-    'py_properties_generated.cpp.in',
-])
+gen_py_property_enums_input_files = []
+gen_py_properties_template = files('py_properties_generated.cpp.in')
+
+foreach file : properties_files
+    gen_py_property_enums_input_files += files('../../libcamera/' + file)
+endforeach
 
 pycamera_sources += custom_target('py_gen_properties',
                                   input : gen_py_property_enums_input_files,
                                   output : ['py_properties_generated.cpp'],
-                                  command : [gen_py_controls, '--mode', 'properties', '-o', '@OUTPUT@', '@INPUT@'])
+                                  command : [gen_py_controls, '--mode', 'properties', '-o', '@OUTPUT@',
+                                             '-t', gen_py_properties_template, '@INPUT@'])
 
 # Generate formats
 
@@ -84,6 +90,7 @@
                          pycamera_sources,
                          install : true,
                          install_dir : destdir,
+                         install_tag : 'python-runtime',
                          name_prefix : '',
                          dependencies : pycamera_deps,
                          cpp_args : pycamera_args)
@@ -99,7 +106,9 @@
             meson.current_build_dir() / 'utils',
             check : true)
 
-install_data(['__init__.py'], install_dir : destdir)
+install_data(['__init__.py'],
+             install_dir : destdir,
+             install_tag : 'python-runtime')
 
 # \todo Generate stubs when building. See https://peps.python.org/pep-0484/#stub-files
 # Note: Depends on pybind11-stubgen. To generate pylibcamera stubs:
diff --git a/src/py/libcamera/py_color_space.cpp b/src/py/libcamera/py_color_space.cpp
index 5201121..fd5a5da 100644
--- a/src/py/libcamera/py_color_space.cpp
+++ b/src/py/libcamera/py_color_space.cpp
@@ -12,6 +12,8 @@
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 
+#include "py_main.h"
+
 namespace py = pybind11;
 
 using namespace libcamera;
diff --git a/src/py/libcamera/py_controls_generated.cpp.in b/src/py/libcamera/py_controls_generated.cpp.in
index 18fa57d..26d5a10 100644
--- a/src/py/libcamera/py_controls_generated.cpp.in
+++ b/src/py/libcamera/py_controls_generated.cpp.in
@@ -11,20 +11,20 @@
 
 #include <pybind11/pybind11.h>
 
+#include "py_main.h"
+
 namespace py = pybind11;
 
 class PyControls
 {
 };
 
-class PyDraftControls
-{
-};
+${vendors_class_def}
 
 void init_py_controls_generated(py::module& m)
 {
 	auto controls = py::class_<PyControls>(m, "controls");
-	auto draft = py::class_<PyDraftControls>(controls, "draft");
+${vendors_defs}
 
 ${controls}
 }
diff --git a/src/py/libcamera/py_enums.cpp b/src/py/libcamera/py_enums.cpp
index 803c4e7..ca6aeb8 100644
--- a/src/py/libcamera/py_enums.cpp
+++ b/src/py/libcamera/py_enums.cpp
@@ -9,6 +9,8 @@
 
 #include <pybind11/pybind11.h>
 
+#include "py_main.h"
+
 namespace py = pybind11;
 
 using namespace libcamera;
@@ -31,4 +33,14 @@
 		.value("String", ControlType::ControlTypeString)
 		.value("Rectangle", ControlType::ControlTypeRectangle)
 		.value("Size", ControlType::ControlTypeSize);
+
+	py::enum_<Orientation>(m, "Orientation")
+		.value("Rotate0", Orientation::Rotate0)
+		.value("Rotate0Mirror", Orientation::Rotate0Mirror)
+		.value("Rotate180", Orientation::Rotate180)
+		.value("Rotate180Mirror", Orientation::Rotate180Mirror)
+		.value("Rotate90Mirror", Orientation::Rotate90Mirror)
+		.value("Rotate270", Orientation::Rotate270)
+		.value("Rotate270Mirror", Orientation::Rotate270Mirror)
+		.value("Rotate90", Orientation::Rotate90);
 }
diff --git a/src/py/libcamera/py_formats_generated.cpp.in b/src/py/libcamera/py_formats_generated.cpp.in
index a3f7f94..c5fb906 100644
--- a/src/py/libcamera/py_formats_generated.cpp.in
+++ b/src/py/libcamera/py_formats_generated.cpp.in
@@ -11,6 +11,8 @@
 
 #include <pybind11/pybind11.h>
 
+#include "py_main.h"
+
 namespace py = pybind11;
 
 class PyFormats
diff --git a/src/py/libcamera/py_geometry.cpp b/src/py/libcamera/py_geometry.cpp
index 5c2aeac..c7e3036 100644
--- a/src/py/libcamera/py_geometry.cpp
+++ b/src/py/libcamera/py_geometry.cpp
@@ -14,6 +14,8 @@
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 
+#include "py_main.h"
+
 namespace py = pybind11;
 
 using namespace libcamera;
diff --git a/src/py/libcamera/py_main.cpp b/src/py/libcamera/py_main.cpp
index 01fb15a..ab33f38 100644
--- a/src/py/libcamera/py_main.cpp
+++ b/src/py/libcamera/py_main.cpp
@@ -85,14 +85,6 @@
  */
 static std::weak_ptr<PyCameraManager> gCameraManager;
 
-void init_py_color_space(py::module &m);
-void init_py_controls_generated(py::module &m);
-void init_py_enums(py::module &m);
-void init_py_formats_generated(py::module &m);
-void init_py_geometry(py::module &m);
-void init_py_properties_generated(py::module &m);
-void init_py_transform(py::module &m);
-
 PYBIND11_MODULE(_libcamera, m)
 {
 	init_py_enums(m);
@@ -112,6 +104,7 @@
 
 	auto pyCameraManager = py::class_<PyCameraManager, std::shared_ptr<PyCameraManager>>(m, "CameraManager");
 	auto pyCamera = py::class_<Camera, PyCameraSmartPtr<Camera>>(m, "Camera");
+	auto pySensorConfiguration = py::class_<SensorConfiguration>(m, "SensorConfiguration");
 	auto pyCameraConfiguration = py::class_<CameraConfiguration>(m, "CameraConfiguration");
 	auto pyCameraConfigurationStatus = py::enum_<CameraConfiguration::Status>(pyCameraConfiguration, "Status");
 	auto pyStreamConfiguration = py::class_<StreamConfiguration>(m, "StreamConfiguration");
@@ -281,6 +274,40 @@
 			return ret;
 		});
 
+	pySensorConfiguration
+		.def(py::init<>())
+		.def_readwrite("bit_depth", &SensorConfiguration::bitDepth)
+		.def_readwrite("analog_crop", &SensorConfiguration::analogCrop)
+		.def_property(
+			"binning",
+			[](SensorConfiguration &self) {
+				return py::make_tuple(self.binning.binX, self.binning.binY);
+			},
+			[](SensorConfiguration &self, py::object value) {
+				auto vec = value.cast<std::vector<unsigned int>>();
+				if (vec.size() != 2)
+					throw std::runtime_error("binning requires iterable of 2 values");
+				self.binning.binX = vec[0];
+				self.binning.binY = vec[1];
+			})
+		.def_property(
+			"skipping",
+			[](SensorConfiguration &self) {
+				return py::make_tuple(self.skipping.xOddInc, self.skipping.xEvenInc,
+						      self.skipping.yOddInc, self.skipping.yEvenInc);
+			},
+			[](SensorConfiguration &self, py::object value) {
+				auto vec = value.cast<std::vector<unsigned int>>();
+				if (vec.size() != 4)
+					throw std::runtime_error("skipping requires iterable of 4 values");
+				self.skipping.xOddInc = vec[0];
+				self.skipping.xEvenInc = vec[1];
+				self.skipping.yOddInc = vec[2];
+				self.skipping.yEvenInc = vec[3];
+			})
+		.def_readwrite("output_size", &SensorConfiguration::outputSize)
+		.def("is_valid", &SensorConfiguration::isValid);
+
 	pyCameraConfiguration
 		.def("__iter__", [](CameraConfiguration &self) {
 			return py::make_iterator<py::return_value_policy::reference_internal>(self);
@@ -293,7 +320,8 @@
 		     py::return_value_policy::reference_internal)
 		.def_property_readonly("size", &CameraConfiguration::size)
 		.def_property_readonly("empty", &CameraConfiguration::empty)
-		.def_readwrite("transform", &CameraConfiguration::transform);
+		.def_readwrite("sensor_config", &CameraConfiguration::sensorConfig)
+		.def_readwrite("orientation", &CameraConfiguration::orientation);
 
 	pyCameraConfigurationStatus
 		.value("Valid", CameraConfiguration::Valid)
diff --git a/src/py/libcamera/py_main.h b/src/py/libcamera/py_main.h
index 5bb5f2d..4d59432 100644
--- a/src/py/libcamera/py_main.h
+++ b/src/py/libcamera/py_main.h
@@ -7,8 +7,18 @@
 
 #include <libcamera/base/log.h>
 
+#include <pybind11/pybind11.h>
+
 namespace libcamera {
 
 LOG_DECLARE_CATEGORY(Python)
 
 }
+
+void init_py_color_space(pybind11::module &m);
+void init_py_controls_generated(pybind11::module &m);
+void init_py_enums(pybind11::module &m);
+void init_py_formats_generated(pybind11::module &m);
+void init_py_geometry(pybind11::module &m);
+void init_py_properties_generated(pybind11::module &m);
+void init_py_transform(pybind11::module &m);
diff --git a/src/py/libcamera/py_properties_generated.cpp.in b/src/py/libcamera/py_properties_generated.cpp.in
index e49b6e9..d28f1ab 100644
--- a/src/py/libcamera/py_properties_generated.cpp.in
+++ b/src/py/libcamera/py_properties_generated.cpp.in
@@ -11,20 +11,20 @@
 
 #include <pybind11/pybind11.h>
 
+#include "py_main.h"
+
 namespace py = pybind11;
 
 class PyProperties
 {
 };
 
-class PyDraftProperties
-{
-};
+${vendors_class_def}
 
 void init_py_properties_generated(py::module& m)
 {
 	auto controls = py::class_<PyProperties>(m, "properties");
-	auto draft = py::class_<PyDraftProperties>(controls, "draft");
+${vendors_defs}
 
 ${controls}
 }
diff --git a/src/py/libcamera/py_transform.cpp b/src/py/libcamera/py_transform.cpp
index f3a0bfa..768260f 100644
--- a/src/py/libcamera/py_transform.cpp
+++ b/src/py/libcamera/py_transform.cpp
@@ -12,6 +12,8 @@
 #include <pybind11/pybind11.h>
 #include <pybind11/stl.h>
 
+#include "py_main.h"
+
 namespace py = pybind11;
 
 using namespace libcamera;
diff --git a/src/v4l2/meson.build b/src/v4l2/meson.build
index ab4b35d..58f53bf 100644
--- a/src/v4l2/meson.build
+++ b/src/v4l2/meson.build
@@ -24,6 +24,7 @@
     '-U_FILE_OFFSET_BITS',
     '-D_FILE_OFFSET_BITS=32',
     '-D_LARGEFILE64_SOURCE',
+    '-U_TIME_BITS',
     '-fvisibility=hidden',
 ]
 
@@ -44,4 +45,5 @@
 configure_file(input : 'libcamerify.in',
                output : 'libcamerify',
                configuration : cdata,
-               install_dir : get_option('bindir'))
+               install_dir : get_option('bindir'),
+               install_tag : 'bin')
diff --git a/src/v4l2/v4l2_camera.cpp b/src/v4l2/v4l2_camera.cpp
index 7b97c2d..0f3b862 100644
--- a/src/v4l2/v4l2_camera.cpp
+++ b/src/v4l2/v4l2_camera.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_camera.cpp - V4L2 compatibility camera
+ * V4L2 compatibility camera
  */
 
 #include "v4l2_camera.h"
diff --git a/src/v4l2/v4l2_camera.h b/src/v4l2/v4l2_camera.h
index d348344..278cc33 100644
--- a/src/v4l2/v4l2_camera.h
+++ b/src/v4l2/v4l2_camera.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_camera.h - V4L2 compatibility camera
+ * V4L2 compatibility camera
  */
 
 #pragma once
diff --git a/src/v4l2/v4l2_camera_file.cpp b/src/v4l2/v4l2_camera_file.cpp
index 0a41587..d8fe854 100644
--- a/src/v4l2/v4l2_camera_file.cpp
+++ b/src/v4l2/v4l2_camera_file.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * v4l2_camera_file.h - V4L2 compatibility camera file information
+ * V4L2 compatibility camera file information
  */
 
 #include "v4l2_camera_file.h"
diff --git a/src/v4l2/v4l2_camera_file.h b/src/v4l2/v4l2_camera_file.h
index 1a7b6a6..1212989 100644
--- a/src/v4l2/v4l2_camera_file.h
+++ b/src/v4l2/v4l2_camera_file.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * v4l2_camera_file.h - V4L2 compatibility camera file information
+ * V4L2 compatibility camera file information
  */
 
 #pragma once
diff --git a/src/v4l2/v4l2_camera_proxy.cpp b/src/v4l2/v4l2_camera_proxy.cpp
index 341f790..3f7c00a 100644
--- a/src/v4l2/v4l2_camera_proxy.cpp
+++ b/src/v4l2/v4l2_camera_proxy.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_camera_proxy.cpp - Proxy to V4L2 compatibility camera
+ * Proxy to V4L2 compatibility camera
  */
 
 #include "v4l2_camera_proxy.h"
diff --git a/src/v4l2/v4l2_camera_proxy.h b/src/v4l2/v4l2_camera_proxy.h
index 8a0195e..3d8784d 100644
--- a/src/v4l2/v4l2_camera_proxy.h
+++ b/src/v4l2/v4l2_camera_proxy.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_camera_proxy.h - Proxy to V4L2 compatibility camera
+ * Proxy to V4L2 compatibility camera
  */
 
 #pragma once
diff --git a/src/v4l2/v4l2_compat.cpp b/src/v4l2/v4l2_compat.cpp
index 1765fb5..66468bf 100644
--- a/src/v4l2/v4l2_compat.cpp
+++ b/src/v4l2/v4l2_compat.cpp
@@ -2,17 +2,20 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_compat.cpp - V4L2 compatibility layer
+ * V4L2 compatibility layer
  */
 
 #include "v4l2_compat_manager.h"
 
+#include <assert.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <stdarg.h>
+#include <sys/ioctl.h>
 #include <sys/mman.h>
 #include <sys/stat.h>
 #include <sys/types.h>
+#include <unistd.h>
 
 #include <libcamera/base/utils.h>
 
@@ -28,71 +31,97 @@
 	va_end(ap);			\
 }
 
+namespace {
+
+/*
+ * Determine if the flags require a further mode arguments that needs to be
+ * parsed from va_args.
+ */
+bool needs_mode(int flags)
+{
+	return (flags & O_CREAT) || ((flags & O_TMPFILE) == O_TMPFILE);
+}
+
+} /* namespace */
+
 extern "C" {
 
 LIBCAMERA_PUBLIC int open(const char *path, int oflag, ...)
 {
 	mode_t mode = 0;
-	if (oflag & O_CREAT || oflag & O_TMPFILE)
+	if (needs_mode(oflag))
 		extract_va_arg(mode_t, mode, oflag);
 
 	return V4L2CompatManager::instance()->openat(AT_FDCWD, path,
 						     oflag, mode);
 }
 
-/* _FORTIFY_SOURCE redirects open to __open_2 */
-LIBCAMERA_PUBLIC int __open_2(const char *path, int oflag)
-{
-	return open(path, oflag);
-}
-
 #ifndef open64
 LIBCAMERA_PUBLIC int open64(const char *path, int oflag, ...)
 {
 	mode_t mode = 0;
-	if (oflag & O_CREAT || oflag & O_TMPFILE)
+	if (needs_mode(oflag))
 		extract_va_arg(mode_t, mode, oflag);
 
 	return V4L2CompatManager::instance()->openat(AT_FDCWD, path,
 						     oflag | O_LARGEFILE, mode);
 }
-
-LIBCAMERA_PUBLIC int __open64_2(const char *path, int oflag)
-{
-	return open(path, oflag);
-}
 #endif
 
 LIBCAMERA_PUBLIC int openat(int dirfd, const char *path, int oflag, ...)
 {
 	mode_t mode = 0;
-	if (oflag & O_CREAT || oflag & O_TMPFILE)
+	if (needs_mode(oflag))
 		extract_va_arg(mode_t, mode, oflag);
 
 	return V4L2CompatManager::instance()->openat(dirfd, path, oflag, mode);
 }
 
-LIBCAMERA_PUBLIC int __openat_2(int dirfd, const char *path, int oflag)
-{
-	return openat(dirfd, path, oflag);
-}
-
 #ifndef openat64
 LIBCAMERA_PUBLIC int openat64(int dirfd, const char *path, int oflag, ...)
 {
 	mode_t mode = 0;
-	if (oflag & O_CREAT || oflag & O_TMPFILE)
+	if (needs_mode(oflag))
 		extract_va_arg(mode_t, mode, oflag);
 
 	return V4L2CompatManager::instance()->openat(dirfd, path,
 						     oflag | O_LARGEFILE, mode);
 }
+#endif
+
+/*
+ * _FORTIFY_SOURCE redirects open* to __open*_2. Disable the
+ * -Wmissing-declarations warnings, as the functions won't be declared if
+ *  _FORTIFY_SOURCE is not in use.
+ */
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wmissing-declarations"
+
+LIBCAMERA_PUBLIC int __open_2(const char *path, int oflag)
+{
+	assert(!needs_mode(oflag));
+	return open(path, oflag);
+}
+
+LIBCAMERA_PUBLIC int __open64_2(const char *path, int oflag)
+{
+	assert(!needs_mode(oflag));
+	return open64(path, oflag);
+}
+
+LIBCAMERA_PUBLIC int __openat_2(int dirfd, const char *path, int oflag)
+{
+	assert(!needs_mode(oflag));
+	return openat(dirfd, path, oflag);
+}
 
 LIBCAMERA_PUBLIC int __openat64_2(int dirfd, const char *path, int oflag)
 {
-	return openat(dirfd, path, oflag);
+	assert(!needs_mode(oflag));
+	return openat64(dirfd, path, oflag);
 }
-#endif
+
+#pragma GCC diagnostic pop
 
 LIBCAMERA_PUBLIC int dup(int oldfd)
 {
diff --git a/src/v4l2/v4l2_compat_manager.cpp b/src/v4l2/v4l2_compat_manager.cpp
index 5e8cdb4..6a00afb 100644
--- a/src/v4l2/v4l2_compat_manager.cpp
+++ b/src/v4l2/v4l2_compat_manager.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_compat_manager.cpp - V4L2 compatibility manager
+ * V4L2 compatibility manager
  */
 
 #include "v4l2_compat_manager.h"
diff --git a/src/v4l2/v4l2_compat_manager.h b/src/v4l2/v4l2_compat_manager.h
index 64af9a8..f7c6f12 100644
--- a/src/v4l2/v4l2_compat_manager.h
+++ b/src/v4l2/v4l2_compat_manager.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_compat_manager.h - V4L2 compatibility manager
+ * V4L2 compatibility manager
  */
 
 #pragma once
diff --git a/test/bayer-format.cpp b/test/bayer-format.cpp
index 54f0348..f8d1980 100644
--- a/test/bayer-format.cpp
+++ b/test/bayer-format.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Sebastian Fricke
  *
- * bayer_format.cpp - BayerFormat class tests
+ * BayerFormat class tests
  */
 
 #include <iostream>
diff --git a/test/byte-stream-buffer.cpp b/test/byte-stream-buffer.cpp
index 04ff057..04aab3d 100644
--- a/test/byte-stream-buffer.cpp
+++ b/test/byte-stream-buffer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * byte_stream_buffer.cpp - ByteStreamBuffer tests
+ * ByteStreamBuffer tests
  */
 
 #include <array>
diff --git a/test/camera-sensor.cpp b/test/camera-sensor.cpp
index 2a17cc7..1d402c4 100644
--- a/test/camera-sensor.cpp
+++ b/test/camera-sensor.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera-sensor.cpp - Camera sensor tests
+ * Camera sensor tests
  */
 
 #include <algorithm>
@@ -100,7 +100,7 @@
 								  MEDIA_BUS_FMT_SBGGR10_1X10,
 								  MEDIA_BUS_FMT_BGR888_1X24 },
 								Size(1024, 768));
-		if (format.mbus_code != MEDIA_BUS_FMT_SBGGR10_1X10 ||
+		if (format.code != MEDIA_BUS_FMT_SBGGR10_1X10 ||
 		    format.size != Size(4096, 2160)) {
 			cerr << "Failed to get a suitable format, expected 4096x2160-0x"
 			     << utils::hex(MEDIA_BUS_FMT_SBGGR10_1X10)
diff --git a/test/camera/buffer_import.cpp b/test/camera/buffer_import.cpp
index 9288400..815d1ca 100644
--- a/test/camera/buffer_import.cpp
+++ b/test/camera/buffer_import.cpp
@@ -63,6 +63,8 @@
 		request->reuse();
 		request->addBuffer(stream, buffer);
 		camera_->queueRequest(request);
+
+		dispatcher_->interrupt();
 	}
 
 	int init() override
@@ -76,6 +78,8 @@
 			return TestFail;
 		}
 
+		dispatcher_ = Thread::current()->eventDispatcher();
+
 		return TestPass;
 	}
 
@@ -133,17 +137,20 @@
 			}
 		}
 
-		EventDispatcher *dispatcher = Thread::current()->eventDispatcher();
+		const unsigned int nFrames = cfg.bufferCount * 2;
 
 		Timer timer;
-		timer.start(1000ms);
-		while (timer.isRunning())
-			dispatcher->processEvents();
+		timer.start(500ms * nFrames);
+		while (timer.isRunning()) {
+			dispatcher_->processEvents();
+			if (completeRequestsCount_ > nFrames)
+				break;
+		}
 
-		if (completeRequestsCount_ < cfg.bufferCount * 2) {
+		if (completeRequestsCount_ < nFrames) {
 			std::cout << "Failed to capture enough frames (got "
 				  << completeRequestsCount_ << " expected at least "
-				  << cfg.bufferCount * 2 << ")" << std::endl;
+				  << nFrames << ")" << std::endl;
 			return TestFail;
 		}
 
@@ -161,6 +168,8 @@
 	}
 
 private:
+	EventDispatcher *dispatcher_;
+
 	std::vector<std::unique_ptr<Request>> requests_;
 
 	unsigned int completeBuffersCount_;
diff --git a/test/camera/capture.cpp b/test/camera/capture.cpp
index de82408..8766fb1 100644
--- a/test/camera/capture.cpp
+++ b/test/camera/capture.cpp
@@ -59,6 +59,8 @@
 		request->reuse();
 		request->addBuffer(stream, buffer);
 		camera_->queueRequest(request);
+
+		dispatcher_->interrupt();
 	}
 
 	int init() override
@@ -73,6 +75,7 @@
 		}
 
 		allocator_ = new FrameBufferAllocator(camera_);
+		dispatcher_ = Thread::current()->eventDispatcher();
 
 		return TestPass;
 	}
@@ -135,19 +138,20 @@
 			}
 		}
 
-		EventDispatcher *dispatcher = Thread::current()->eventDispatcher();
+		unsigned int nFrames = allocator_->buffers(stream).size() * 2;
 
 		Timer timer;
-		timer.start(1000ms);
-		while (timer.isRunning())
-			dispatcher->processEvents();
+		timer.start(500ms * nFrames);
+		while (timer.isRunning()) {
+			dispatcher_->processEvents();
+			if (completeRequestsCount_ > nFrames)
+				break;
+		}
 
-		unsigned int nbuffers = allocator_->buffers(stream).size();
-
-		if (completeRequestsCount_ < nbuffers * 2) {
+		if (completeRequestsCount_ < nFrames) {
 			cout << "Failed to capture enough frames (got "
 			     << completeRequestsCount_ << " expected at least "
-			     << nbuffers * 2 << ")" << endl;
+			     << nFrames * 2 << ")" << endl;
 			return TestFail;
 		}
 
@@ -164,6 +168,8 @@
 		return TestPass;
 	}
 
+	EventDispatcher *dispatcher_;
+
 	std::vector<std::unique_ptr<Request>> requests_;
 
 	std::unique_ptr<CameraConfiguration> config_;
diff --git a/test/controls/control_info.cpp b/test/controls/control_info.cpp
index 1176a50..e1bb43f 100644
--- a/test/controls/control_info.cpp
+++ b/test/controls/control_info.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_info.cpp - ControlInfo tests
+ * ControlInfo tests
  */
 
 #include <iostream>
diff --git a/test/controls/control_info_map.cpp b/test/controls/control_info_map.cpp
index 29b3351..b0be14b 100644
--- a/test/controls/control_info_map.cpp
+++ b/test/controls/control_info_map.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_info.cpp - ControlInfoMap tests
+ * ControlInfoMap tests
  */
 
 #include <iostream>
diff --git a/test/controls/control_list.cpp b/test/controls/control_list.cpp
index c03f230..e27325c 100644
--- a/test/controls/control_list.cpp
+++ b/test/controls/control_list.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_list.cpp - ControlList tests
+ * ControlList tests
  */
 
 #include <iostream>
@@ -196,6 +196,56 @@
 			return TestFail;
 		}
 
+		/*
+		 * Create two lists with overlapping controls. Merge them with
+		 * overwriteExisting = true, verifying that the existing control
+		 * values *get* overwritten.
+		 */
+		mergeList.clear();
+		mergeList.set(controls::Brightness, 0.7f);
+		mergeList.set(controls::Saturation, 0.4f);
+
+		list.clear();
+		list.set(controls::Brightness, 0.5f);
+		list.set(controls::Contrast, 1.1f);
+
+		mergeList.merge(list, ControlList::MergePolicy::OverwriteExisting);
+		if (mergeList.size() != 3) {
+			cout << "Merged list should contain three elements" << endl;
+			return TestFail;
+		}
+
+		if (list.size() != 2) {
+			cout << "The list to merge should contain two elements"
+			     << endl;
+			return TestFail;
+		}
+
+		if (!mergeList.get(controls::Brightness) ||
+		    !mergeList.get(controls::Contrast) ||
+		    !mergeList.get(controls::Saturation)) {
+			cout << "Merged list does not contain all controls" << endl;
+			return TestFail;
+		}
+
+		if (mergeList.get(controls::Brightness) != 0.5f) {
+			cout << "Brightness control value did not change after merging lists"
+			     << endl;
+			return TestFail;
+		}
+
+		if (mergeList.get(controls::Contrast) != 1.1f) {
+			cout << "Contrast control value changed after merging lists"
+			     << endl;
+			return TestFail;
+		}
+
+		if (mergeList.get(controls::Saturation) != 0.4f) {
+			cout << "Saturation control value changed after merging lists"
+			     << endl;
+			return TestFail;
+		}
+
 		return TestPass;
 	}
 };
diff --git a/test/controls/control_value.cpp b/test/controls/control_value.cpp
index ad8e05d..344107f 100644
--- a/test/controls/control_value.cpp
+++ b/test/controls/control_value.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_value.cpp - ControlValue tests
+ * ControlValue tests
  */
 
 #include <algorithm>
diff --git a/test/delayed_controls.cpp b/test/delayed_controls.cpp
index a8ce982..7bd30e7 100644
--- a/test/delayed_controls.cpp
+++ b/test/delayed_controls.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * delayed_controls.cpp - libcamera delayed controls test
+ * libcamera delayed controls test
  */
 
 #include <iostream>
diff --git a/test/event-dispatcher.cpp b/test/event-dispatcher.cpp
index 9b07ab2..f71c1c0 100644
--- a/test/event-dispatcher.cpp
+++ b/test/event-dispatcher.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event-dispatcher.cpp - Event dispatcher test
+ * Event dispatcher test
  */
 
 #include <chrono>
diff --git a/test/event-thread.cpp b/test/event-thread.cpp
index ef8a52c..5499bbf 100644
--- a/test/event-thread.cpp
+++ b/test/event-thread.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event-thread.cpp - Threaded event test
+ * Threaded event test
  */
 
 #include <chrono>
@@ -11,6 +11,7 @@
 #include <unistd.h>
 
 #include <libcamera/base/event_notifier.h>
+#include <libcamera/base/object.h>
 #include <libcamera/base/thread.h>
 #include <libcamera/base/timer.h>
 
@@ -84,10 +85,17 @@
 class EventThreadTest : public Test
 {
 protected:
+	int init()
+	{
+		thread_.start();
+
+		handler_ = new EventHandler();
+
+		return TestPass;
+	}
+
 	int run()
 	{
-		Thread thread;
-		thread.start();
 
 		/*
 		 * Fire the event notifier and then move the notifier to a
@@ -97,23 +105,33 @@
 		 * different thread will correctly process already pending
 		 * events in the new thread.
 		 */
-		EventHandler handler;
-		handler.notify();
-		handler.moveToThread(&thread);
+		handler_->notify();
+		handler_->moveToThread(&thread_);
 
 		this_thread::sleep_for(chrono::milliseconds(100));
 
-		/* Must stop thread before destroying the handler. */
-		thread.exit(0);
-		thread.wait();
-
-		if (!handler.notified()) {
+		if (!handler_->notified()) {
 			cout << "Thread event handling test failed" << endl;
 			return TestFail;
 		}
 
 		return TestPass;
 	}
+
+	void cleanup()
+	{
+		/*
+		 * Object class instances must be destroyed from the thread
+		 * they live in.
+		 */
+		handler_->deleteLater();
+		thread_.exit(0);
+		thread_.wait();
+	}
+
+private:
+	EventHandler *handler_;
+	Thread thread_;
 };
 
 TEST_REGISTER(EventThreadTest)
diff --git a/test/event.cpp b/test/event.cpp
index 19dceae..9f7b1ed 100644
--- a/test/event.cpp
+++ b/test/event.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * event.cpp - Event test
+ * Event test
  */
 
 #include <iostream>
diff --git a/test/fence.cpp b/test/fence.cpp
index 1e38bc2..8095b22 100644
--- a/test/fence.cpp
+++ b/test/fence.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * fence.cpp - Fence test
+ * Fence test
  */
 
 #include <iostream>
@@ -43,7 +43,6 @@
 
 	void signalFence();
 
-	std::unique_ptr<Fence> fence_;
 	EventDispatcher *dispatcher_;
 	UniqueFD eventFd_;
 	UniqueFD eventFd2_;
@@ -58,8 +57,11 @@
 	bool expectedCompletionResult_ = true;
 	bool setFence_ = true;
 
-	unsigned int completedRequest_;
-
+	/*
+	 * Request IDs track the number of requests that have completed. They
+	 * are one-based, and don't wrap.
+	 */
+	unsigned int completedRequestId_;
 	unsigned int signalledRequestId_;
 	unsigned int expiredRequestId_;
 	unsigned int nbuffers_;
@@ -127,8 +129,19 @@
 		return TestFail;
 	}
 
-	signalledRequestId_ = nbuffers_ - 2;
-	expiredRequestId_ = nbuffers_ - 1;
+	completedRequestId_ = 0;
+
+	/*
+	 * All but two requests are queued without a fence. Request
+	 * expiredRequestId_ will be queued with a fence that we won't signal
+	 * (which is then expected to expire), and request signalledRequestId_
+	 * will be queued with a fence that gets signalled. Select nbuffers_
+	 * and nbuffers_ * 2 for those two requests, to space them by a few
+	 * frames while still not requiring a long time for the test to
+	 * complete.
+	 */
+	expiredRequestId_ = nbuffers_;
+	signalledRequestId_ = nbuffers_ * 2;
 
 	return TestPass;
 }
@@ -190,16 +203,16 @@
 	const Request::BufferMap &buffers = request->buffers();
 	const Stream *stream = buffers.begin()->first;
 	FrameBuffer *buffer = buffers.begin()->second;
-	uint64_t cookie = request->cookie();
 
 	request->reuse();
 
-	if (cookie == signalledRequestId_ && setFence_) {
+	if (completedRequestId_ == signalledRequestId_ - nbuffers_ && setFence_) {
 		/*
-		 * The second time this request is queued add a fence to it.
-		 *
-		 * The main loop signals it by using a timer to write to the
-		 * efd2_ file descriptor before the fence expires.
+		 * This is the request that will be used to test fence
+		 * signalling when it completes next time. Add a fence to it,
+		 * using efd2_. The main loop will signal the fence by using a
+		 * timer to write to the efd2_ file descriptor before the fence
+		 * expires.
 		 */
 		std::unique_ptr<Fence> fence =
 			std::make_unique<Fence>(std::move(eventFd2_));
@@ -214,16 +227,15 @@
 
 void FenceTest::requestComplete(Request *request)
 {
-	uint64_t cookie = request->cookie();
-	completedRequest_ = cookie;
+	completedRequestId_++;
 
 	/*
-	 * The last request is expected to fail as its fence has not been
-	 * signaled.
+	 * Request expiredRequestId_ is expected to fail as its fence has not
+	 * been signalled.
 	 *
 	 * Validate the fence status but do not re-queue it.
 	 */
-	if (cookie == expiredRequestId_) {
+	if (completedRequestId_ == expiredRequestId_) {
 		if (validateExpiredRequest(request) != TestPass)
 			expectedCompletionResult_ = false;
 
@@ -231,7 +243,7 @@
 		return;
 	}
 
-	/* Validate all requests but the last. */
+	/* Validate all other requests. */
 	if (validateRequest(request) != TestPass) {
 		expectedCompletionResult_ = false;
 
@@ -272,15 +284,16 @@
 		}
 
 		int ret;
-		if (i == expiredRequestId_) {
+		if (i == expiredRequestId_ - 1) {
 			/* This request will have a fence, and it will expire. */
-			fence_ = std::make_unique<Fence>(std::move(eventFd_));
-			if (!fence_->isValid()) {
+			std::unique_ptr<Fence> fence =
+				std::make_unique<Fence>(std::move(eventFd_));
+			if (!fence->isValid()) {
 				cerr << "Fence should be valid" << endl;
 				return TestFail;
 			}
 
-			ret = request->addBuffer(stream_, buffer.get(), std::move(fence_));
+			ret = request->addBuffer(stream_, buffer.get(), std::move(fence));
 		} else {
 			/* All other requests will have no Fence. */
 			ret = request->addBuffer(stream_, buffer.get());
@@ -314,15 +327,21 @@
 	Timer fenceTimer;
 	fenceTimer.timeout.connect(this, &FenceTest::signalFence);
 
-	/* Loop for one second. */
+	/*
+	 * Loop long enough for all requests to complete, allowing 500ms per
+	 * request.
+	 */
 	Timer timer;
-	timer.start(1000ms);
-	while (timer.isRunning() && expectedCompletionResult_) {
-		if (completedRequest_ == signalledRequestId_ && setFence_)
+	timer.start(500ms * (signalledRequestId_ + 1));
+	while (timer.isRunning() && expectedCompletionResult_ &&
+	       completedRequestId_ <= signalledRequestId_ + 1) {
+		if (completedRequestId_ == signalledRequestId_ - 1 && setFence_)
 			/*
-			 * signalledRequestId_ has just completed and it has
-			 * been re-queued with a fence. Start the timer to
-			 * signal the fence in 10 msec.
+			 * The request just before signalledRequestId_ has just
+			 * completed. Request signalledRequestId_ has been
+			 * queued with a fence, and libcamera is likely already
+			 * waiting on the fence, or will soon. Start the timer
+			 * to signal the fence in 10 msec.
 			 */
 			fenceTimer.start(10ms);
 
diff --git a/test/file.cpp b/test/file.cpp
index 5c978eb..170e6cc 100644
--- a/test/file.cpp
+++ b/test/file.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * file.cpp - File I/O operations tests
+ * File I/O operations tests
  */
 
 #include <fstream>
diff --git a/test/flags.cpp b/test/flags.cpp
index 2177e24..85c3478 100644
--- a/test/flags.cpp
+++ b/test/flags.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * flags.cpp - Flags tests
+ * Flags tests
  */
 
 #include <iostream>
diff --git a/test/geometry.cpp b/test/geometry.cpp
index 008d51e..6416920 100644
--- a/test/geometry.cpp
+++ b/test/geometry.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * geometry.cpp - Geometry classes tests
+ * Geometry classes tests
  */
 
 #include <iostream>
diff --git a/test/gstreamer/gstreamer_device_provider_test.cpp b/test/gstreamer/gstreamer_device_provider_test.cpp
index 237af8c..8b8e7cb 100644
--- a/test/gstreamer/gstreamer_device_provider_test.cpp
+++ b/test/gstreamer/gstreamer_device_provider_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2023, Umang Jain <umang.jain@ideasonboard.com>
  *
- * gstreamer_single_stream_test.cpp - GStreamer single stream capture test
+ * GStreamer single stream capture test
  */
 
 #include <vector>
diff --git a/test/gstreamer/gstreamer_multi_stream_test.cpp b/test/gstreamer/gstreamer_multi_stream_test.cpp
index cd66930..263d1e8 100644
--- a/test/gstreamer/gstreamer_multi_stream_test.cpp
+++ b/test/gstreamer/gstreamer_multi_stream_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Vedant Paranjape
  *
- * gstreamer_multi_stream_test.cpp - GStreamer multi stream capture test
+ * GStreamer multi stream capture test
  */
 
 #include <iostream>
diff --git a/test/gstreamer/gstreamer_single_stream_test.cpp b/test/gstreamer/gstreamer_single_stream_test.cpp
index a0dd12c..3ef2d32 100644
--- a/test/gstreamer/gstreamer_single_stream_test.cpp
+++ b/test/gstreamer/gstreamer_single_stream_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Vedant Paranjape
  *
- * gstreamer_single_stream_test.cpp - GStreamer single stream capture test
+ * GStreamer single stream capture test
  */
 
 #include <iostream>
@@ -29,30 +29,21 @@
 		if (status_ != TestPass)
 			return status_;
 
-		const gchar *streamDescription = "videoconvert ! fakesink";
-		g_autoptr(GError) error0 = NULL;
-		stream0_ = gst_parse_bin_from_description_full(streamDescription, TRUE,
-						NULL,
-						GST_PARSE_FLAG_FATAL_ERRORS,
-						&error0);
-
-		if (!stream0_) {
-			g_printerr("Bin could not be created (%s)\n", error0->message);
+		fakesink_ = gst_element_factory_make("fakesink", nullptr);
+		if (!fakesink_) {
+			g_printerr("Your installation is missing 'fakesink'\n");
 			return TestFail;
 		}
-		g_object_ref_sink(stream0_);
+		g_object_ref_sink(fakesink_);
 
-		if (createPipeline() != TestPass)
-			return TestFail;
-
-		return TestPass;
+		return createPipeline();
 	}
 
 	int run() override
 	{
 		/* Build the pipeline */
-		gst_bin_add_many(GST_BIN(pipeline_), libcameraSrc_, stream0_, NULL);
-		if (gst_element_link(libcameraSrc_, stream0_) != TRUE) {
+		gst_bin_add_many(GST_BIN(pipeline_), libcameraSrc_, fakesink_, nullptr);
+		if (!gst_element_link(libcameraSrc_, fakesink_)) {
 			g_printerr("Elements could not be linked.\n");
 			return TestFail;
 		}
@@ -68,11 +59,11 @@
 
 	void cleanup() override
 	{
-		g_clear_object(&stream0_);
+		g_clear_object(&fakesink_);
 	}
 
 private:
-	GstElement *stream0_;
+	GstElement *fakesink_;
 };
 
 TEST_REGISTER(GstreamerSingleStreamTest)
diff --git a/test/gstreamer/gstreamer_test.cpp b/test/gstreamer/gstreamer_test.cpp
index 6ad0c15..a15fef0 100644
--- a/test/gstreamer/gstreamer_test.cpp
+++ b/test/gstreamer/gstreamer_test.cpp
@@ -9,12 +9,17 @@
 
 #include <libcamera/base/utils.h>
 
+#if HAVE_ASAN
+#include <sanitizer/asan_interface.h>
+#endif
+
 #include "gstreamer_test.h"
 
 #include "test.h"
 
 using namespace std;
 
+#if HAVE_ASAN
 extern "C" {
 const char *__asan_default_options()
 {
@@ -26,20 +31,20 @@
 	return "detect_leaks=false";
 }
 }
+#endif
 
 GstreamerTest::GstreamerTest(unsigned int numStreams)
 	: pipeline_(nullptr), libcameraSrc_(nullptr)
 {
 	/*
-	* GStreamer by default spawns a process to run the
-	* gst-plugin-scanner helper. If libcamera is compiled with ASan
-	* enabled, and as GStreamer is most likely not, this causes the
-	* ASan link order check to fail when gst-plugin-scanner
-	* dlopen()s the plugin as many libraries will have already been
-	* loaded by then. Fix this issue by disabling spawning of a
-	* child helper process when scanning the build directory for
-	* plugins.
-	*/
+	 * GStreamer by default spawns a process to run the gst-plugin-scanner
+	 * helper. If libcamera is compiled with ASan enabled, and as GStreamer
+	 * is most likely not, this causes the ASan link order check to fail
+	 * when gst-plugin-scanner dlopen()s the plugin as many libraries will
+	 * have already been loaded by then. Fix this issue by disabling
+	 * spawning of a child helper process when scanning the build directory
+	 * for plugins.
+	 */
 	gst_registry_fork_set_enabled(false);
 
 	/* Initialize GStreamer */
@@ -53,23 +58,6 @@
 	}
 
 	/*
-	* Remove the system libcamera plugin, if any, and add the
-	* plugin from the build directory.
-	*/
-	GstRegistry *registry = gst_registry_get();
-	g_autoptr(GstPlugin) plugin = gst_registry_lookup(registry, "libgstlibcamera.so");
-	if (plugin)
-		gst_registry_remove_plugin(registry, plugin);
-
-	std::string path = libcamera::utils::libcameraBuildPath() + "src/gstreamer";
-	if (!gst_registry_scan_path(registry, path.c_str())) {
-		g_printerr("Failed to add plugin to registry\n");
-
-		status_ = TestFail;
-		return;
-	}
-
-	/*
 	 * Atleast one camera should be available with numStreams streams,
 	 * otherwise skip the test entirely.
 	 */
diff --git a/test/gstreamer/gstreamer_test.h b/test/gstreamer/gstreamer_test.h
index aa2261e..abb37c1 100644
--- a/test/gstreamer/gstreamer_test.h
+++ b/test/gstreamer/gstreamer_test.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Vedant Paranjape
  *
- * gstreamer_test.cpp - GStreamer test base class
+ * GStreamer test base class
  */
 
 #pragma once
diff --git a/test/gstreamer/meson.build b/test/gstreamer/meson.build
index a5c003b..37ad125 100644
--- a/test/gstreamer/meson.build
+++ b/test/gstreamer/meson.build
@@ -11,11 +11,18 @@
 ]
 gstreamer_dep = dependency('gstreamer-1.0', required : true)
 
+gstreamer_test_args = []
+
+if asan_enabled
+    gstreamer_test_args += ['-D', 'HAVE_ASAN=1']
+endif
+
 foreach test : gstreamer_tests
     exe = executable(test['name'], test['sources'], 'gstreamer_test.cpp',
+                     cpp_args : gstreamer_test_args,
                      dependencies : [libcamera_private, gstreamer_dep],
                      link_with : test_libraries,
                      include_directories : test_includes_internal)
 
-    test(test['name'], exe, suite : 'gstreamer', is_parallel : false)
+    test(test['name'], exe, suite : 'gstreamer', is_parallel : false, env : gst_env)
 endforeach
diff --git a/test/hotplug-cameras.cpp b/test/hotplug-cameras.cpp
index 5d9260a..530e9a3 100644
--- a/test/hotplug-cameras.cpp
+++ b/test/hotplug-cameras.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Umang Jain <email@uajain.com>
  *
- * hotplug-cameras.cpp - Test cameraAdded/cameraRemoved signals in CameraManager
+ * Test cameraAdded/cameraRemoved signals in CameraManager
  */
 
 #include <dirent.h>
diff --git a/test/ipa/ipa_interface_test.cpp b/test/ipa/ipa_interface_test.cpp
index 051ef96..e840f6a 100644
--- a/test/ipa/ipa_interface_test.cpp
+++ b/test/ipa/ipa_interface_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_interface_test.cpp - Test the IPA interface
+ * Test the IPA interface
  */
 
 #include <fcntl.h>
@@ -16,6 +16,7 @@
 
 #include <libcamera/base/event_dispatcher.h>
 #include <libcamera/base/event_notifier.h>
+#include <libcamera/base/object.h>
 #include <libcamera/base/thread.h>
 #include <libcamera/base/timer.h>
 
@@ -55,7 +56,7 @@
 		const std::vector<PipelineHandlerFactoryBase *> &factories =
 			PipelineHandlerFactoryBase::factories();
 		for (const PipelineHandlerFactoryBase *factory : factories) {
-			if (factory->name() == "PipelineHandlerVimc") {
+			if (factory->name() == "vimc") {
 				pipe_ = factory->create(nullptr);
 				break;
 			}
diff --git a/test/ipa/ipa_module_test.cpp b/test/ipa/ipa_module_test.cpp
index bd5e0e4..1c97da3 100644
--- a/test/ipa/ipa_module_test.cpp
+++ b/test/ipa/ipa_module_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * ipa_module_test.cpp - Test loading of the VIMC IPA module and verify its info
+ * Test loading of the VIMC IPA module and verify its info
  */
 
 #include <iostream>
@@ -57,7 +57,7 @@
 		const struct IPAModuleInfo testInfo = {
 			IPA_MODULE_API_VERSION,
 			0,
-			"PipelineHandlerVimc",
+			"vimc",
 			"vimc",
 		};
 
diff --git a/test/ipa/meson.build b/test/ipa/meson.build
index 180b0da..fe21ca5 100644
--- a/test/ipa/meson.build
+++ b/test/ipa/meson.build
@@ -1,5 +1,7 @@
 # SPDX-License-Identifier: CC0-1.0
 
+subdir('rkisp1')
+
 ipa_test = [
     {'name': 'ipa_module_test', 'sources': ['ipa_module_test.cpp']},
     {'name': 'ipa_interface_test', 'sources': ['ipa_interface_test.cpp']},
@@ -7,9 +9,9 @@
 
 foreach test : ipa_test
     exe = executable(test['name'], test['sources'], libcamera_generated_ipa_headers,
-                     dependencies : libcamera_private,
-                     link_with : [libipa, test_libraries],
-                     include_directories : [libipa_includes, test_includes_internal])
+                     dependencies : [libcamera_private, libipa_dep],
+                     link_with : [test_libraries],
+                     include_directories : [test_includes_internal])
 
     test(test['name'], exe, suite : 'ipa')
 endforeach
diff --git a/test/ipa/rkisp1/meson.build b/test/ipa/rkisp1/meson.build
new file mode 100644
index 0000000..5b08e29
--- /dev/null
+++ b/test/ipa/rkisp1/meson.build
@@ -0,0 +1,15 @@
+# SPDX-License-Identifier: CC0-1.0
+
+rkisp1_ipa_test = [
+    {'name': 'rkisp1-utils', 'sources': ['rkisp1-utils.cpp']},
+]
+
+foreach test : rkisp1_ipa_test
+    exe = executable(test['name'], test['sources'], libcamera_generated_ipa_headers,
+                     dependencies : [libcamera_private, libipa_dep],
+                     link_with : [test_libraries],
+                     include_directories : [test_includes_internal,
+                                            '../../../src/ipa/rkisp1/'])
+
+    test(test['name'], exe, suite : 'ipa')
+endforeach
diff --git a/test/ipa/rkisp1/rkisp1-utils.cpp b/test/ipa/rkisp1/rkisp1-utils.cpp
new file mode 100644
index 0000000..b186389
--- /dev/null
+++ b/test/ipa/rkisp1/rkisp1-utils.cpp
@@ -0,0 +1,108 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+ *
+ * Miscellaneous utility tests
+ */
+
+#include <cmath>
+#include <iostream>
+#include <map>
+#include <stdint.h>
+
+#include "../src/ipa/rkisp1/utils.h"
+
+#include "test.h"
+
+using namespace std;
+using namespace libcamera;
+using namespace ipa::rkisp1;
+
+class RkISP1UtilsTest : public Test
+{
+protected:
+	/* R for real, I for integer */
+	template<unsigned int IntPrec, unsigned int FracPrec, typename I, typename R>
+	int testFixedToFloat(I input, R expected)
+	{
+		R out = utils::fixedToFloatingPoint<IntPrec, FracPrec, R>(input);
+		R prec = 1.0 / (1 << FracPrec);
+		if (std::abs(out - expected) > prec) {
+			cerr << "Reverse conversion expected " << input
+			     << " to convert to " << expected
+			     << ", got " << out << std::endl;
+			return TestFail;
+		}
+
+		return TestPass;
+	}
+
+	template<unsigned int IntPrec, unsigned int FracPrec, typename T>
+	int testSingleFixedPoint(double input, T expected)
+	{
+		T ret = utils::floatingToFixedPoint<IntPrec, FracPrec, T>(input);
+		if (ret != expected) {
+			cerr << "Expected " << input << " to convert to "
+			     << expected << ", got " << ret << std::endl;
+			return TestFail;
+		}
+
+		/*
+		 * The precision check is fairly arbitrary but is based on what
+		 * the rkisp1 is capable of in the crosstalk module.
+		 */
+		double f = utils::fixedToFloatingPoint<IntPrec, FracPrec, double>(ret);
+		if (std::abs(f - input) > 0.005) {
+			cerr << "Reverse conversion expected " << ret
+			     << " to convert to " << input
+			     << ", got " << f << std::endl;
+			return TestFail;
+		}
+
+		return TestPass;
+	}
+
+	int testFixedPoint()
+	{
+		/*
+		 * The second 7.992 test is to test that unused bits don't
+		 * affect the result.
+		 */
+		std::map<double, uint16_t> testCases = {
+			{ 7.992, 0x3ff },
+			{   0.2, 0x01a },
+			{  -0.2, 0x7e6 },
+			{  -0.8, 0x79a },
+			{  -0.4, 0x7cd },
+			{  -1.4, 0x74d },
+			{    -8, 0x400 },
+			{     0, 0 },
+		};
+
+		int ret;
+		for (const auto &testCase : testCases) {
+			ret = testSingleFixedPoint<4, 7, uint16_t>(testCase.first,
+								   testCase.second);
+			if (ret != TestPass)
+				return ret;
+		}
+
+		/* Special case with a superfluous one in the unused bits */
+		ret = testFixedToFloat<4, 7, uint16_t, double>(0xbff, 7.992);
+		if (ret != TestPass)
+			return ret;
+
+		return TestPass;
+	}
+
+	int run()
+	{
+		/* fixed point conversion test */
+		if (testFixedPoint() != TestPass)
+			return TestFail;
+
+		return TestPass;
+	}
+};
+
+TEST_REGISTER(RkISP1UtilsTest)
diff --git a/test/ipc/unixsocket.cpp b/test/ipc/unixsocket.cpp
index 304e613..2546882 100644
--- a/test/ipc/unixsocket.cpp
+++ b/test/ipc/unixsocket.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * unixsocket.cpp - Unix socket IPC test
+ * Unix socket IPC test
  */
 
 #include <algorithm>
@@ -34,6 +34,8 @@
 using namespace std;
 using namespace std::chrono_literals;
 
+namespace {
+
 int calculateLength(int fd)
 {
 	lseek(fd, 0, 0);
@@ -43,6 +45,8 @@
 	return size;
 }
 
+} /* namespace */
+
 class UnixSocketTestSlave
 {
 public:
@@ -431,7 +435,7 @@
 		if (ret)
 			return ret;
 
-		timeout.start(200ms);
+		timeout.start(2s);
 		while (!callDone_) {
 			if (!timeout.isRunning()) {
 				cerr << "Call timeout!" << endl;
diff --git a/test/ipc/unixsocket_ipc.cpp b/test/ipc/unixsocket_ipc.cpp
index 3ee6017..df7d9c2 100644
--- a/test/ipc/unixsocket_ipc.cpp
+++ b/test/ipc/unixsocket_ipc.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * unixsocket_ipc.cpp - Unix socket IPC test
+ * Unix socket IPC test
  */
 
 #include <algorithm>
diff --git a/test/libtest/buffer_source.h b/test/libtest/buffer_source.h
index 0cc71aa..495da8a 100644
--- a/test/libtest/buffer_source.h
+++ b/test/libtest/buffer_source.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * buffer_source.h - libcamera camera test helper to create FrameBuffers
+ * libcamera camera test helper to create FrameBuffers
  */
 
 #pragma once
diff --git a/test/libtest/camera_test.h b/test/libtest/camera_test.h
index 0b178bc..713b503 100644
--- a/test/libtest/camera_test.h
+++ b/test/libtest/camera_test.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * camera_test.h - libcamera camera test base class
+ * libcamera camera test base class
  */
 
 #pragma once
diff --git a/test/libtest/test.cpp b/test/libtest/test.cpp
index af37b4d..4e03def 100644
--- a/test/libtest/test.cpp
+++ b/test/libtest/test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * test.cpp - libcamera test base class
+ * libcamera test base class
  */
 
 #include <stdlib.h>
diff --git a/test/libtest/test.h b/test/libtest/test.h
index 23b0774..3a90885 100644
--- a/test/libtest/test.h
+++ b/test/libtest/test.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * test.h - libcamera test base class
+ * libcamera test base class
  */
 
 #pragma once
diff --git a/test/log/log_api.cpp b/test/log/log_api.cpp
index 5311896..0b99973 100644
--- a/test/log/log_api.cpp
+++ b/test/log/log_api.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * log.cpp - log API test
+ * log API test
  */
 
 #include <algorithm>
diff --git a/test/log/log_process.cpp b/test/log/log_process.cpp
index 966b80c..9609e23 100644
--- a/test/log/log_process.cpp
+++ b/test/log/log_process.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * log_process.cpp - Logging in isolated child process test
+ * Logging in isolated child process test
  */
 
 #include <fcntl.h>
@@ -81,12 +81,13 @@
 			return TestFail;
 		}
 
-		timeout.start(200ms);
+		timeout.start(2s);
 		while (timeout.isRunning())
 			dispatcher->processEvents();
 
 		if (exitStatus_ != Process::NormalExit) {
-			cerr << "process did not exit normally" << endl;
+			cerr << "process did not exit normally: " << exitStatus_
+			     << endl;
 			return TestFail;
 		}
 
@@ -115,8 +116,11 @@
 		close(fd);
 
 		string str(buf);
-		if (str.find(message) == string::npos)
+		if (str.find(message) == string::npos) {
+			cerr << "Received message is not correct (received "
+			     << str.length() << " bytes)" << endl;
 			return TestFail;
+		}
 
 		return TestPass;
 	}
@@ -136,7 +140,7 @@
 	ProcessManager processManager_;
 
 	Process proc_;
-	Process::ExitStatus exitStatus_;
+	Process::ExitStatus exitStatus_ = Process::NotExited;
 	string logPath_;
 	int exitCode_;
 	int num_;
diff --git a/test/media_device/media_device_link_test.cpp b/test/media_device/media_device_link_test.cpp
index e11f6b7..3152800 100644
--- a/test/media_device/media_device_link_test.cpp
+++ b/test/media_device/media_device_link_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * media_device_link_test.cpp - Tests link handling on VIMC media device
+ * Tests link handling on VIMC media device
  */
 
 #include <iostream>
diff --git a/test/media_device/media_device_print_test.cpp b/test/media_device/media_device_print_test.cpp
index cdec5b8..63aeed4 100644
--- a/test/media_device/media_device_print_test.cpp
+++ b/test/media_device/media_device_print_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018-2019, Google Inc.
  *
- * media_device_print_test.cpp - Print out media devices
+ * Print out media devices
  */
 
 #include <iostream>
diff --git a/test/media_device/media_device_test.cpp b/test/media_device/media_device_test.cpp
index 1397d12..3e41d0f 100644
--- a/test/media_device/media_device_test.cpp
+++ b/test/media_device/media_device_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * media_device_test.cpp - libcamera media device test base class
+ * libcamera media device test base class
  */
 
 #include <iostream>
diff --git a/test/media_device/media_device_test.h b/test/media_device/media_device_test.h
index 9b226f1..5223b76 100644
--- a/test/media_device/media_device_test.h
+++ b/test/media_device/media_device_test.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * media_device_test.h - libcamera media device test base class
+ * libcamera media device test base class
  */
 
 #pragma once
diff --git a/test/meson.build b/test/meson.build
index b227be8..5ed052e 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -46,6 +46,7 @@
     {'name': 'public-api', 'sources': ['public-api.cpp']},
     {'name': 'signal', 'sources': ['signal.cpp']},
     {'name': 'span', 'sources': ['span.cpp']},
+    {'name': 'transform', 'sources': ['transform.cpp']},
 ]
 
 internal_tests = [
@@ -68,6 +69,7 @@
     {'name': 'signal-threads', 'sources': ['signal-threads.cpp']},
     {'name': 'threads', 'sources': 'threads.cpp', 'dependencies': [libthreads]},
     {'name': 'timer', 'sources': ['timer.cpp']},
+    {'name': 'timer-fail', 'sources': ['timer-fail.cpp'], 'should_fail': true},
     {'name': 'timer-thread', 'sources': ['timer-thread.cpp']},
     {'name': 'unique-fd', 'sources': ['unique-fd.cpp']},
     {'name': 'utils', 'sources': ['utils.cpp']},
@@ -87,10 +89,11 @@
 
     exe = executable(test['name'], test['sources'],
                      dependencies : deps,
+                     implicit_include_directories : false,
                      link_with : test_libraries,
                      include_directories : test_includes_public)
 
-    test(test['name'], exe)
+    test(test['name'], exe, should_fail : test.get('should_fail', false))
 endforeach
 
 foreach test : internal_tests
@@ -101,10 +104,11 @@
 
     exe = executable(test['name'], test['sources'],
                      dependencies : deps,
+                     implicit_include_directories : false,
                      link_with : test_libraries,
                      include_directories : test_includes_internal)
 
-    test(test['name'], exe)
+    test(test['name'], exe, should_fail : test.get('should_fail', false))
 endforeach
 
 foreach test : internal_non_parallel_tests
@@ -115,8 +119,11 @@
 
     exe = executable(test['name'], test['sources'],
                      dependencies : deps,
+                     implicit_include_directories : false,
                      link_with : test_libraries,
                      include_directories : test_includes_internal)
 
-    test(test['name'], exe, is_parallel : false)
+    test(test['name'], exe,
+         is_parallel : false,
+         should_fail : test.get('should_fail', false))
 endforeach
diff --git a/test/message.cpp b/test/message.cpp
index d148a13..19e6646 100644
--- a/test/message.cpp
+++ b/test/message.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * message.cpp - Messages test
+ * Messages test
  */
 
 #include <chrono>
@@ -11,6 +11,7 @@
 #include <thread>
 
 #include <libcamera/base/message.h>
+#include <libcamera/base/object.h>
 #include <libcamera/base/thread.h>
 
 #include "test.h"
@@ -92,25 +93,6 @@
 	bool success_;
 };
 
-class SlowMessageReceiver : public Object
-{
-protected:
-	void message(Message *msg)
-	{
-		if (msg->type() != Message::None) {
-			Object::message(msg);
-			return;
-		}
-
-		/*
-		 * Don't access any member of the object here (including the
-		 * vtable) as the object will be deleted by the main thread
-		 * while we're sleeping.
-		 */
-		this_thread::sleep_for(chrono::milliseconds(100));
-	}
-};
-
 class MessageTest : public Test
 {
 protected:
@@ -127,16 +109,19 @@
 			return TestFail;
 		}
 
-		MessageReceiver receiver;
-		receiver.moveToThread(&thread_);
+		MessageReceiver *receiver = new MessageReceiver();
+		receiver->moveToThread(&thread_);
 
 		thread_.start();
 
-		receiver.postMessage(std::make_unique<Message>(Message::None));
+		receiver->postMessage(std::make_unique<Message>(Message::None));
 
 		this_thread::sleep_for(chrono::milliseconds(100));
 
-		switch (receiver.status()) {
+		MessageReceiver::Status status = receiver->status();
+		receiver->deleteLater();
+
+		switch (status) {
 		case MessageReceiver::NoMessage:
 			cout << "No message received" << endl;
 			return TestFail;
@@ -148,28 +133,12 @@
 		}
 
 		/*
-		 * Test for races between message delivery and object deletion.
-		 * Failures result in assertion errors, there is no need for
-		 * explicit checks.
-		 */
-		SlowMessageReceiver *slowReceiver = new SlowMessageReceiver();
-		slowReceiver->moveToThread(&thread_);
-		slowReceiver->postMessage(std::make_unique<Message>(Message::None));
-
-		this_thread::sleep_for(chrono::milliseconds(10));
-
-		delete slowReceiver;
-
-		this_thread::sleep_for(chrono::milliseconds(100));
-
-		/*
 		 * Test recursive calls to Thread::dispatchMessages(). Messages
 		 * should be delivered correctly, without crashes or memory
 		 * leaks. Two messages need to be posted to ensure we don't only
 		 * test the simple case of a queue containing a single message.
 		 */
-		std::unique_ptr<RecursiveMessageReceiver> recursiveReceiver =
-			std::make_unique<RecursiveMessageReceiver>();
+		RecursiveMessageReceiver *recursiveReceiver = new RecursiveMessageReceiver();
 		recursiveReceiver->moveToThread(&thread_);
 
 		recursiveReceiver->postMessage(std::make_unique<Message>(Message::None));
@@ -177,7 +146,10 @@
 
 		this_thread::sleep_for(chrono::milliseconds(10));
 
-		if (!recursiveReceiver->success()) {
+		bool success = recursiveReceiver->success();
+		recursiveReceiver->deleteLater();
+
+		if (!success) {
 			cout << "Recursive message delivery failed" << endl;
 			return TestFail;
 		}
diff --git a/test/object-delete.cpp b/test/object-delete.cpp
index eabefe9..676c397 100644
--- a/test/object-delete.cpp
+++ b/test/object-delete.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * object.cpp - Object deletion tests
+ * Object deletion tests
  */
 
 #include <iostream>
@@ -33,10 +33,10 @@
 	unsigned int *deleteCount_;
 };
 
-class NewThread : public Thread
+class DeleterThread : public Thread
 {
 public:
-	NewThread(Object *obj)
+	DeleterThread(Object *obj)
 		: object_(obj)
 	{
 	}
@@ -63,9 +63,9 @@
 		unsigned int count = 0;
 		TestObject *obj = new TestObject(&count);
 
-		NewThread thread(obj);
-		thread.start();
-		thread.wait();
+		DeleterThread delThread(obj);
+		delThread.start();
+		delThread.wait();
 
 		Thread::current()->dispatchMessages(Message::Type::DeferredDelete);
 
@@ -89,6 +89,26 @@
 			return TestFail;
 		}
 
+		/*
+		 * Test that deleteLater() works properly when called just
+		 * before the object's thread exits.
+		 */
+		Thread boundThread;
+		boundThread.start();
+
+		count = 0;
+		obj = new TestObject(&count);
+		obj->moveToThread(&boundThread);
+
+		obj->deleteLater();
+		boundThread.exit();
+		boundThread.wait();
+
+		if (count != 1) {
+			cout << "Object deletion right before thread exit failed (" << count << ")" << endl;
+			return TestFail;
+		}
+
 		return TestPass;
 	}
 };
diff --git a/test/object-invoke.cpp b/test/object-invoke.cpp
index b1c0f47..def1e61 100644
--- a/test/object-invoke.cpp
+++ b/test/object-invoke.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * object-invoke.cpp - Cross-thread Object method invocation test
+ * Cross-thread Object method invocation test
  */
 
 #include <iostream>
diff --git a/test/object.cpp b/test/object.cpp
index cbd0d3e..95dc1ef 100644
--- a/test/object.cpp
+++ b/test/object.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * object.cpp - Object tests
+ * Object tests
  */
 
 #include <iostream>
diff --git a/test/process/process_test.cpp b/test/process/process_test.cpp
index cb6940c..e9f5e7e 100644
--- a/test/process/process_test.cpp
+++ b/test/process/process_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * process_test.cpp - Process test
+ * Process test
  */
 
 #include <iostream>
diff --git a/test/public-api.cpp b/test/public-api.cpp
index a1cebcf..b1336f7 100644
--- a/test/public-api.cpp
+++ b/test/public-api.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * public-api.cpp - Public API validation
+ * Public API validation
  */
 
 #include <libcamera/libcamera.h>
diff --git a/test/serialization/control_serialization.cpp b/test/serialization/control_serialization.cpp
index a507d98..06c572b 100644
--- a/test/serialization/control_serialization.cpp
+++ b/test/serialization/control_serialization.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * control_serialization.cpp - Serialize and deserialize controls
+ * Serialize and deserialize controls
  */
 
 #include <iostream>
diff --git a/test/serialization/generated_serializer/generated_serializer_test.cpp b/test/serialization/generated_serializer/generated_serializer_test.cpp
index 4670fe4..4b11d67 100644
--- a/test/serialization/generated_serializer/generated_serializer_test.cpp
+++ b/test/serialization/generated_serializer/generated_serializer_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * generated_serializer_test.cpp - Test generated serializer
+ * Test generated serializer
  */
 
 #include <algorithm>
diff --git a/test/serialization/ipa_data_serializer_test.cpp b/test/serialization/ipa_data_serializer_test.cpp
index 377ecdb..aea63c7 100644
--- a/test/serialization/ipa_data_serializer_test.cpp
+++ b/test/serialization/ipa_data_serializer_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * ipa_data_serializer_test.cpp - Test serializing/deserializing with IPADataSerializer
+ * Test serializing/deserializing with IPADataSerializer
  */
 
 #include <algorithm>
diff --git a/test/serialization/serialization_test.cpp b/test/serialization/serialization_test.cpp
index 11d0f0f..af9969f 100644
--- a/test/serialization/serialization_test.cpp
+++ b/test/serialization/serialization_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * serialization_test.cpp - Base class for serialization tests
+ * Base class for serialization tests
  */
 
 #include "serialization_test.h"
diff --git a/test/serialization/serialization_test.h b/test/serialization/serialization_test.h
index 609f9fd..760e372 100644
--- a/test/serialization/serialization_test.h
+++ b/test/serialization/serialization_test.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * serialization_test.h - Base class for serialization tests
+ * Base class for serialization tests
  */
 
 #pragma once
diff --git a/test/shared-fd.cpp b/test/shared-fd.cpp
index 997d7be..57199df 100644
--- a/test/shared-fd.cpp
+++ b/test/shared-fd.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * shared_fd.cpp - SharedFD test
+ * SharedFD test
  */
 
 #include <fcntl.h>
diff --git a/test/signal-threads.cpp b/test/signal-threads.cpp
index d5e2eb6..c4789c8 100644
--- a/test/signal-threads.cpp
+++ b/test/signal-threads.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * signal-threads.cpp - Cross-thread signal delivery test
+ * Cross-thread signal delivery test
  */
 
 #include <chrono>
@@ -10,6 +10,7 @@
 #include <thread>
 
 #include <libcamera/base/message.h>
+#include <libcamera/base/object.h>
 #include <libcamera/base/thread.h>
 #include <libcamera/base/utils.h>
 
@@ -58,15 +59,20 @@
 class SignalThreadsTest : public Test
 {
 protected:
+	int init()
+	{
+		receiver_ = new SignalReceiver();
+		signal_.connect(receiver_, &SignalReceiver::slot);
+
+		return TestPass;
+	}
+
 	int run()
 	{
-		SignalReceiver receiver;
-		signal_.connect(&receiver, &SignalReceiver::slot);
-
 		/* Test that a signal is received in the main thread. */
 		signal_.emit(0);
 
-		switch (receiver.status()) {
+		switch (receiver_->status()) {
 		case SignalReceiver::NoSignal:
 			cout << "No signal received for direct connection" << endl;
 			return TestFail;
@@ -82,8 +88,8 @@
 		 * Move the object to a thread and verify that the signal is
 		 * correctly delivered, with the correct data.
 		 */
-		receiver.reset();
-		receiver.moveToThread(&thread_);
+		receiver_->reset();
+		receiver_->moveToThread(&thread_);
 
 		thread_.start();
 
@@ -91,7 +97,7 @@
 
 		this_thread::sleep_for(chrono::milliseconds(100));
 
-		switch (receiver.status()) {
+		switch (receiver_->status()) {
 		case SignalReceiver::NoSignal:
 			cout << "No signal received for message connection" << endl;
 			return TestFail;
@@ -103,7 +109,7 @@
 			break;
 		}
 
-		if (receiver.value() != 42) {
+		if (receiver_->value() != 42) {
 			cout << "Signal received with incorrect value" << endl;
 			return TestFail;
 		}
@@ -113,11 +119,13 @@
 
 	void cleanup()
 	{
+		receiver_->deleteLater();
 		thread_.exit(0);
 		thread_.wait();
 	}
 
 private:
+	SignalReceiver *receiver_;
 	Thread thread_;
 
 	Signal<int> signal_;
diff --git a/test/signal.cpp b/test/signal.cpp
index 5c6b304..3f596b2 100644
--- a/test/signal.cpp
+++ b/test/signal.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * signal.cpp - Signal test
+ * Signal test
  */
 
 #include <iostream>
diff --git a/test/span.cpp b/test/span.cpp
index abf3a5d..5452967 100644
--- a/test/span.cpp
+++ b/test/span.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * span.cpp - Span tests
+ * Span tests
  */
 
 /*
diff --git a/test/stream/stream_colorspace.cpp b/test/stream/stream_colorspace.cpp
index 1b7afe6..4c904c4 100644
--- a/test/stream/stream_colorspace.cpp
+++ b/test/stream/stream_colorspace.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Ideas on Board Oy.
  *
- * stream_colorspace.cpp - Stream colorspace adjustment test
+ * Stream colorspace adjustment test
  */
 
 #include <iostream>
diff --git a/test/stream/stream_formats.cpp b/test/stream/stream_formats.cpp
index 99fa038..553b59a 100644
--- a/test/stream/stream_formats.cpp
+++ b/test/stream/stream_formats.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * stream_formats.cpp - StreamFormats test
+ * StreamFormats test
  */
 
 #include <iostream>
diff --git a/test/threads.cpp b/test/threads.cpp
index 8f366c9..ceb4fa0 100644
--- a/test/threads.cpp
+++ b/test/threads.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * threads.cpp - Threads test
+ * Threads test
  */
 
 #include <chrono>
diff --git a/test/timer-fail.cpp b/test/timer-fail.cpp
new file mode 100644
index 0000000..0ced644
--- /dev/null
+++ b/test/timer-fail.cpp
@@ -0,0 +1,109 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2024, Ideas on Board Oy
+ *
+ * Threaded timer failure test
+ */
+
+#include <chrono>
+#include <iostream>
+
+#include <libcamera/base/event_dispatcher.h>
+#include <libcamera/base/object.h>
+#include <libcamera/base/thread.h>
+#include <libcamera/base/timer.h>
+
+#include "test.h"
+
+using namespace libcamera;
+using namespace std;
+using namespace std::chrono_literals;
+
+class TimeoutHandler : public Object
+{
+public:
+	TimeoutHandler()
+		: timer_(this), timeout_(false)
+	{
+		timer_.timeout.connect(this, &TimeoutHandler::timeoutHandler);
+	}
+
+	void start()
+	{
+		timer_.start(100ms);
+	}
+
+	bool timeout() const
+	{
+		return timeout_;
+	}
+
+private:
+	void timeoutHandler()
+	{
+		timeout_ = true;
+	}
+
+	Timer timer_;
+	bool timeout_;
+};
+
+class TimerFailTest : public Test
+{
+protected:
+	int init()
+	{
+		thread_.start();
+
+		timeout_ = new TimeoutHandler();
+		timeout_->moveToThread(&thread_);
+
+		return TestPass;
+	}
+
+	int run()
+	{
+		/*
+		 * Test that the forbidden operation of starting the timer from
+		 * another thread results in a failure. We need to interrupt the
+		 * event dispatcher to make sure we don't succeed simply because
+		 * the event dispatcher hasn't noticed the timer restart.
+		 */
+		timeout_->start();
+		thread_.eventDispatcher()->interrupt();
+
+		this_thread::sleep_for(chrono::milliseconds(200));
+
+		/*
+		 * The wrong start() call should result in an assertion in debug
+		 * builds, and a timeout in release builds. The test is
+		 * therefore marked in meson.build as expected to fail. We need
+		 * to return TestPass in the unexpected (usually known as
+		 * "fail") case, and TestFail otherwise.
+		 */
+		if (timeout_->timeout()) {
+			cout << "Timer start from wrong thread succeeded unexpectedly"
+			     << endl;
+			return TestPass;
+		}
+
+		return TestFail;
+	}
+
+	void cleanup()
+	{
+		/*
+		 * Object class instances must be destroyed from the thread
+		 * they live in.
+		 */
+		timeout_->deleteLater();
+		thread_.exit(0);
+		thread_.wait();
+	}
+
+private:
+	TimeoutHandler *timeout_;
+	Thread thread_;
+};
+
+TEST_REGISTER(TimerFailTest)
diff --git a/test/timer-thread.cpp b/test/timer-thread.cpp
index 6182175..55e5cfd 100644
--- a/test/timer-thread.cpp
+++ b/test/timer-thread.cpp
@@ -2,13 +2,14 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * timer-thread.cpp - Threaded timer test
+ * Threaded timer test
  */
 
 #include <chrono>
 #include <iostream>
 
 #include <libcamera/base/event_dispatcher.h>
+#include <libcamera/base/object.h>
 #include <libcamera/base/thread.h>
 #include <libcamera/base/timer.h>
 
@@ -28,12 +29,6 @@
 		timer_.start(100ms);
 	}
 
-	void restart()
-	{
-		timeout_ = false;
-		timer_.start(100ms);
-	}
-
 	bool timeout() const
 	{
 		return timeout_;
@@ -55,7 +50,9 @@
 	int init()
 	{
 		thread_.start();
-		timeout_.moveToThread(&thread_);
+
+		timeout_ = new TimeoutHandler();
+		timeout_->moveToThread(&thread_);
 
 		return TestPass;
 	}
@@ -68,39 +65,27 @@
 		 */
 		this_thread::sleep_for(chrono::milliseconds(200));
 
-		if (!timeout_.timeout()) {
+		if (!timeout_->timeout()) {
 			cout << "Timer expiration test failed" << endl;
 			return TestFail;
 		}
 
-		/*
-		 * Test that starting the timer from another thread fails. We
-		 * need to interrupt the event dispatcher to make sure we don't
-		 * succeed simply because the event dispatcher hasn't noticed
-		 * the timer restart.
-		 */
-		timeout_.restart();
-		thread_.eventDispatcher()->interrupt();
-
-		this_thread::sleep_for(chrono::milliseconds(200));
-
-		if (timeout_.timeout()) {
-			cout << "Timer restart test failed" << endl;
-			return TestFail;
-		}
-
 		return TestPass;
 	}
 
 	void cleanup()
 	{
-		/* Must stop thread before destroying timeout. */
+		/*
+		 * Object class instances must be destroyed from the thread
+		 * they live in.
+		 */
+		timeout_->deleteLater();
 		thread_.exit(0);
 		thread_.wait();
 	}
 
 private:
-	TimeoutHandler timeout_;
+	TimeoutHandler *timeout_;
 	Thread thread_;
 };
 
diff --git a/test/timer.cpp b/test/timer.cpp
index 0f01c3c..2eacc05 100644
--- a/test/timer.cpp
+++ b/test/timer.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * timer.cpp - Timer test
+ * Timer test
  */
 
 #include <chrono>
diff --git a/test/transform.cpp b/test/transform.cpp
new file mode 100644
index 0000000..4ec7a1e
--- /dev/null
+++ b/test/transform.cpp
@@ -0,0 +1,329 @@
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+/*
+ * Copyright (C) 2023, Ideas On Board Oy
+ *
+ * Transform and Orientation tests
+ */
+
+#include <iostream>
+
+#include <libcamera/orientation.h>
+#include <libcamera/transform.h>
+
+#include "test.h"
+
+using namespace std;
+using namespace libcamera;
+
+class TransformTest : public Test
+{
+protected:
+	int run();
+};
+
+int TransformTest::run()
+{
+	/*
+	 * RotationTestEntry collects two Orientation and one Transform that
+	 * gets combined to validate that (o1 / o2 = T) and (o1 = o2 * T)
+	 *
+	 * o1 / o2 = t computes the Transform to apply to o2 to obtain o1
+	 * o2 * t = o1 combines o2 with t by applying o2 first then t
+	 *
+	 * The comments on the (most complex) transform show how applying to
+	 * an image with orientation o2 the Transform t allows to obtain o1.
+	 *
+	 * The image with basic rotation0 is assumed to be:
+	 *
+	 * 	AB
+	 * 	CD
+	 *
+	 * And the Transform operators are:
+	 *
+	 * 	V = vertical flip
+	 * 	H = horizontal flip
+	 * 	T = transpose
+	 *
+	 * the operator '* (T|V)' applies V first then T.
+	 */
+	static const struct RotationTestEntry {
+		Orientation o1;
+		Orientation o2;
+		Transform t;
+	} testEntries[] = {
+		/* Test identities transforms first. */
+		{
+			Orientation::Rotate0, Orientation::Rotate0,
+			Transform::Identity,
+		},
+		{
+			Orientation::Rotate0Mirror, Orientation::Rotate0Mirror,
+			Transform::Identity,
+		},
+		{
+			Orientation::Rotate180, Orientation::Rotate180,
+			Transform::Identity,
+		},
+		{
+			Orientation::Rotate180Mirror, Orientation::Rotate180Mirror,
+			Transform::Identity,
+		},
+		{
+			Orientation::Rotate90, Orientation::Rotate90,
+			Transform::Identity,
+		},
+		{
+			Orientation::Rotate90Mirror, Orientation::Rotate90Mirror,
+			Transform::Identity,
+		},
+		{
+			Orientation::Rotate270, Orientation::Rotate270,
+			Transform::Identity,
+		},
+		{
+			Orientation::Rotate270Mirror, Orientation::Rotate270Mirror,
+			Transform::Identity,
+		},
+		/*
+		 * Combine 0 and 180 degrees rotation as they're the most common
+		 * ones.
+		 */
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	CD  * (H|V) =  	BA	AB
+			 *	BA		CD	CD
+			 */
+			Orientation::Rotate0, Orientation::Rotate180,
+			Transform::Rot180,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	AB  * (H|V) =  	CD	DC
+			 *	CD		AB	BA
+			 */
+			Orientation::Rotate180, Orientation::Rotate0,
+			Transform::Rot180
+		},
+		/* Test that transpositions are handled correctly. */
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	AB  * (T|V) =  	CD	CA
+			 *	CD		AB	DB
+			 */
+			Orientation::Rotate90, Orientation::Rotate0,
+			Transform::Rot90,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	CA  * (T|H) =  	AC	AB
+			 *	DB		BD	CD
+			 */
+			Orientation::Rotate0, Orientation::Rotate90,
+			Transform::Rot270,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	AB  * (T|H) =  	BA	BD
+			 *	CD		DC	AC
+			 */
+			Orientation::Rotate270, Orientation::Rotate0,
+			Transform::Rot270,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	BD  * (T|V) =  	AC	AB
+			 *	AC		BD	CD
+			 */
+			Orientation::Rotate0, Orientation::Rotate270,
+			Transform::Rot90,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	CD  * (T|H) =  	DC	DA
+			 *	BA		AB	CB
+			 */
+			Orientation::Rotate90, Orientation::Rotate180,
+			Transform::Rot270,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	DA  * (T|V) =  	CB	CD
+			 *	CB		DA	BA
+			 */
+			Orientation::Rotate180, Orientation::Rotate90,
+			Transform::Rot90,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	CD  * (T|V) =  	BA	BC
+			 *	BA		CD	AD
+			 */
+			Orientation::Rotate270, Orientation::Rotate180,
+			Transform::Rot90,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	BC  * (T|H) =  	CB	CD
+			 *	AD		DA	BA
+			 */
+			Orientation::Rotate180, Orientation::Rotate270,
+			Transform::Rot270,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	DA  * (V|H) =  	AD	BC
+			 *	CB		BC	AD
+			 */
+			Orientation::Rotate270, Orientation::Rotate90,
+			Transform::Rot180,
+		},
+		/* Test that mirroring is handled correctly. */
+		{
+			Orientation::Rotate0, Orientation::Rotate0Mirror,
+			Transform::HFlip
+		},
+		{
+			Orientation::Rotate0Mirror, Orientation::Rotate0,
+			Transform::HFlip
+		},
+		{
+			Orientation::Rotate180, Orientation::Rotate180Mirror,
+			Transform::HFlip
+		},
+		{
+			Orientation::Rotate180Mirror, Orientation::Rotate180,
+			Transform::HFlip
+		},
+		{
+			Orientation::Rotate90, Orientation::Rotate90Mirror,
+			Transform::HFlip
+		},
+		{
+			Orientation::Rotate90Mirror, Orientation::Rotate90,
+			Transform::HFlip
+		},
+		{
+			Orientation::Rotate270, Orientation::Rotate270Mirror,
+			Transform::HFlip
+		},
+		{
+			Orientation::Rotate270Mirror, Orientation::Rotate270,
+			Transform::HFlip
+		},
+		{
+			Orientation::Rotate0, Orientation::Rotate0Mirror,
+			Transform::HFlip
+		},
+		/*
+		 * More exotic transforms which include Transpositions and
+		 * mirroring.
+		 */
+		{
+			/*
+			 *      o2      t       o1
+			 *      ------------------
+			 *	BC  * (V) =  	AD
+			 *	AD		BC
+			 */
+			Orientation::Rotate90Mirror, Orientation::Rotate270,
+			Transform::VFlip,
+		},
+		{
+			/*
+			 *      o2      t       o1
+			 *      ------------------
+			 *	CB  * (T) =  	CD
+			 *	DA		BA
+			 */
+			Orientation::Rotate180, Orientation::Rotate270Mirror,
+			Transform::Transpose,
+		},
+		{
+			/*
+			 *      o2      t       o1
+			 *      ------------------
+			 *	AD  * (T) =  	AB
+			 *	BC		DC
+			 */
+			Orientation::Rotate0, Orientation::Rotate90Mirror,
+			Transform::Transpose,
+		},
+		{
+			/*
+			 *      o2      t       o1
+			 *      ------------------
+			 *	AD  * (V) =  	BC
+			 *	BC		AD
+			 */
+			Orientation::Rotate270, Orientation::Rotate90Mirror,
+			Transform::VFlip,
+		},
+		{
+			/*
+			 *      o2      t       o1
+			 *      ------------------
+			 *	DA  * (V) =  	CB
+			 *	CB		DA
+			 */
+			Orientation::Rotate270Mirror, Orientation::Rotate90,
+			Transform::VFlip,
+		},
+		{
+			/*
+			 *      o2      t               o1
+			 *      --------------------------
+			 *	CB  * (V|H) =	BC  	AD
+			 *	DA		AD	BC
+			 */
+			Orientation::Rotate90Mirror, Orientation::Rotate270Mirror,
+			Transform::Rot180,
+		},
+	};
+
+	for (const auto &entry : testEntries) {
+		Transform transform = entry.o1 / entry.o2;
+		if (transform != entry.t) {
+			cerr << "Failed to validate: " << entry.o1
+			     << " / " << entry.o2
+			     << " = " << transformToString(entry.t) << endl;
+			cerr << "Got back: "
+			     << transformToString(transform) << endl;
+			return TestFail;
+		}
+
+		Orientation adjusted = entry.o2 * entry.t;
+		if (adjusted != entry.o1) {
+			cerr << "Failed to validate: " << entry.o2
+			     << " * " << transformToString(entry.t)
+			     << " = " << entry.o1 << endl;
+			cerr << "Got back: " << adjusted << endl;
+			return TestFail;
+		}
+	}
+
+	return TestPass;
+}
+
+TEST_REGISTER(TransformTest)
diff --git a/test/unique-fd.cpp b/test/unique-fd.cpp
index eb3b591..e556439 100644
--- a/test/unique-fd.cpp
+++ b/test/unique-fd.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * unique-fd.cpp - UniqueFD test
+ * UniqueFD test
  */
 
 #include <fcntl.h>
diff --git a/test/utils.cpp b/test/utils.cpp
index fc56e14..41af954 100644
--- a/test/utils.cpp
+++ b/test/utils.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * utils.cpp - Miscellaneous utility tests
+ * Miscellaneous utility tests
  */
 
 #include <iostream>
diff --git a/test/v4l2_compat/v4l2_compat_test.py b/test/v4l2_compat/v4l2_compat_test.py
index bd89d49..443babc 100755
--- a/test/v4l2_compat/v4l2_compat_test.py
+++ b/test/v4l2_compat/v4l2_compat_test.py
@@ -4,7 +4,7 @@
 #
 # Author: Paul Elder <paul.elder@ideasonboard.com>
 #
-# v4l2_compat_test.py - Test the V4L2 compatibility layer
+# Test the V4L2 compatibility layer
 
 import argparse
 import glob
diff --git a/test/v4l2_subdevice/v4l2_subdevice_test.cpp b/test/v4l2_subdevice/v4l2_subdevice_test.cpp
index d8fbfd9..c349c9e 100644
--- a/test/v4l2_subdevice/v4l2_subdevice_test.cpp
+++ b/test/v4l2_subdevice/v4l2_subdevice_test.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_subdevice_test.cpp - VIMC-based V4L2 subdevice test
+ * VIMC-based V4L2 subdevice test
  */
 
 #include <iostream>
diff --git a/test/v4l2_subdevice/v4l2_subdevice_test.h b/test/v4l2_subdevice/v4l2_subdevice_test.h
index e73c583..89b7830 100644
--- a/test/v4l2_subdevice/v4l2_subdevice_test.h
+++ b/test/v4l2_subdevice/v4l2_subdevice_test.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * v4l2_subdevice_test.h - VIMC-based V4L2 subdevice test
+ * VIMC-based V4L2 subdevice test
  */
 
 #pragma once
diff --git a/test/v4l2_videodevice/capture_async.cpp b/test/v4l2_videodevice/capture_async.cpp
index 42e1e67..6736646 100644
--- a/test/v4l2_videodevice/capture_async.cpp
+++ b/test/v4l2_videodevice/capture_async.cpp
@@ -61,10 +61,12 @@
 		if (ret)
 			return TestFail;
 
-		timeout.start(10000ms);
+		const unsigned int nFrames = 30;
+
+		timeout.start(500ms * nFrames);
 		while (timeout.isRunning()) {
 			dispatcher->processEvents();
-			if (frames > 30)
+			if (frames > nFrames)
 				break;
 		}
 
@@ -73,8 +75,9 @@
 			return TestFail;
 		}
 
-		if (frames < 30) {
-			std::cout << "Failed to capture 30 frames within timeout." << std::endl;
+		if (frames < nFrames) {
+			std::cout << "Failed to capture " << nFrames
+				  << " frames within timeout." << std::endl;
 			return TestFail;
 		}
 
diff --git a/test/v4l2_videodevice/controls.cpp b/test/v4l2_videodevice/controls.cpp
index 0f603b8..b013029 100644
--- a/test/v4l2_videodevice/controls.cpp
+++ b/test/v4l2_videodevice/controls.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2019, Google Inc.
  *
- * controls.cpp - V4L2 device controls handling test
+ * V4L2 device controls handling test
  */
 
 #include <algorithm>
diff --git a/test/v4l2_videodevice/v4l2_videodevice_test.cpp b/test/v4l2_videodevice/v4l2_videodevice_test.cpp
index 125aafd..1113cf5 100644
--- a/test/v4l2_videodevice/v4l2_videodevice_test.cpp
+++ b/test/v4l2_videodevice/v4l2_videodevice_test.cpp
@@ -75,7 +75,7 @@
 		format.fourcc = V4L2PixelFormat(V4L2_PIX_FMT_SBGGR8);
 
 		V4L2SubdeviceFormat subformat = {};
-		subformat.mbus_code = MEDIA_BUS_FMT_SBGGR8_1X8;
+		subformat.code = MEDIA_BUS_FMT_SBGGR8_1X8;
 		subformat.size = format.size;
 
 		if (sensor_->setFormat(&subformat))
diff --git a/test/v4l2_videodevice/v4l2_videodevice_test.h b/test/v4l2_videodevice/v4l2_videodevice_test.h
index d2de1a6..b5871ce 100644
--- a/test/v4l2_videodevice/v4l2_videodevice_test.h
+++ b/test/v4l2_videodevice/v4l2_videodevice_test.h
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2018, Google Inc.
  *
- * vl42device_test.h - libcamera v4l2device test base class
+ * libcamera v4l2device test base class
  */
 
 #pragma once
diff --git a/test/yaml-parser.cpp b/test/yaml-parser.cpp
index 2d92463..81c8298 100644
--- a/test/yaml-parser.cpp
+++ b/test/yaml-parser.cpp
@@ -2,7 +2,7 @@
 /*
  * Copyright (C) 2022, Google Inc.
  *
- * yaml-parser.cpp - YAML parser operations tests
+ * YAML parser operations tests
  */
 
 #include <array>
diff --git a/utils/checkstyle.py b/utils/checkstyle.py
index 836ea80..4185c39 100755
--- a/utils/checkstyle.py
+++ b/utils/checkstyle.py
@@ -1,10 +1,10 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
 # SPDX-License-Identifier: GPL-2.0-or-later
 # Copyright (C) 2018, Google Inc.
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# checkstyle.py - A patch style checker script based on clang-format
+# A patch style checker script based on clang-format
 #
 # TODO:
 #
@@ -168,6 +168,12 @@
             hunk = DiffHunk(line)
 
         elif hunk is not None:
+            # Work around https://github.com/python/cpython/issues/46395
+            # See https://www.gnu.org/software/diffutils/manual/html_node/Incomplete-Lines.html
+            if line[-1] != '\n':
+                hunk.append(line + '\n')
+                line = '\\ No newline at end of file\n'
+
             hunk.append(line)
 
     if hunk:
@@ -471,6 +477,7 @@
     known_trailers = {
         'Acked-by': email_regex,
         'Bug': link_regex,
+        'Co-developed-by': email_regex,
         'Fixes': commit_regex,
         'Link': link_regex,
         'Reported-by': validate_reported_by,
@@ -549,20 +556,49 @@
 
 
 class StyleIssue(object):
-    def __init__(self, line_number, line, msg):
+    def __init__(self, line_number, position, line, msg):
         self.line_number = line_number
+        self.position = position
         self.line = line
         self.msg = msg
 
 
+class HexValueChecker(StyleChecker):
+    patterns = ('*.c', '*.cpp', '*.h')
+
+    regex = re.compile(r'\b0[xX][0-9a-fA-F]+\b')
+
+    def __init__(self, content):
+        super().__init__()
+        self.__content = content
+
+    def check(self, line_numbers):
+        issues = []
+
+        for line_number in line_numbers:
+            line = self.__content[line_number - 1]
+            match = HexValueChecker.regex.search(line)
+            if not match:
+                continue
+
+            value = match.group(0)
+            if value == value.lower():
+                continue
+
+            issues.append(StyleIssue(line_number, match.span(0), line,
+                                     f'Use lowercase hex constant {value.lower()}'))
+
+        return issues
+
+
 class IncludeChecker(StyleChecker):
     patterns = ('*.cpp', '*.h')
 
-    headers = ('assert', 'ctype', 'errno', 'fenv', 'float', 'inttypes',
-               'limits', 'locale', 'setjmp', 'signal', 'stdarg', 'stddef',
-               'stdint', 'stdio', 'stdlib', 'string', 'time', 'uchar', 'wchar',
-               'wctype')
-    include_regex = re.compile('^#include <c([a-z]*)>')
+    headers = ('cassert', 'cctype', 'cerrno', 'cfenv', 'cfloat', 'cinttypes',
+               'climits', 'clocale', 'csetjmp', 'csignal', 'cstdarg', 'cstddef',
+               'cstdint', 'cstdio', 'cstdlib', 'cstring', 'ctime', 'cuchar',
+               'cwchar', 'cwctype', 'math.h')
+    include_regex = re.compile(r'^#include <([a-z.]*)>')
 
     def __init__(self, content):
         super().__init__()
@@ -581,14 +617,21 @@
             if header not in IncludeChecker.headers:
                 continue
 
-            issues.append(StyleIssue(line_number, line,
-                                     'C compatibility header <%s.h> is preferred' % header))
+            if header.endswith('.h'):
+                header_type = 'C++'
+                header = 'c' + header[:-2]
+            else:
+                header_type = 'C compatibility'
+                header = header[1:] + '.h'
+
+            issues.append(StyleIssue(line_number, match.span(1), line,
+                                     f'{header_type} header <{header}> is preferred'))
 
         return issues
 
 
 class LogCategoryChecker(StyleChecker):
-    log_regex = re.compile('\\bLOG\((Debug|Info|Warning|Error|Fatal)\)')
+    log_regex = re.compile(r'\bLOG\((Debug|Info|Warning|Error|Fatal)\)')
     patterns = ('*.cpp',)
 
     def __init__(self, content):
@@ -599,10 +642,12 @@
         issues = []
         for line_number in line_numbers:
             line = self.__content[line_number-1]
-            if not LogCategoryChecker.log_regex.search(line):
+            match = LogCategoryChecker.log_regex.search(line)
+            if not match:
                 continue
 
-            issues.append(StyleIssue(line_number, line, 'LOG() should use categories'))
+            issues.append(StyleIssue(line_number, match.span(1), line,
+                                     'LOG() should use categories'))
 
         return issues
 
@@ -618,14 +663,16 @@
         issues = []
         for line_number in line_numbers:
             line = self.__content[line_number-1]
-            if line.find('\t') != -1:
-                issues.append(StyleIssue(line_number, line, 'meson.build should use spaces for indentation'))
+            pos = line.find('\t')
+            if pos != -1:
+                issues.append(StyleIssue(line_number, [pos, pos], line,
+                                         'meson.build should use spaces for indentation'))
         return issues
 
 
 class Pep8Checker(StyleChecker):
     patterns = ('*.py',)
-    results_regex = re.compile('stdin:([0-9]+):([0-9]+)(.*)')
+    results_regex = re.compile(r'stdin:([0-9]+):([0-9]+)(.*)')
 
     def __init__(self, content):
         super().__init__()
@@ -639,7 +686,7 @@
             ret = subprocess.run(['pycodestyle', '--ignore=E501', '-'],
                                  input=data, stdout=subprocess.PIPE)
         except FileNotFoundError:
-            issues.append(StyleIssue(0, None, 'Please install pycodestyle to validate python additions'))
+            issues.append(StyleIssue(0, None, None, 'Please install pycodestyle to validate python additions'))
             return issues
 
         results = ret.stdout.decode('utf-8').splitlines()
@@ -651,14 +698,14 @@
 
             if line_number in line_numbers:
                 line = self.__content[line_number - 1]
-                issues.append(StyleIssue(line_number, line, msg))
+                issues.append(StyleIssue(line_number, None, line, msg))
 
         return issues
 
 
 class ShellChecker(StyleChecker):
     patterns = ('*.sh',)
-    results_line_regex = re.compile('In - line ([0-9]+):')
+    results_line_regex = re.compile(r'In - line ([0-9]+):')
 
     def __init__(self, content):
         super().__init__()
@@ -672,7 +719,7 @@
             ret = subprocess.run(['shellcheck', '-Cnever', '-'],
                                  input=data, stdout=subprocess.PIPE)
         except FileNotFoundError:
-            issues.append(StyleIssue(0, None, 'Please install shellcheck to validate shell script additions'))
+            issues.append(StyleIssue(0, None, None, 'Please install shellcheck to validate shell script additions'))
             return issues
 
         results = ret.stdout.decode('utf-8').splitlines()
@@ -685,11 +732,8 @@
             line = results[nr + 1]
             msg = results[nr + 2]
 
-            # Determined, but not yet used
-            position = msg.find('^') + 1
-
             if line_number in line_numbers:
-                issues.append(StyleIssue(line_number, line, msg))
+                issues.append(StyleIssue(line_number, None, line, msg))
 
         return issues
 
@@ -746,7 +790,8 @@
 class DoxygenFormatter(Formatter):
     patterns = ('*.c', '*.cpp')
 
-    return_regex = re.compile(' +\\* +\\\\return +[a-z]')
+    oneliner_regex = re.compile(r'^ +\* +\\(brief|param|return)\b.*\.$')
+    return_regex = re.compile(r' +\* +\\return +[a-z]')
 
     @classmethod
     def format(cls, filename, data):
@@ -761,6 +806,7 @@
                 lines.append(line)
                 continue
 
+            line = cls.oneliner_regex.sub(lambda m: m.group(0)[:-1], line)
             line = cls.return_regex.sub(lambda m: m.group(0)[:-1] + m.group(0)[-1].upper(), line)
 
             if line.find('*/') != -1:
@@ -806,7 +852,7 @@
 class IncludeOrderFormatter(Formatter):
     patterns = ('*.cpp', '*.h')
 
-    include_regex = re.compile('^#include (["<])([^">]*)([">])')
+    include_regex = re.compile(r'^#include (["<])([^">]*)([">])')
 
     @classmethod
     def format(cls, filename, data):
@@ -929,6 +975,16 @@
                 print('%s+%s%s' % (Colours.fg(Colours.Yellow), issue.line.rstrip(),
                                    Colours.reset()))
 
+                if issue.position is not None:
+                    # Align the position marker by using the original line with
+                    # all characters except for tabs replaced with spaces. This
+                    # ensures proper alignment regardless of how the code is
+                    # indented.
+                    start = issue.position[0]
+                    prefix = ''.join([c if c == '\t' else ' ' for c in issue.line[:start]])
+                    length = issue.position[1] - start - 1
+                    print(' ' + prefix + '^' + '~' * length)
+
     return len(formatted_diff) + len(issues)
 
 
diff --git a/utils/gen-controls.py b/utils/gen-controls.py
index 1075ae3..56315f5 100755
--- a/utils/gen-controls.py
+++ b/utils/gen-controls.py
@@ -4,7 +4,7 @@
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# gen-controls.py - Generate control definitions from YAML
+# Generate control definitions from YAML
 
 import argparse
 from functools import reduce
@@ -12,6 +12,7 @@
 import string
 import sys
 import yaml
+import os
 
 
 class ControlEnum(object):
@@ -35,11 +36,12 @@
 
 
 class Control(object):
-    def __init__(self, name, data):
+    def __init__(self, name, data, vendor):
         self.__name = name
         self.__data = data
         self.__enum_values = None
         self.__size = None
+        self.__vendor = vendor
 
         enum_values = data.get('enum')
         if enum_values is not None:
@@ -85,9 +87,9 @@
         return self.__enum_values is not None
 
     @property
-    def is_draft(self):
-        """Is the control a draft control"""
-        return self.__data.get('draft') is not None
+    def vendor(self):
+        """The vendor string, or None"""
+        return self.__vendor
 
     @property
     def name(self):
@@ -95,12 +97,6 @@
         return self.__name
 
     @property
-    def q_name(self):
-        """The control name, qualified with a namespace"""
-        ns = 'draft::' if self.is_draft else ''
-        return ns + self.__name
-
-    @property
     def type(self):
         typ = self.__data.get('type')
         size = self.__data.get('size')
@@ -144,16 +140,25 @@
  */''')
     enum_values_start = string.Template('''extern const std::array<const ControlValue, ${size}> ${name}Values = {''')
     enum_values_values = string.Template('''\tstatic_cast<int32_t>(${name}),''')
+    name_value_map_doc = string.Template('''/**
+ * \\var ${name}NameValueMap
+ * \\brief Map of all $name supported value names (in std::string format) to value
+ */''')
+    name_value_map_start = string.Template('''extern const std::map<std::string, ${type}> ${name}NameValueMap = {''')
+    name_value_values = string.Template('''\t{ "${name}", ${name} },''')
 
-    ctrls_doc = []
-    ctrls_def = []
-    draft_ctrls_doc = []
-    draft_ctrls_def = []
+    ctrls_doc = {}
+    ctrls_def = {}
     ctrls_map = []
 
     for ctrl in controls:
         id_name = snake_case(ctrl.name).upper()
 
+        vendor = ctrl.vendor
+        if vendor not in ctrls_doc:
+            ctrls_doc[vendor] = []
+            ctrls_def[vendor] = []
+
         info = {
             'name': ctrl.name,
             'type': ctrl.type,
@@ -161,11 +166,8 @@
             'id_name': id_name,
         }
 
-        target_doc = ctrls_doc
-        target_def = ctrls_def
-        if ctrl.is_draft:
-            target_doc = draft_ctrls_doc
-            target_def = draft_ctrls_def
+        target_doc = ctrls_doc[vendor]
+        target_def = ctrls_def[vendor]
 
         if ctrl.is_enum:
             enum_doc = []
@@ -187,6 +189,7 @@
 
             values_info = {
                 'name': info['name'],
+                'type': ctrl.type,
                 'size': num_entries,
             }
             target_doc.append(enum_values_doc.substitute(values_info))
@@ -198,44 +201,79 @@
                 target_def.append(enum_values_values.substitute(value_info))
             target_def.append("};")
 
+            target_doc.append(name_value_map_doc.substitute(values_info))
+            target_def.append(name_value_map_start.substitute(values_info))
+            for enum in ctrl.enum_values:
+                value_info = {
+                    'name': enum.name
+                }
+                target_def.append(name_value_values.substitute(value_info))
+            target_def.append("};")
+
         target_doc.append(doc_template.substitute(info))
         target_def.append(def_template.substitute(info))
 
-        ctrls_map.append('\t{ ' + id_name + ', &' + ctrl.q_name + ' },')
+        vendor_ns = vendor + '::' if vendor != "libcamera" else ''
+        ctrls_map.append('\t{ ' + vendor_ns + id_name + ', &' + vendor_ns + ctrl.name + ' },')
+
+    vendor_ctrl_doc_sub = []
+    vendor_ctrl_template = string.Template('''
+/**
+ * \\brief Namespace for ${vendor} controls
+ */
+namespace ${vendor} {
+
+${vendor_controls_str}
+
+} /* namespace ${vendor} */''')
+
+    for vendor in [v for v in ctrls_doc.keys() if v not in ['libcamera']]:
+        vendor_ctrl_doc_sub.append(vendor_ctrl_template.substitute({'vendor': vendor, 'vendor_controls_str': '\n\n'.join(ctrls_doc[vendor])}))
+
+    vendor_ctrl_def_sub = []
+    for vendor in [v for v in ctrls_def.keys() if v not in ['libcamera']]:
+        vendor_ctrl_def_sub.append(vendor_ctrl_template.substitute({'vendor': vendor, 'vendor_controls_str': '\n'.join(ctrls_def[vendor])}))
 
     return {
-        'controls_doc': '\n\n'.join(ctrls_doc),
-        'controls_def': '\n'.join(ctrls_def),
-        'draft_controls_doc': '\n\n'.join(draft_ctrls_doc),
-        'draft_controls_def': '\n\n'.join(draft_ctrls_def),
+        'controls_doc': '\n\n'.join(ctrls_doc['libcamera']),
+        'controls_def': '\n'.join(ctrls_def['libcamera']),
         'controls_map': '\n'.join(ctrls_map),
+        'vendor_controls_doc': '\n'.join(vendor_ctrl_doc_sub),
+        'vendor_controls_def': '\n'.join(vendor_ctrl_def_sub),
     }
 
 
-def generate_h(controls):
+def generate_h(controls, mode, ranges):
     enum_template_start = string.Template('''enum ${name}Enum {''')
     enum_value_template = string.Template('''\t${name} = ${value},''')
     enum_values_template = string.Template('''extern const std::array<const ControlValue, ${size}> ${name}Values;''')
+    name_value_map_template = string.Template('''extern const std::map<std::string, ${type}> ${name}NameValueMap;''')
     template = string.Template('''extern const Control<${type}> ${name};''')
 
-    ctrls = []
-    draft_ctrls = []
-    ids = []
-    id_value = 1
+    ctrls = {}
+    ids = {}
+    id_value = {}
 
     for ctrl in controls:
         id_name = snake_case(ctrl.name).upper()
 
-        ids.append('\t' + id_name + ' = ' + str(id_value) + ',')
+        vendor = ctrl.vendor
+        if vendor not in ctrls:
+            if vendor not in ranges.keys():
+                raise RuntimeError(f'Control id range is not defined for vendor {vendor}')
+            id_value[vendor] = ranges[vendor] + 1
+            ids[vendor] = []
+            ctrls[vendor] = []
+
+        target_ids = ids[vendor]
+        target_ids.append('\t' + id_name + ' = ' + str(id_value[vendor]) + ',')
 
         info = {
             'name': ctrl.name,
             'type': ctrl.type,
         }
 
-        target_ctrls = ctrls
-        if ctrl.is_draft:
-            target_ctrls = draft_ctrls
+        target_ctrls = ctrls[vendor]
 
         if ctrl.is_enum:
             target_ctrls.append(enum_template_start.substitute(info))
@@ -252,17 +290,41 @@
 
             values_info = {
                 'name': info['name'],
+                'type': ctrl.type,
                 'size': num_entries,
             }
             target_ctrls.append(enum_values_template.substitute(values_info))
+            target_ctrls.append(name_value_map_template.substitute(values_info))
 
         target_ctrls.append(template.substitute(info))
-        id_value += 1
+        id_value[vendor] += 1
+
+    vendor_template = string.Template('''
+namespace ${vendor} {
+
+#define LIBCAMERA_HAS_${vendor_def}_VENDOR_${mode}
+
+enum {
+${vendor_enums}
+};
+
+${vendor_controls}
+
+} /* namespace ${vendor} */
+''')
+
+    vendor_sub = []
+    for vendor in [v for v in ctrls.keys() if v != 'libcamera']:
+        vendor_sub.append(vendor_template.substitute({'mode': mode.upper(),
+                                                      'vendor': vendor,
+                                                      'vendor_def': vendor.upper(),
+                                                      'vendor_enums': '\n'.join(ids[vendor]),
+                                                      'vendor_controls': '\n'.join(ctrls[vendor])}))
 
     return {
-        'ids': '\n'.join(ids),
-        'controls': '\n'.join(ctrls),
-        'draft_controls': '\n'.join(draft_ctrls)
+        'ids': '\n'.join(ids['libcamera']),
+        'controls': '\n'.join(ctrls['libcamera']),
+        'vendor_controls': '\n'.join(vendor_sub)
     }
 
 
@@ -278,22 +340,36 @@
 
     # Parse command line arguments
     parser = argparse.ArgumentParser()
-    parser.add_argument('-o', dest='output', metavar='file', type=str,
+    parser.add_argument('--mode', '-m', type=str, required=True, choices=['controls', 'properties'],
+                        help='Mode of operation')
+    parser.add_argument('--output', '-o', metavar='file', type=str,
                         help='Output file name. Defaults to standard output if not specified.')
-    parser.add_argument('input', type=str,
-                        help='Input file name.')
-    parser.add_argument('template', type=str,
+    parser.add_argument('--ranges', '-r', type=str, required=True,
+                        help='Control id range reservation file.')
+    parser.add_argument('--template', '-t', dest='template', type=str, required=True,
                         help='Template file name.')
+    parser.add_argument('input', type=str, nargs='+',
+                        help='Input file name.')
+
     args = parser.parse_args(argv[1:])
 
-    data = open(args.input, 'rb').read()
-    controls = yaml.safe_load(data)['controls']
-    controls = [Control(*ctrl.popitem()) for ctrl in controls]
+    ranges = {}
+    with open(args.ranges, 'rb') as f:
+        data = open(args.ranges, 'rb').read()
+        ranges = yaml.safe_load(data)['ranges']
+
+    controls = []
+    for input in args.input:
+        with open(input, 'rb') as f:
+            data = f.read()
+            vendor = yaml.safe_load(data)['vendor']
+            ctrls = yaml.safe_load(data)['controls']
+            controls = controls + [Control(*ctrl.popitem(), vendor) for ctrl in ctrls]
 
     if args.template.endswith('.cpp.in'):
         data = generate_cpp(controls)
     elif args.template.endswith('.h.in'):
-        data = generate_h(controls)
+        data = generate_h(controls, args.mode, ranges)
     else:
         raise RuntimeError('Unknown template type')
 
diff --git a/utils/gen-formats.py b/utils/gen-formats.py
index da79a8b..0c0932a 100755
--- a/utils/gen-formats.py
+++ b/utils/gen-formats.py
@@ -4,7 +4,7 @@
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# gen-formats.py - Generate formats definitions from YAML
+# Generate formats definitions from YAML
 
 import argparse
 import re
diff --git a/utils/gen-header.sh b/utils/gen-header.sh
index 8b66c5d..d469275 100755
--- a/utils/gen-header.sh
+++ b/utils/gen-header.sh
@@ -9,7 +9,7 @@
 /*
  * Copyright (C) 2018-2019, Google Inc.
  *
- * libcamera.h - libcamera public API
+ * libcamera public API
  */
 
 #pragma once
diff --git a/utils/gen-ipa-priv-key.sh b/utils/gen-ipa-priv-key.sh
index 919751f..2ca7b88 100755
--- a/utils/gen-ipa-priv-key.sh
+++ b/utils/gen-ipa-priv-key.sh
@@ -4,7 +4,7 @@
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# gen-ipa-priv-key.sh - Generate an RSA private key to sign IPA modules
+# Generate an RSA private key to sign IPA modules
 
 key="$1"
 
diff --git a/utils/gen-ipa-pub-key.py b/utils/gen-ipa-pub-key.py
index a4a1f7b..dc3e7d5 100755
--- a/utils/gen-ipa-pub-key.py
+++ b/utils/gen-ipa-pub-key.py
@@ -4,7 +4,7 @@
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# ipa-gen-key.py - Generate the IPA module signing public key
+# Generate the IPA module signing public key
 
 import string
 import subprocess
diff --git a/utils/hooks/pre-push b/utils/hooks/pre-push
index 90ffdf6..9918b28 100755
--- a/utils/hooks/pre-push
+++ b/utils/hooks/pre-push
@@ -1,4 +1,4 @@
-#!/bin/sh
+#!/bin/bash
 
 # SPDX-License-Identifier: GPL-2.0-or-later
 
@@ -61,7 +61,7 @@
 		msg=$(git cat-file commit "$commit")
 
 		# 1. The commit message shall not contain a local changelog.
-		if echo "$msg" | grep -q '^--- *$'
+		if echo -E "$msg" | grep -q '^--- *$'
 		then
 			echo >&2 "Found local changelog in commit $commit"
 			errors=$((errors+1))
@@ -71,7 +71,7 @@
 		# corresponding the committer and the author.
 		committer=$(echo "$msg" | grep '^committer ' | head -1 | \
 				cut -d ' ' -f 2- | rev | cut -d ' ' -f 3- | rev)
-		if ! echo "$msg" | grep -F -q "Signed-off-by: ${committer}"
+		if ! echo -E "$msg" | grep -F -q "Signed-off-by: ${committer}"
 		then
 			echo >&2 "Missing committer Signed-off-by in commit $commit"
 			errors=$((errors+1))
@@ -79,21 +79,21 @@
 
 		author=$(echo "$msg" | grep '^author ' | head -1 | \
 				cut -d ' ' -f 2- | rev | cut -d ' ' -f 3- | rev)
-		if ! echo "$msg" | grep -F -q "Signed-off-by: ${author}"
+		if ! echo -E "$msg" | grep -F -q "Signed-off-by: ${author}"
 		then
 			echo >&2 "Missing author Signed-off-by in commit $commit"
 			errors=$((errors+1))
 		fi
 
 		# 3. A Reviewed-by or Acked-by is required.
-		if ! echo "$msg" | grep -q '^\(Reviewed\|Acked\)-by: '
+		if ! echo -E "$msg" | grep -q '^\(Reviewed\|Acked\)-by: '
 		then
 			echo >&2 "No Reviewed-by or Acked-by in commit $commit"
 			errors=$((errors+1))
 		fi
 
 		# 4. The commit message shall not contain a Change-Id.
-		if echo "$msg" | grep -q '^Change-Id:'
+		if echo -E "$msg" | grep -q '^Change-Id:'
 		then
 			echo >&2 "Found Change-Id in commit $commit"
 			errors=$((errors+1))
diff --git a/utils/ipc/extract-docs.py b/utils/ipc/extract-docs.py
index 8f7fff9..61f44ca 100755
--- a/utils/ipc/extract-docs.py
+++ b/utils/ipc/extract-docs.py
@@ -4,15 +4,15 @@
 #
 # Author: Paul Elder <paul.elder@ideasonboard.com>
 #
-# extract-docs.py - Extract doxygen documentation from mojom files
+# Extract doxygen documentation from mojom files
 
 import argparse
 import re
 import sys
 
-regex_block_start = re.compile('^\/\*\*$')
-regex_block_end = re.compile('^ \*\/$')
-regex_spdx = re.compile('^\/\* SPDX-License-Identifier: .* \*\/$')
+regex_block_start = re.compile(r'^/\*\*$')
+regex_block_end = re.compile(r'^ \*/$')
+regex_spdx = re.compile(r'^/\* SPDX-License-Identifier: .* \*/$')
 
 
 def main(argv):
@@ -38,7 +38,7 @@
 /*
  * Copyright (C) 2021, Google Inc.
  *
- * {pipeline}_ipa_interface.cpp - Docs file for generated {pipeline}.mojom
+ * Docs file for generated {pipeline}.mojom
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/utils/ipc/generate.py b/utils/ipc/generate.py
index 8771e0a..c2b3fcb 100755
--- a/utils/ipc/generate.py
+++ b/utils/ipc/generate.py
@@ -4,7 +4,7 @@
 #
 # Author: Paul Elder <paul.elder@ideasonboard.com>
 #
-# generate.py - Run mojo code generator for generating libcamera IPC files
+# Run mojo code generator for generating libcamera IPC files
 
 import os
 import sys
@@ -12,10 +12,20 @@
 # TODO set sys.pycache_prefix for >= python3.8
 sys.dont_write_bytecode = True
 
+sys.path.insert(0, f'{os.path.dirname(__file__)}/mojo/public/tools/bindings')
+
 import mojo.public.tools.bindings.mojom_bindings_generator as generator
 
 def _GetModulePath(path, output_dir):
-  return os.path.join(output_dir, path.relative_path())
+    return os.path.join(output_dir, path.relative_path())
+
+
+# Disable the attribute checker to support our custom attributes. Ideally we
+# should add the attributes to the list of allowed attributes in
+# utils/ipc/mojo/public/tools/bindings/checks/mojom_attributes_check.py, but
+# we're trying hard to use the upstream mojom as-is.
+if hasattr(generator, '_BUILTIN_CHECKS'):
+    del generator._BUILTIN_CHECKS['attributes']
 
 # Override the mojo code generator's generator list to only contain our
 # libcamera generator
diff --git a/utils/ipc/generators/libcamera_templates/core_ipa_interface.h.tmpl b/utils/ipc/generators/libcamera_templates/core_ipa_interface.h.tmpl
index c60b99b..7f2d081 100644
--- a/utils/ipc/generators/libcamera_templates/core_ipa_interface.h.tmpl
+++ b/utils/ipc/generators/libcamera_templates/core_ipa_interface.h.tmpl
@@ -7,7 +7,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * core_ipa_interface.h - libcamera core definitions for Image Processing Algorithms
+ * libcamera core definitions for Image Processing Algorithms
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/utils/ipc/generators/libcamera_templates/core_ipa_serializer.h.tmpl b/utils/ipc/generators/libcamera_templates/core_ipa_serializer.h.tmpl
index 5738a1a..036518f 100644
--- a/utils/ipc/generators/libcamera_templates/core_ipa_serializer.h.tmpl
+++ b/utils/ipc/generators/libcamera_templates/core_ipa_serializer.h.tmpl
@@ -8,7 +8,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * core_ipa_serializer.h - Data serializer for core libcamera definitions for IPA
+ * Data serializer for core libcamera definitions for IPA
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/utils/ipc/generators/libcamera_templates/module_ipa_interface.h.tmpl b/utils/ipc/generators/libcamera_templates/module_ipa_interface.h.tmpl
index 160601f..4d88a3d 100644
--- a/utils/ipc/generators/libcamera_templates/module_ipa_interface.h.tmpl
+++ b/utils/ipc/generators/libcamera_templates/module_ipa_interface.h.tmpl
@@ -7,7 +7,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * {{module_name}}_ipa_interface.h - Image Processing Algorithm interface for {{module_name}}
+ * Image Processing Algorithm interface for {{module_name}}
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/utils/ipc/generators/libcamera_templates/module_ipa_proxy.cpp.tmpl b/utils/ipc/generators/libcamera_templates/module_ipa_proxy.cpp.tmpl
index c37c494..ce3cc5a 100644
--- a/utils/ipc/generators/libcamera_templates/module_ipa_proxy.cpp.tmpl
+++ b/utils/ipc/generators/libcamera_templates/module_ipa_proxy.cpp.tmpl
@@ -8,7 +8,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * {{module_name}}_ipa_proxy.cpp - Image Processing Algorithm proxy for {{module_name}}
+ * Image Processing Algorithm proxy for {{module_name}}
  *
  * This file is auto-generated. Do not edit.
  */
@@ -175,9 +175,9 @@
 );
 {% elif method|is_async %}
 	ASSERT(state_ == ProxyRunning);
-	proxy_.invokeMethod(&ThreadProxy::{{method.mojom_name}}, ConnectionTypeQueued,
+	proxy_.invokeMethod(&ThreadProxy::{{method.mojom_name}}, ConnectionTypeQueued
 	{%- for param in method|method_param_names -%}
-		{{param}}{{- ", " if not loop.last}}
+		, {{param}}
 	{%- endfor -%}
 );
 {%- endif %}
@@ -235,8 +235,8 @@
 }
 
 void {{proxy_name}}::{{method.mojom_name}}IPC(
-	std::vector<uint8_t>::const_iterator data,
-	size_t dataSize,
+	[[maybe_unused]] std::vector<uint8_t>::const_iterator data,
+	[[maybe_unused]] size_t dataSize,
 	[[maybe_unused]] const std::vector<SharedFD> &fds)
 {
 {%- for param in method.parameters %}
diff --git a/utils/ipc/generators/libcamera_templates/module_ipa_proxy.h.tmpl b/utils/ipc/generators/libcamera_templates/module_ipa_proxy.h.tmpl
index ed270f5..e213b18 100644
--- a/utils/ipc/generators/libcamera_templates/module_ipa_proxy.h.tmpl
+++ b/utils/ipc/generators/libcamera_templates/module_ipa_proxy.h.tmpl
@@ -8,7 +8,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * {{module_name}}_ipa_proxy.h - Image Processing Algorithm proxy for {{module_name}}
+ * Image Processing Algorithm proxy for {{module_name}}
  *
  * This file is auto-generated. Do not edit.
  */
@@ -18,6 +18,7 @@
 #include <libcamera/ipa/ipa_interface.h>
 #include <libcamera/ipa/{{module_name}}_ipa_interface.h>
 
+#include <libcamera/base/object.h>
 #include <libcamera/base/thread.h>
 
 #include "libcamera/internal/control_serializer.h"
diff --git a/utils/ipc/generators/libcamera_templates/module_ipa_proxy_worker.cpp.tmpl b/utils/ipc/generators/libcamera_templates/module_ipa_proxy_worker.cpp.tmpl
index b65dc4c..1f990d3 100644
--- a/utils/ipc/generators/libcamera_templates/module_ipa_proxy_worker.cpp.tmpl
+++ b/utils/ipc/generators/libcamera_templates/module_ipa_proxy_worker.cpp.tmpl
@@ -8,7 +8,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * {{module_name}}_ipa_proxy_worker.cpp - Image Processing Algorithm proxy worker for {{module_name}}
+ * Image Processing Algorithm proxy worker for {{module_name}}
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/utils/ipc/generators/libcamera_templates/module_ipa_serializer.h.tmpl b/utils/ipc/generators/libcamera_templates/module_ipa_serializer.h.tmpl
index 8b70970..cd5a65a 100644
--- a/utils/ipc/generators/libcamera_templates/module_ipa_serializer.h.tmpl
+++ b/utils/ipc/generators/libcamera_templates/module_ipa_serializer.h.tmpl
@@ -8,7 +8,7 @@
 /*
  * Copyright (C) 2020, Google Inc.
  *
- * {{module_name}}_ipa_serializer.h - Image Processing Algorithm data serializer for {{module_name}}
+ * Image Processing Algorithm data serializer for {{module_name}}
  *
  * This file is auto-generated. Do not edit.
  */
diff --git a/utils/ipc/generators/libcamera_templates/proxy_functions.tmpl b/utils/ipc/generators/libcamera_templates/proxy_functions.tmpl
index 2be65d4..b5797b1 100644
--- a/utils/ipc/generators/libcamera_templates/proxy_functions.tmpl
+++ b/utils/ipc/generators/libcamera_templates/proxy_functions.tmpl
@@ -186,7 +186,7 @@
 {% for param in params|with_fds %}
 {%- if loop.first %}
 	const size_t {{param.mojom_name}}FdStart = 0;
-{%- elif not loop.last %}
+{%- else %}
 	const size_t {{param.mojom_name}}FdStart = {{loop.previtem.mojom_name}}FdStart + {{loop.previtem.mojom_name}}FdsSize;
 {%- endif %}
 {%- endfor %}
diff --git a/utils/ipc/generators/mojom_libcamera_generator.py b/utils/ipc/generators/mojom_libcamera_generator.py
index 1a629f9..b8209e5 100644
--- a/utils/ipc/generators/mojom_libcamera_generator.py
+++ b/utils/ipc/generators/mojom_libcamera_generator.py
@@ -4,7 +4,7 @@
 #
 # Author: Paul Elder <paul.elder@ideasonboard.com>
 #
-# mojom_libcamera_generator.py - Generates libcamera files from a mojom.Module.
+# Generates libcamera files from a mojom.Module.
 
 import argparse
 import datetime
@@ -72,7 +72,7 @@
 def GetDefaultValue(element):
     if element.default is not None:
         return element.default
-    if type(element.kind) == mojom.Kind:
+    if type(element.kind) == mojom.ValueKind:
         return '0'
     if IsFlags(element):
         return ''
@@ -369,7 +369,7 @@
     if namespace == '':
         raise Exception('Must have a namespace')
 
-    if not re.match('^ipa\.[0-9A-Za-z_]+', namespace):
+    if not re.match(r'^ipa\.[0-9A-Za-z_]+', namespace):
         raise Exception('Namespace must be of the form "ipa.{pipeline_name}"')
 
 def ValidateInterfaces(interfaces):
diff --git a/utils/ipc/mojo/README b/utils/ipc/mojo/README
index d5c24fc..961cabd 100644
--- a/utils/ipc/mojo/README
+++ b/utils/ipc/mojo/README
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: CC0-1.0
 
-Files in this directory are imported from 9c138d992bfc of Chromium. Do not
+Files in this directory are imported from 9be4263648d7 of Chromium. Do not
 modify them manually.
diff --git a/utils/ipc/mojo/public/LICENSE b/utils/ipc/mojo/public/LICENSE
index 972bb2e..513e8a6 100644
--- a/utils/ipc/mojo/public/LICENSE
+++ b/utils/ipc/mojo/public/LICENSE
@@ -1,4 +1,4 @@
-// Copyright 2014 The Chromium Authors. All rights reserved.
+// Copyright 2014 The Chromium Authors
 //
 // Redistribution and use in source and binary forms, with or without
 // modification, are permitted provided that the following conditions are
diff --git a/utils/ipc/mojo/public/tools/BUILD.gn b/utils/ipc/mojo/public/tools/BUILD.gn
index eb6391a..5328a34 100644
--- a/utils/ipc/mojo/public/tools/BUILD.gn
+++ b/utils/ipc/mojo/public/tools/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -10,7 +10,11 @@
     "run_all_python_unittests.py",
     "//testing/scripts/run_isolated_script_test.py",
   ]
-  deps = [ "//mojo/public/tools/mojom/mojom:tests" ]
+  deps = [
+    "//mojo/public/tools/bindings:tests",
+    "//mojo/public/tools/mojom:tests",
+    "//mojo/public/tools/mojom/mojom:tests",
+  ]
   data_deps = [
     "//testing:test_scripts_shared",
     "//third_party/catapult/third_party/typ/",
diff --git a/utils/ipc/mojo/public/tools/bindings/BUILD.gn b/utils/ipc/mojo/public/tools/bindings/BUILD.gn
index 3e24253..eeca73e 100644
--- a/utils/ipc/mojo/public/tools/bindings/BUILD.gn
+++ b/utils/ipc/mojo/public/tools/bindings/BUILD.gn
@@ -1,24 +1,27 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
+# Copyright 2016 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import("//build/config/python.gni")
 import("//mojo/public/tools/bindings/mojom.gni")
 import("//third_party/jinja2/jinja2.gni")
 
-# TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-python2_action("precompile_templates") {
+action("precompile_templates") {
   sources = mojom_generator_sources
   sources += [
+    "$mojom_generator_root/generators/cpp_templates/cpp_macros.tmpl",
     "$mojom_generator_root/generators/cpp_templates/enum_macros.tmpl",
     "$mojom_generator_root/generators/cpp_templates/enum_serialization_declaration.tmpl",
+    "$mojom_generator_root/generators/cpp_templates/feature_declaration.tmpl",
+    "$mojom_generator_root/generators/cpp_templates/feature_definition.tmpl",
     "$mojom_generator_root/generators/cpp_templates/interface_declaration.tmpl",
     "$mojom_generator_root/generators/cpp_templates/interface_definition.tmpl",
+    "$mojom_generator_root/generators/cpp_templates/interface_feature_declaration.tmpl",
     "$mojom_generator_root/generators/cpp_templates/interface_macros.tmpl",
     "$mojom_generator_root/generators/cpp_templates/interface_proxy_declaration.tmpl",
     "$mojom_generator_root/generators/cpp_templates/interface_request_validator_declaration.tmpl",
     "$mojom_generator_root/generators/cpp_templates/interface_response_validator_declaration.tmpl",
     "$mojom_generator_root/generators/cpp_templates/interface_stub_declaration.tmpl",
+    "$mojom_generator_root/generators/cpp_templates/module-features.h.tmpl",
     "$mojom_generator_root/generators/cpp_templates/module-forward.h.tmpl",
     "$mojom_generator_root/generators/cpp_templates/module-import-headers.h.tmpl",
     "$mojom_generator_root/generators/cpp_templates/module-params-data.h.tmpl",
@@ -26,7 +29,6 @@
     "$mojom_generator_root/generators/cpp_templates/module-shared-message-ids.h.tmpl",
     "$mojom_generator_root/generators/cpp_templates/module-shared.cc.tmpl",
     "$mojom_generator_root/generators/cpp_templates/module-shared.h.tmpl",
-    "$mojom_generator_root/generators/cpp_templates/module-test-utils.cc.tmpl",
     "$mojom_generator_root/generators/cpp_templates/module-test-utils.h.tmpl",
     "$mojom_generator_root/generators/cpp_templates/module.cc.tmpl",
     "$mojom_generator_root/generators/cpp_templates/module.h.tmpl",
@@ -65,9 +67,6 @@
     "$mojom_generator_root/generators/java_templates/struct.java.tmpl",
     "$mojom_generator_root/generators/java_templates/union.java.tmpl",
     "$mojom_generator_root/generators/js_templates/enum_definition.tmpl",
-    "$mojom_generator_root/generators/js_templates/externs/interface_definition.tmpl",
-    "$mojom_generator_root/generators/js_templates/externs/module.externs.tmpl",
-    "$mojom_generator_root/generators/js_templates/externs/struct_definition.tmpl",
     "$mojom_generator_root/generators/js_templates/fuzzing.tmpl",
     "$mojom_generator_root/generators/js_templates/interface_definition.tmpl",
     "$mojom_generator_root/generators/js_templates/lite/enum_definition.tmpl",
@@ -93,8 +92,11 @@
     "$mojom_generator_root/generators/mojolpm_templates/mojolpm_macros.tmpl",
     "$mojom_generator_root/generators/mojolpm_templates/mojolpm_to_proto_macros.tmpl",
     "$mojom_generator_root/generators/mojolpm_templates/mojolpm_traits_specialization_macros.tmpl",
+    "$mojom_generator_root/generators/ts_templates/enum_definition.tmpl",
+    "$mojom_generator_root/generators/ts_templates/interface_definition.tmpl",
     "$mojom_generator_root/generators/ts_templates/module_definition.tmpl",
-    "$mojom_generator_root/generators/ts_templates/mojom.tmpl",
+    "$mojom_generator_root/generators/ts_templates/struct_definition.tmpl",
+    "$mojom_generator_root/generators/ts_templates/union_definition.tmpl",
   ]
   script = mojom_generator_script
 
@@ -102,8 +104,8 @@
   outputs = [
     "$target_gen_dir/cpp_templates.zip",
     "$target_gen_dir/java_templates.zip",
-    "$target_gen_dir/mojolpm_templates.zip",
     "$target_gen_dir/js_templates.zip",
+    "$target_gen_dir/mojolpm_templates.zip",
     "$target_gen_dir/ts_templates.zip",
   ]
   args = [
@@ -113,3 +115,17 @@
     "precompile",
   ]
 }
+
+group("tests") {
+  data = [
+    mojom_generator_script,
+    "checks/mojom_attributes_check_unittest.py",
+    "checks/mojom_interface_feature_check_unittest.py",
+    "checks/mojom_restrictions_checks_unittest.py",
+    "mojom_bindings_generator_unittest.py",
+    "//tools/diagnosis/crbug_1001171.py",
+    "//third_party/markupsafe/",
+  ]
+  data += mojom_generator_sources
+  data += jinja2_sources
+}
diff --git a/utils/ipc/mojo/public/tools/bindings/README.md b/utils/ipc/mojo/public/tools/bindings/README.md
index 4388245..b27b2d0 100644
--- a/utils/ipc/mojo/public/tools/bindings/README.md
+++ b/utils/ipc/mojo/public/tools/bindings/README.md
@@ -96,7 +96,7 @@
 | `string`                      | UTF-8 encoded string.
 | `array<T>`                    | Array of any Mojom type *T*; for example, `array<uint8>` or `array<array<string>>`.
 | `array<T, N>`                 | Fixed-length array of any Mojom type *T*. The parameter *N* must be an integral constant.
-| `map<S, T>`                   | Associated array maping values of type *S* to values of type *T*. *S* may be a `string`, `enum`, or numeric type.
+| `map<S, T>`                   | Associated array mapping values of type *S* to values of type *T*. *S* may be a `string`, `enum`, or numeric type.
 | `handle`                      | Generic Mojo handle. May be any type of handle, including a wrapped native platform handle.
 | `handle<message_pipe>`        | Generic message pipe handle.
 | `handle<shared_buffer>`       | Shared buffer handle.
@@ -188,8 +188,8 @@
 };
 
 enum AnEnum {
-  YES,
-  NO
+  kYes,
+  kNo
 };
 
 interface SampleInterface {
@@ -209,7 +209,7 @@
   uint64 unsigned_64bit_value;
   float float_value_32bit;
   double float_value_64bit;
-  AnEnum enum_value = AnEnum.YES;
+  AnEnum enum_value = AnEnum.kYes;
 
   // Strings may be nullable.
   string? maybe_a_string_maybe_not;
@@ -300,14 +300,14 @@
 module business.mojom;
 
 enum Department {
-  SALES = 0,
-  DEV,
+  kSales = 0,
+  kDev,
 };
 
 struct Employee {
   enum Type {
-    FULL_TIME,
-    PART_TIME,
+    kFullTime,
+    kPartTime,
   };
 
   Type type;
@@ -315,6 +315,9 @@
 };
 ```
 
+C++ constant-style enum value names are preferred as specified in the
+[Google C++ Style Guide](https://google.github.io/styleguide/cppguide.html#Enumerator_Names).
+
 Similar to C-style enums, individual values may be explicitly assigned within an
 enum definition. By default, values are based at zero and increment by
 1 sequentially.
@@ -336,8 +339,8 @@
   const uint64 kInvalidId = 0;
 
   enum Type {
-    FULL_TIME,
-    PART_TIME,
+    kFullTime,
+    kPartTime,
   };
 
   uint64 id = kInvalidId;
@@ -348,6 +351,37 @@
 The effect of nested definitions on generated bindings varies depending on the
 target language. See [documentation for individual target languages](#Generated-Code-For-Target-Languages).
 
+### Features
+
+Features can be declared with a `name` and `default_state` and can be attached
+in mojo to interfaces or methods using the `RuntimeFeature` attribute. If the
+feature is disabled at runtime, the method will crash and the interface will
+refuse to be bound / instantiated. Features cannot be serialized to be sent over
+IPC at this time.
+
+```
+module experimental.mojom;
+
+feature kUseElevators {
+  const string name = "UseElevators";
+  const bool default_state = false;
+}
+
+[RuntimeFeature=kUseElevators]
+interface Elevator {
+  // This interface cannot be bound or called if the feature is disabled.
+}
+
+interface Building {
+  // This method cannot be called if the feature is disabled.
+  [RuntimeFeature=kUseElevators]
+  CallElevator(int floor);
+
+  // This method can be called.
+  RingDoorbell(int volume);
+}
+```
+
 ### Interfaces
 
 An **interface** is a logical bundle of parameterized request messages. Each
@@ -396,20 +430,33 @@
   extreme caution, because it can lead to deadlocks otherwise.
 
 * **`[Default]`**:
-  The `Default` attribute may be used to specify an enumerator value that
-  will be used if an `Extensible` enumeration does not deserialize to a known
-  value on the receiver side, i.e. the sender is using a newer version of the
-  enum. This allows unknown values to be mapped to a well-defined value that can
-  be appropriately handled.
+  The `Default` attribute may be used to specify an enumerator value or union
+  field that will be used if an `Extensible` enumeration or union does not
+  deserialize to a known value on the receiver side, i.e. the sender is using a
+  newer version of the enum or union. This allows unknown values to be mapped to
+  a well-defined value that can be appropriately handled.
+
+  Note: The `Default` field for a union must be of nullable or integral type.
+  When a union is defaulted to this field, the field takes on the default value
+  for its type: null for nullable types, and zero/false for integral types.
 
 * **`[Extensible]`**:
-  The `Extensible` attribute may be specified for any enum definition. This
-  essentially disables builtin range validation when receiving values of the
-  enum type in a message, allowing older bindings to tolerate unrecognized
-  values from newer versions of the enum.
+  The `Extensible` attribute may be specified for any enum or union definition.
+  For enums, this essentially disables builtin range validation when receiving
+  values of the enum type in a message, allowing older bindings to tolerate
+  unrecognized values from newer versions of the enum.
 
-  Note: in the future, an `Extensible` enumeration will require that a `Default`
-  enumerator value also be specified.
+  If an enum value within an extensible enum definition is affixed with the
+  `Default` attribute, out-of-range values for the enum will deserialize to that
+  default value. Only one enum value may be designated as the `Default`.
+
+  Similarly, a union marked `Extensible` will deserialize to its `Default` field
+  when an unrecognized field is received. Extensible unions MUST specify exactly
+  one `Default` field, and the field must be of nullable or integral type. When
+  defaulted to this field, the value is always null/zero/false as appropriate.
+
+  An `Extensible` enumeration REQUIRES that a `Default` value be specified,
+  so all new extensible enums should specify one.
 
 * **`[Native]`**:
   The `Native` attribute may be specified for an empty struct declaration to
@@ -422,7 +469,10 @@
 * **`[MinVersion=N]`**:
   The `MinVersion` attribute is used to specify the version at which a given
   field, enum value, interface method, or method parameter was introduced.
-  See [Versioning](#Versioning) for more details.
+  See [Versioning](#Versioning) for more details. `MinVersion` does not apply
+  to interfaces, structs or enums, but to the fields of those types.
+  `MinVersion` is not a module-global value, but it is ok to pretend it is by
+  skipping versions when adding fields or parameters.
 
 * **`[Stable]`**:
   The `Stable` attribute specifies that a given mojom type or interface
@@ -442,13 +492,73 @@
   string representation as specified by RFC 4122. New UUIDs can be generated
   with common tools such as `uuidgen`.
 
+* **`[RuntimeFeature=feature]`**
+  The `RuntimeFeature` attribute should reference a mojo `feature`. If this
+  feature is enabled (e.g. using `--enable-features={feature.name}`) then the
+  interface behaves entirely as expected. If the feature is not enabled the
+  interface cannot be bound to a concrete receiver or remote - attempting to do
+  so will result in the receiver or remote being reset() to an unbound state.
+  Note that this is a different concept to the build-time `EnableIf` directive.
+  `RuntimeFeature` is currently only supported for C++ bindings and has no
+  effect for, say, Java or TypeScript bindings (see https://crbug.com/1278253).
+
 * **`[EnableIf=value]`**:
   The `EnableIf` attribute is used to conditionally enable definitions when the
   mojom is parsed. If the `mojom` target in the GN file does not include the
   matching `value` in the list of `enabled_features`, the definition will be
   disabled. This is useful for mojom definitions that only make sense on one
   platform. Note that the `EnableIf` attribute can only be set once per
-  definition.
+  definition and cannot be set at the same time as `EnableIfNot`. Also be aware
+  that only one condition can be tested, `EnableIf=value,xyz` introduces a new
+  `xyz` attribute. `xyz` is not part of the `EnableIf` condition that depends
+  only on the feature `value`. Complex conditions can be introduced via
+  enabled_features in `build.gn` files.
+
+* **`[EnableIfNot=value]`**:
+  The `EnableIfNot` attribute is used to conditionally enable definitions when
+  the mojom is parsed. If the `mojom` target in the GN file includes the
+  matching `value` in the list of `enabled_features`, the definition will be
+  disabled. This is useful for mojom definitions that only make sense on all but
+  one platform. Note that the `EnableIfNot` attribute can only be set once per
+  definition and cannot be set at the same time as `EnableIf`.
+
+* **`[ServiceSandbox=value]`**:
+  The `ServiceSandbox` attribute is used in Chromium to tag which sandbox a
+  service hosting an implementation of interface will be launched in. This only
+  applies to `C++` bindings. `value` should match a constant defined in an
+  imported `sandbox.mojom.Sandbox` enum (for Chromium this is
+  `//sandbox/policy/mojom/sandbox.mojom`), such as `kService`.
+
+* **`[RequireContext=enum]`**:
+  The `RequireContext` attribute is used in Chromium to tag interfaces that
+  should be passed (as remotes or receivers) only to privileged process
+  contexts. The process context must be an enum that is imported into the
+  mojom that defines the tagged interface. `RequireContext` may be used in
+  future to DCHECK or CHECK if remotes are made available in contexts that
+  conflict with the one provided in the interface definition. Process contexts
+  are not the same as the sandbox a process is running in, but will reflect
+  the set of capabilities provided to the service.
+
+* **`[AllowedContext=enum]`**:
+  The `AllowedContext` attribute is used in Chromium to tag methods that pass
+  remotes or receivers of interfaces that are marked with a `RequireContext`
+  attribute. The enum provided on the method must be equal or better (lower
+  numerically) than the one required on the interface being passed. At present
+  failing to specify an adequate `AllowedContext` value will cause mojom
+  generation to fail at compile time. In future DCHECKs or CHECKs might be
+  added to enforce that method is only called from a process context that meets
+  the given `AllowedContext` value. The enum must of the same type as that
+  specified in the interface's `RequireContext` attribute. Adding an
+  `AllowedContext` attribute to a method is a strong indication that you need
+   a detailed security review of your design - please reach out to the security
+   team.
+
+* **`[SupportsUrgent]`**:
+  The `SupportsUrgent` attribute is used in conjunction with
+  `mojo::UrgentMessageScope` in Chromium to tag messages as having high
+  priority. The IPC layer notifies the underlying scheduler upon both receiving
+  and processing an urgent message. At present, this attribute only affects
+  channel associated messages in the renderer process.
 
 ## Generated Code For Target Languages
 
@@ -495,9 +605,9 @@
 
 ``` cpp
 enum AdvancedBoolean {
-  TRUE = 0,
-  FALSE = 1,
-  FILE_NOT_FOUND = 2,
+  kTrue = 0,
+  kFalse = 1,
+  kFileNotFound = 2,
 };
 ```
 
@@ -550,10 +660,16 @@
 
 *** note
 **NOTE:** You don't need to worry about versioning if you don't care about
-backwards compatibility. Specifically, all parts of Chrome are updated
-atomically today and there is not yet any possibility of any two Chrome
-processes communicating with two different versions of any given Mojom
-interface.
+backwards compatibility. Today, all parts of the Chrome browser are
+updated atomically and there is not yet any possibility of any two
+Chrome processes communicating with two different versions of any given Mojom
+interface. On Chrome OS, there are several places where versioning is required.
+For example,
+[ARC++](https://developer.android.com/chrome-os/intro)
+uses versioned mojo to send IPC to the Android container.
+Likewise, the
+[Lacros](/docs/lacros.md)
+browser uses versioned mojo to talk to the ash system UI.
 ***
 
 Services extend their interfaces to support new features over time, and clients
@@ -593,8 +709,8 @@
 
 *** note
 **NOTE:** Mojo object or handle types added with a `MinVersion` **MUST** be
-optional (nullable). See [Primitive Types](#Primitive-Types) for details on
-nullable values.
+optional (nullable) or primitive. See [Primitive Types](#Primitive-Types) for
+details on nullable values.
 ***
 
 By default, fields belong to version 0. New fields must be appended to the
@@ -624,10 +740,10 @@
 * For any given struct or interface, if any field or method explicitly specifies
     an ordinal value, all fields or methods must explicitly specify an ordinal
     value.
-* For an *N*-field struct or *N*-method interface, the set of explicitly
-    assigned ordinal values must be limited to the range *[0, N-1]*. Interfaces
-    should include placeholder methods to fill the ordinal positions of removed
-    methods (for example "Unused_Message_7@7()" or "RemovedMessage@42()", etc).
+* For an *N*-field struct, the set of explicitly assigned ordinal values must be
+    limited to the range *[0, N-1]*. Structs should include placeholder fields
+    to fill the ordinal positions of removed fields (for example "Unused_Field"
+    or "RemovedField", etc).
 
 You may reorder fields, but you must ensure that the ordinal values of existing
 fields remain unchanged. For example, the following struct remains
@@ -652,6 +768,24 @@
     that the version number is scoped to the whole interface rather than to any
     individual parameter list.
 
+``` cpp
+// Old version:
+interface HumanResourceDatabase {
+  QueryEmployee(uint64 id) => (Employee? employee);
+};
+
+// New version:
+interface HumanResourceDatabase {
+  QueryEmployee(uint64 id, [MinVersion=1] bool retrieve_finger_print)
+      => (Employee? employee,
+          [MinVersion=1] array<uint8>? finger_print);
+};
+```
+
+Similar to [versioned structs](#Versioned-Structs), when you pass the parameter
+list of a request or response method to a destination using an older version of
+an interface, unrecognized fields are silently discarded.
+
     Please note that adding a response to a message which did not previously
     expect a response is a not a backwards-compatible change.
 
@@ -664,17 +798,12 @@
 ``` cpp
 // Old version:
 interface HumanResourceDatabase {
-  AddEmployee(Employee employee) => (bool success);
   QueryEmployee(uint64 id) => (Employee? employee);
 };
 
 // New version:
 interface HumanResourceDatabase {
-  AddEmployee(Employee employee) => (bool success);
-
-  QueryEmployee(uint64 id, [MinVersion=1] bool retrieve_finger_print)
-      => (Employee? employee,
-          [MinVersion=1] array<uint8>? finger_print);
+  QueryEmployee(uint64 id) => (Employee? employee);
 
   [MinVersion=1]
   AttachFingerPrint(uint64 id, array<uint8> finger_print)
@@ -682,10 +811,7 @@
 };
 ```
 
-Similar to [versioned structs](#Versioned-Structs), when you pass the parameter
-list of a request or response method to a destination using an older version of
-an interface, unrecognized fields are silently discarded. However, if the method
-call itself is not recognized, it is considered a validation error and the
+If a method call is not recognized, it is considered a validation error and the
 receiver will close its end of the interface pipe. For example, if a client on
 version 1 of the above interface sends an `AttachFingerPrint` request to an
 implementation of version 0, the client will be disconnected.
@@ -712,8 +838,8 @@
 ``` cpp
 [Extensible]
 enum Department {
-  SALES,
-  DEV,
+  kSales,
+  kDev,
 };
 ```
 
@@ -722,9 +848,9 @@
 ``` cpp
 [Extensible]
 enum Department {
-  SALES,
-  DEV,
-  [MinVersion=1] RESEARCH,
+  kSales,
+  kDev,
+  [MinVersion=1] kResearch,
 };
 ```
 
@@ -782,7 +908,7 @@
 
 ModuleStatement = AttributeSection "module" Identifier ";"
 ImportStatement = "import" StringLiteral ";"
-Definition = Struct Union Interface Enum Const
+Definition = Struct Union Interface Enum Feature Const
 
 AttributeSection = <empty> | "[" AttributeList "]"
 AttributeList = <empty> | NonEmptyAttributeList
@@ -809,7 +935,7 @@
               | InterfaceBody Const
               | InterfaceBody Enum
               | InterfaceBody Method
-Method = AttributeSection Name Ordinal "(" ParamterList ")" Response ";"
+Method = AttributeSection Name Ordinal "(" ParameterList ")" Response ";"
 ParameterList = <empty> | NonEmptyParameterList
 NonEmptyParameterList = Parameter
                       | Parameter "," NonEmptyParameterList
@@ -847,6 +973,13 @@
           | AttributeSection Name "=" Integer
           | AttributeSection Name "=" Identifier
 
+; Note: `feature` is a weak keyword and can appear as, say, a struct field name.
+Feature = AttributeSection "feature" Name "{" FeatureBody "}" ";"
+       | AttributeSection "feature" Name ";"
+FeatureBody = <empty>
+           | FeatureBody FeatureField
+FeatureField = AttributeSection TypeSpec Name Default ";"
+
 Const = "const" TypeSpec Name "=" Constant ";"
 
 Constant = Literal | Identifier ";"
diff --git a/src/libcamera/formats.c b/utils/ipc/mojo/public/tools/bindings/checks/__init__.py
similarity index 100%
rename from src/libcamera/formats.c
rename to utils/ipc/mojo/public/tools/bindings/checks/__init__.py
diff --git a/utils/ipc/mojo/public/tools/bindings/checks/mojom_attributes_check.py b/utils/ipc/mojo/public/tools/bindings/checks/mojom_attributes_check.py
new file mode 100644
index 0000000..e6e4f2c
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/bindings/checks/mojom_attributes_check.py
@@ -0,0 +1,170 @@
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Validate mojo attributes are allowed in Chrome before generation."""
+
+import mojom.generate.check as check
+import mojom.generate.module as module
+
+_COMMON_ATTRIBUTES = {
+    'EnableIf',
+    'EnableIfNot',
+}
+
+# For struct, union & parameter lists.
+_COMMON_FIELD_ATTRIBUTES = _COMMON_ATTRIBUTES | {
+    'MinVersion',
+    'RenamedFrom',
+}
+
+# Note: `Default`` goes on the default _value_, not on the enum.
+# Note: [Stable] without [Extensible] is not allowed.
+_ENUM_ATTRIBUTES = _COMMON_ATTRIBUTES | {
+    'Extensible',
+    'Native',
+    'Stable',
+    'RenamedFrom',
+    'Uuid',
+}
+
+# TODO(crbug.com/1234883) MinVersion is not needed for EnumVal.
+_ENUMVAL_ATTRIBUTES = _COMMON_ATTRIBUTES | {
+    'Default',
+    'MinVersion',
+}
+
+_INTERFACE_ATTRIBUTES = _COMMON_ATTRIBUTES | {
+    'RenamedFrom',
+    'RequireContext',
+    'RuntimeFeature',
+    'ServiceSandbox',
+    'Stable',
+    'Uuid',
+}
+
+_METHOD_ATTRIBUTES = _COMMON_ATTRIBUTES | {
+    'AllowedContext',
+    'MinVersion',
+    'NoInterrupt',
+    'RuntimeFeature',
+    'SupportsUrgent',
+    'Sync',
+    'UnlimitedSize',
+}
+
+_MODULE_ATTRIBUTES = _COMMON_ATTRIBUTES | {
+    'JavaConstantsClassName',
+    'JavaPackage',
+}
+
+_PARAMETER_ATTRIBUTES = _COMMON_FIELD_ATTRIBUTES
+
+_STRUCT_ATTRIBUTES = _COMMON_ATTRIBUTES | {
+    'CustomSerializer',
+    'JavaClassName',
+    'Native',
+    'Stable',
+    'RenamedFrom',
+    'Uuid',
+}
+
+_STRUCT_FIELD_ATTRIBUTES = _COMMON_FIELD_ATTRIBUTES
+
+_UNION_ATTRIBUTES = _COMMON_ATTRIBUTES | {
+    'Extensible',
+    'Stable',
+    'RenamedFrom',
+    'Uuid',
+}
+
+_UNION_FIELD_ATTRIBUTES = _COMMON_FIELD_ATTRIBUTES | {
+    'Default',
+}
+
+# TODO(https://crbug.com/1193875) empty this set and remove the allowlist.
+_STABLE_ONLY_ALLOWLISTED_ENUMS = {
+    'crosapi.mojom.OptionalBool',
+    'crosapi.mojom.TriState',
+}
+
+
+class Check(check.Check):
+  def __init__(self, *args, **kwargs):
+    super(Check, self).__init__(*args, **kwargs)
+
+  def _Respell(self, allowed, attribute):
+    for a in allowed:
+      if a.lower() == attribute.lower():
+        return f" - Did you mean: {a}?"
+    return ""
+
+  def _CheckAttributes(self, context, allowed, attributes):
+    if not attributes:
+      return
+    for attribute in attributes:
+      if not attribute in allowed:
+        # Is there a close misspelling?
+        hint = self._Respell(allowed, attribute)
+        raise check.CheckException(
+            self.module,
+            f"attribute {attribute} not allowed on {context}{hint}")
+
+  def _CheckEnumAttributes(self, enum):
+    if enum.attributes:
+      self._CheckAttributes("enum", _ENUM_ATTRIBUTES, enum.attributes)
+      if 'Stable' in enum.attributes and not 'Extensible' in enum.attributes:
+        full_name = f"{self.module.mojom_namespace}.{enum.mojom_name}"
+        if full_name not in _STABLE_ONLY_ALLOWLISTED_ENUMS:
+          raise check.CheckException(
+              self.module,
+              f"[Extensible] required on [Stable] enum {full_name}")
+    for enumval in enum.fields:
+      self._CheckAttributes("enum value", _ENUMVAL_ATTRIBUTES,
+                            enumval.attributes)
+
+  def _CheckInterfaceAttributes(self, interface):
+    self._CheckAttributes("interface", _INTERFACE_ATTRIBUTES,
+                          interface.attributes)
+    for method in interface.methods:
+      self._CheckAttributes("method", _METHOD_ATTRIBUTES, method.attributes)
+      for param in method.parameters:
+        self._CheckAttributes("parameter", _PARAMETER_ATTRIBUTES,
+                              param.attributes)
+      if method.response_parameters:
+        for param in method.response_parameters:
+          self._CheckAttributes("parameter", _PARAMETER_ATTRIBUTES,
+                                param.attributes)
+    for enum in interface.enums:
+      self._CheckEnumAttributes(enum)
+
+  def _CheckModuleAttributes(self):
+    self._CheckAttributes("module", _MODULE_ATTRIBUTES, self.module.attributes)
+
+  def _CheckStructAttributes(self, struct):
+    self._CheckAttributes("struct", _STRUCT_ATTRIBUTES, struct.attributes)
+    for field in struct.fields:
+      self._CheckAttributes("struct field", _STRUCT_FIELD_ATTRIBUTES,
+                            field.attributes)
+    for enum in struct.enums:
+      self._CheckEnumAttributes(enum)
+
+  def _CheckUnionAttributes(self, union):
+    self._CheckAttributes("union", _UNION_ATTRIBUTES, union.attributes)
+    for field in union.fields:
+      self._CheckAttributes("union field", _UNION_FIELD_ATTRIBUTES,
+                            field.attributes)
+
+  def CheckModule(self):
+    """Note that duplicate attributes are forbidden at the parse phase.
+    We also do not need to look at the types of any parameters, as they will be
+    checked where they are defined. Consts do not have attributes so can be
+    skipped."""
+    self._CheckModuleAttributes()
+    for interface in self.module.interfaces:
+      self._CheckInterfaceAttributes(interface)
+    for enum in self.module.enums:
+      self._CheckEnumAttributes(enum)
+    for struct in self.module.structs:
+      self._CheckStructAttributes(struct)
+    for union in self.module.unions:
+      self._CheckUnionAttributes(union)
diff --git a/utils/ipc/mojo/public/tools/bindings/checks/mojom_attributes_check_unittest.py b/utils/ipc/mojo/public/tools/bindings/checks/mojom_attributes_check_unittest.py
new file mode 100644
index 0000000..f1a50a4
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/bindings/checks/mojom_attributes_check_unittest.py
@@ -0,0 +1,194 @@
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+
+import mojom.generate.check as check
+from mojom_bindings_generator import LoadChecks, _Generate
+from mojom_parser_test_case import MojomParserTestCase
+
+
+class FakeArgs:
+  """Fakes args to _Generate - intention is to do just enough to run checks"""
+
+  def __init__(self, tester, files=None):
+    """ `tester` is MojomParserTestCase for paths.
+        `files` will have tester path added."""
+    self.checks_string = 'attributes'
+    self.depth = tester.GetPath('')
+    self.filelist = None
+    self.filename = [tester.GetPath(x) for x in files]
+    self.gen_directories = tester.GetPath('gen')
+    self.generators_string = ''
+    self.import_directories = []
+    self.output_dir = tester.GetPath('out')
+    self.scrambled_message_id_salt_paths = None
+    self.typemaps = []
+    self.variant = 'none'
+
+
+class MojoBindingsCheckTest(MojomParserTestCase):
+  def _ParseAndGenerate(self, mojoms):
+    self.ParseMojoms(mojoms)
+    args = FakeArgs(self, files=mojoms)
+    _Generate(args, {})
+
+  def _testValid(self, filename, content):
+    self.WriteFile(filename, content)
+    self._ParseAndGenerate([filename])
+
+  def _testThrows(self, filename, content, regexp):
+    mojoms = []
+    self.WriteFile(filename, content)
+    mojoms.append(filename)
+    with self.assertRaisesRegexp(check.CheckException, regexp):
+      self._ParseAndGenerate(mojoms)
+
+  def testLoads(self):
+    """Validate that the check is registered under the expected name."""
+    check_modules = LoadChecks('attributes')
+    self.assertTrue(check_modules['attributes'])
+
+  def testNoAnnotations(self):
+    # Undecorated mojom should be fine.
+    self._testValid(
+        "a.mojom", """
+      module a;
+      struct Bar { int32 a; };
+      enum Hello { kValue };
+      union Thingy { Bar b; Hello hi; };
+      interface Foo {
+        Foo(int32 a, Hello hi, Thingy t) => (Bar b);
+      };
+    """)
+
+  def testValidAnnotations(self):
+    # Obviously this is meaningless and won't generate, but it should pass
+    # the attribute check's validation.
+    self._testValid(
+        "a.mojom", """
+      [JavaConstantsClassName="FakeClass",JavaPackage="org.chromium.Fake"]
+      module a;
+      [Stable, Extensible]
+      enum Hello { [Default] kValue, kValue2, [MinVersion=2] kValue3 };
+      [Native]
+      enum NativeEnum {};
+      [Stable,Extensible]
+      union Thingy { Bar b; [Default]int32 c; Hello hi; };
+
+      [Stable,RenamedFrom="module.other.Foo",
+       Uuid="4C178401-4B07-4C2E-9255-5401A943D0C7"]
+      struct Structure { Hello hi; };
+
+      [ServiceSandbox=Hello.kValue,RequireContext=Hello.kValue,Stable,
+       Uuid="2F17D7DD-865A-4B1C-9394-9C94E035E82F"]
+      interface Foo {
+        [AllowedContext=Hello.kValue]
+        Foo@0(int32 a) => (int32 b);
+        [MinVersion=2,Sync,UnlimitedSize,NoInterrupt]
+        Bar@1(int32 b, [MinVersion=2]Structure? s) => (bool c);
+      };
+
+      [RuntimeFeature=test.mojom.FeatureName]
+      interface FooFeatureControlled {};
+
+      interface FooMethodFeatureControlled {
+        [RuntimeFeature=test.mojom.FeatureName]
+        MethodWithFeature() => (bool c);
+      };
+    """)
+
+  def testWrongModuleStable(self):
+    contents = """
+      // err: module cannot be Stable
+      [Stable]
+      module a;
+      enum Hello { kValue, kValue2, kValue3 };
+      enum NativeEnum {};
+      struct Structure { Hello hi; };
+
+      interface Foo {
+        Foo(int32 a) => (int32 b);
+        Bar(int32 b, Structure? s) => (bool c);
+      };
+    """
+    self._testThrows('b.mojom', contents,
+                     'attribute Stable not allowed on module')
+
+  def testWrongEnumDefault(self):
+    contents = """
+      module a;
+      // err: default should go on EnumValue not Enum.
+      [Default=kValue]
+      enum Hello { kValue, kValue2, kValue3 };
+      enum NativeEnum {};
+      struct Structure { Hello hi; };
+
+      interface Foo {
+        Foo(int32 a) => (int32 b);
+        Bar(int32 b, Structure? s) => (bool c);
+      };
+    """
+    self._testThrows('b.mojom', contents,
+                     'attribute Default not allowed on enum')
+
+  def testWrongStructMinVersion(self):
+    contents = """
+      module a;
+      enum Hello { kValue, kValue2, kValue3 };
+      enum NativeEnum {};
+      // err: struct cannot have MinVersion.
+      [MinVersion=2]
+      struct Structure { Hello hi; };
+
+      interface Foo {
+        Foo(int32 a) => (int32 b);
+        Bar(int32 b, Structure? s) => (bool c);
+      };
+    """
+    self._testThrows('b.mojom', contents,
+                     'attribute MinVersion not allowed on struct')
+
+  def testWrongMethodRequireContext(self):
+    contents = """
+      module a;
+      enum Hello { kValue, kValue2, kValue3 };
+      enum NativeEnum {};
+      struct Structure { Hello hi; };
+
+      interface Foo {
+        // err: RequireContext is for interfaces.
+        [RequireContext=Hello.kValue]
+        Foo(int32 a) => (int32 b);
+        Bar(int32 b, Structure? s) => (bool c);
+      };
+    """
+    self._testThrows('b.mojom', contents,
+                     'RequireContext not allowed on method')
+
+  def testWrongMethodRequireContext(self):
+    # crbug.com/1230122
+    contents = """
+      module a;
+      interface Foo {
+        // err: sync not Sync.
+        [sync]
+        Foo(int32 a) => (int32 b);
+      };
+    """
+    self._testThrows('b.mojom', contents,
+                     'attribute sync not allowed.*Did you mean: Sync')
+
+  def testStableExtensibleEnum(self):
+    # crbug.com/1193875
+    contents = """
+      module a;
+      [Stable]
+      enum Foo {
+        kDefaultVal,
+        kOtherVal = 2,
+      };
+    """
+    self._testThrows('a.mojom', contents,
+                     'Extensible.*?required.*?Stable.*?enum')
diff --git a/utils/ipc/mojo/public/tools/bindings/checks/mojom_definitions_check.py b/utils/ipc/mojo/public/tools/bindings/checks/mojom_definitions_check.py
new file mode 100644
index 0000000..702d41c
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/bindings/checks/mojom_definitions_check.py
@@ -0,0 +1,34 @@
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Ensure no duplicate type definitions before generation."""
+
+import mojom.generate.check as check
+import mojom.generate.module as module
+
+
+class Check(check.Check):
+  def __init__(self, *args, **kwargs):
+    super(Check, self).__init__(*args, **kwargs)
+
+  def CheckModule(self):
+    kinds = dict()
+    for module in self.module.imports:
+      for kind in module.enums + module.structs + module.unions:
+        kind_name = f'{kind.module.mojom_namespace}.{kind.mojom_name}'
+        if kind_name in kinds:
+          previous_module = kinds[kind_name]
+          if previous_module.path != module.path:
+            raise check.CheckException(
+                self.module, f"multiple-definition for type {kind_name}" +
+                f"(defined in both {previous_module} and {module})")
+        kinds[kind_name] = kind.module
+
+    for kind in self.module.enums + self.module.structs + self.module.unions:
+      kind_name = f'{kind.module.mojom_namespace}.{kind.mojom_name}'
+      if kind_name in kinds:
+        previous_module = kinds[kind_name]
+        raise check.CheckException(
+            self.module, f"multiple-definition for type {kind_name}" +
+            f"(previous definition in {previous_module})")
+    return True
diff --git a/utils/ipc/mojo/public/tools/bindings/checks/mojom_interface_feature_check.py b/utils/ipc/mojo/public/tools/bindings/checks/mojom_interface_feature_check.py
new file mode 100644
index 0000000..07f51a6
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/bindings/checks/mojom_interface_feature_check.py
@@ -0,0 +1,62 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Validate mojo runtime feature guarded interfaces are nullable."""
+
+import mojom.generate.check as check
+import mojom.generate.module as module
+
+
+class Check(check.Check):
+  def __init__(self, *args, **kwargs):
+    super(Check, self).__init__(*args, **kwargs)
+
+  # `param` is an Interface of some sort.
+  def _CheckNonNullableFeatureGuardedInterface(self, kind):
+    # Only need to validate interface if it has a RuntimeFeature
+    if not kind.kind.runtime_feature:
+      return
+    # Nullable (optional) is ok as the interface expects they might not be sent.
+    if kind.is_nullable:
+      return
+    interface = kind.kind.mojom_name
+    raise check.CheckException(
+        self.module,
+        f"interface {interface} has a RuntimeFeature but is not nullable")
+
+  # `param` can be a lot of things so check if it is a remote/receiver.
+  # Array/Map must be recursed into.
+  def _CheckFieldOrParam(self, kind):
+    if module.IsAnyInterfaceKind(kind):
+      self._CheckNonNullableFeatureGuardedInterface(kind)
+    if module.IsArrayKind(kind):
+      self._CheckFieldOrParam(kind.kind)
+    if module.IsMapKind(kind):
+      self._CheckFieldOrParam(kind.key_kind)
+      self._CheckFieldOrParam(kind.value_kind)
+
+  def _CheckInterfaceFeatures(self, interface):
+    for method in interface.methods:
+      for param in method.parameters:
+        self._CheckFieldOrParam(param.kind)
+      if method.response_parameters:
+        for param in method.response_parameters:
+          self._CheckFieldOrParam(param.kind)
+
+  def _CheckStructFeatures(self, struct):
+    for field in struct.fields:
+      self._CheckFieldOrParam(field.kind)
+
+  def _CheckUnionFeatures(self, union):
+    for field in union.fields:
+      self._CheckFieldOrParam(field.kind)
+
+  def CheckModule(self):
+    """Validate that any runtime feature guarded interfaces that might be passed
+    over mojo are nullable."""
+    for interface in self.module.interfaces:
+      self._CheckInterfaceFeatures(interface)
+    for struct in self.module.structs:
+      self._CheckStructFeatures(struct)
+    for union in self.module.unions:
+      self._CheckUnionFeatures(union)
diff --git a/utils/ipc/mojo/public/tools/bindings/checks/mojom_interface_feature_check_unittest.py b/utils/ipc/mojo/public/tools/bindings/checks/mojom_interface_feature_check_unittest.py
new file mode 100644
index 0000000..e96152f
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/bindings/checks/mojom_interface_feature_check_unittest.py
@@ -0,0 +1,173 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+
+import mojom.generate.check as check
+from mojom_bindings_generator import LoadChecks, _Generate
+from mojom_parser_test_case import MojomParserTestCase
+
+
+class FakeArgs:
+  """Fakes args to _Generate - intention is to do just enough to run checks"""
+  def __init__(self, tester, files=None):
+    """ `tester` is MojomParserTestCase for paths.
+        `files` will have tester path added."""
+    self.checks_string = 'features'
+    self.depth = tester.GetPath('')
+    self.filelist = None
+    self.filename = [tester.GetPath(x) for x in files]
+    self.gen_directories = tester.GetPath('gen')
+    self.generators_string = ''
+    self.import_directories = []
+    self.output_dir = tester.GetPath('out')
+    self.scrambled_message_id_salt_paths = None
+    self.typemaps = []
+    self.variant = 'none'
+
+
+class MojoBindingsCheckTest(MojomParserTestCase):
+  def _ParseAndGenerate(self, mojoms):
+    self.ParseMojoms(mojoms)
+    args = FakeArgs(self, files=mojoms)
+    _Generate(args, {})
+
+  def assertValid(self, filename, content):
+    self.WriteFile(filename, content)
+    self._ParseAndGenerate([filename])
+
+  def assertThrows(self, filename, content, regexp):
+    mojoms = []
+    self.WriteFile(filename, content)
+    mojoms.append(filename)
+    with self.assertRaisesRegexp(check.CheckException, regexp):
+      self._ParseAndGenerate(mojoms)
+
+  def testLoads(self):
+    """Validate that the check is registered under the expected name."""
+    check_modules = LoadChecks('features')
+    self.assertTrue(check_modules['features'])
+
+  def testNullableOk(self):
+    self.assertValid(
+        "a.mojom", """
+          module a;
+          // Scaffolding.
+          feature kFeature {
+            const string name = "Hello";
+            const bool enabled_state = false;
+          };
+          [RuntimeFeature=kFeature]
+          interface Guarded {
+          };
+
+          // Unguarded interfaces should be ok everywhere.
+          interface NotGuarded { };
+
+          // Optional (nullable) interfaces should be ok everywhere:
+          struct Bar {
+            pending_remote<Guarded>? remote;
+            pending_receiver<Guarded>? receiver;
+          };
+          union Thingy {
+            pending_remote<Guarded>? remote;
+            pending_receiver<Guarded>? receiver;
+          };
+          interface Foo {
+            Foo(
+              pending_remote<Guarded>? remote,
+              pending_receiver<Guarded>? receiver,
+              pending_associated_remote<Guarded>? a_remote,
+              pending_associated_receiver<Guarded>? a_receiver,
+              // Unguarded interfaces do not have to be nullable.
+              pending_remote<NotGuarded> remote,
+              pending_receiver<NotGuarded> receiver,
+              pending_associated_remote<NotGuarded> a_remote,
+              pending_associated_receiver<NotGuarded> a_receiver
+            ) => (
+              pending_remote<Guarded>? remote,
+              pending_receiver<Guarded>? receiver
+            );
+            Bar(array<pending_remote<Guarded>?> remote)
+              => (map<string, pending_receiver<Guarded>?> a);
+          };
+    """)
+
+  def testMethodParamsMustBeNullable(self):
+    prelude = """
+      module a;
+      // Scaffolding.
+      feature kFeature {
+        const string name = "Hello";
+        const bool enabled_state = false;
+      };
+      [RuntimeFeature=kFeature]
+      interface Guarded { };
+    """
+    self.assertThrows(
+        'a.mojom', prelude + """
+          interface Trial {
+            Method(pending_remote<Guarded> a) => ();
+          };
+                     """, 'interface Guarded has a RuntimeFeature')
+    self.assertThrows(
+        'a.mojom', prelude + """
+          interface Trial {
+            Method(bool foo) => (pending_receiver<Guarded> a);
+          };
+                     """, 'interface Guarded has a RuntimeFeature')
+    self.assertThrows(
+        'a.mojom', prelude + """
+          interface Trial {
+            Method(pending_receiver<Guarded> a) => ();
+          };
+                     """, 'interface Guarded has a RuntimeFeature')
+    self.assertThrows(
+        'a.mojom', prelude + """
+          interface Trial {
+            Method(pending_associated_remote<Guarded> a) => ();
+          };
+                     """, 'interface Guarded has a RuntimeFeature')
+    self.assertThrows(
+        'a.mojom', prelude + """
+          interface Trial {
+            Method(pending_associated_receiver<Guarded> a) => ();
+          };
+                     """, 'interface Guarded has a RuntimeFeature')
+    self.assertThrows(
+        'a.mojom', prelude + """
+          interface Trial {
+            Method(array<pending_associated_receiver<Guarded>> a) => ();
+          };
+                     """, 'interface Guarded has a RuntimeFeature')
+    self.assertThrows(
+        'a.mojom', prelude + """
+          interface Trial {
+            Method(map<string, pending_associated_receiver<Guarded>> a) => ();
+          };
+                     """, 'interface Guarded has a RuntimeFeature')
+
+  def testStructUnionMembersMustBeNullable(self):
+    prelude = """
+      module a;
+      // Scaffolding.
+      feature kFeature {
+        const string name = "Hello";
+        const bool enabled_state = false;
+      };
+      [RuntimeFeature=kFeature]
+      interface Guarded { };
+    """
+    self.assertThrows(
+        'a.mojom', prelude + """
+          struct Trial {
+            pending_remote<Guarded> a;
+          };
+                     """, 'interface Guarded has a RuntimeFeature')
+    self.assertThrows(
+        'a.mojom', prelude + """
+          union Trial {
+            pending_remote<Guarded> a;
+          };
+                     """, 'interface Guarded has a RuntimeFeature')
diff --git a/utils/ipc/mojo/public/tools/bindings/checks/mojom_restrictions_check.py b/utils/ipc/mojo/public/tools/bindings/checks/mojom_restrictions_check.py
new file mode 100644
index 0000000..d570e26
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/bindings/checks/mojom_restrictions_check.py
@@ -0,0 +1,102 @@
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Validate RequireContext and AllowedContext annotations before generation."""
+
+import mojom.generate.check as check
+import mojom.generate.module as module
+
+
+class Check(check.Check):
+  def __init__(self, *args, **kwargs):
+    self.kind_to_interfaces = dict()
+    super(Check, self).__init__(*args, **kwargs)
+
+  def _IsPassedInterface(self, candidate):
+    if isinstance(
+        candidate.kind,
+        (module.PendingReceiver, module.PendingRemote,
+         module.PendingAssociatedReceiver, module.PendingAssociatedRemote)):
+      return True
+    return False
+
+  def _CheckInterface(self, method, param):
+    # |param| is a pending_x<Interface> so need .kind.kind to get Interface.
+    interface = param.kind.kind
+    if interface.require_context:
+      if method.allowed_context is None:
+        raise check.CheckException(
+            self.module, "method `{}` has parameter `{}` which passes interface"
+            " `{}` that requires an AllowedContext annotation but none exists.".
+            format(
+                method.mojom_name,
+                param.mojom_name,
+                interface.mojom_name,
+            ))
+      # If a string was provided, or if an enum was not imported, this will
+      # be a string and we cannot validate that it is in range.
+      if not isinstance(method.allowed_context, module.EnumValue):
+        raise check.CheckException(
+            self.module,
+            "method `{}` has AllowedContext={} which is not a valid enum value."
+            .format(method.mojom_name, method.allowed_context))
+      # EnumValue must be from the same enum to be compared.
+      if interface.require_context.enum != method.allowed_context.enum:
+        raise check.CheckException(
+            self.module, "method `{}` has parameter `{}` which passes interface"
+            " `{}` that requires AllowedContext={} but one of kind `{}` was "
+            "provided.".format(
+                method.mojom_name,
+                param.mojom_name,
+                interface.mojom_name,
+                interface.require_context.enum,
+                method.allowed_context.enum,
+            ))
+      # RestrictContext enums have most privileged field first (lowest value).
+      interface_value = interface.require_context.field.numeric_value
+      method_value = method.allowed_context.field.numeric_value
+      if interface_value < method_value:
+        raise check.CheckException(
+            self.module, "RequireContext={} > AllowedContext={} for method "
+            "`{}` which passes interface `{}`.".format(
+                interface.require_context.GetSpec(),
+                method.allowed_context.GetSpec(), method.mojom_name,
+                interface.mojom_name))
+      return True
+
+  def _GatherReferencedInterfaces(self, field):
+    key = field.kind.spec
+    # structs/unions can nest themselves so we need to bookkeep.
+    if not key in self.kind_to_interfaces:
+      # Might reference ourselves so have to create the list first.
+      self.kind_to_interfaces[key] = set()
+      for param in field.kind.fields:
+        if self._IsPassedInterface(param):
+          self.kind_to_interfaces[key].add(param)
+        elif isinstance(param.kind, (module.Struct, module.Union)):
+          for iface in self._GatherReferencedInterfaces(param):
+            self.kind_to_interfaces[key].add(iface)
+    return self.kind_to_interfaces[key]
+
+  def _CheckParams(self, method, params):
+    # Note: we have to repeat _CheckParams for each method as each might have
+    # different AllowedContext= attributes. We cannot memoize this function,
+    # but can do so for gathering referenced interfaces as their RequireContext
+    # attributes do not change.
+    for param in params:
+      if self._IsPassedInterface(param):
+        self._CheckInterface(method, param)
+      elif isinstance(param.kind, (module.Struct, module.Union)):
+        for interface in self._GatherReferencedInterfaces(param):
+          self._CheckInterface(method, interface)
+
+  def _CheckMethod(self, method):
+    if method.parameters:
+      self._CheckParams(method, method.parameters)
+    if method.response_parameters:
+      self._CheckParams(method, method.response_parameters)
+
+  def CheckModule(self):
+    for interface in self.module.interfaces:
+      for method in interface.methods:
+        self._CheckMethod(method)
diff --git a/utils/ipc/mojo/public/tools/bindings/checks/mojom_restrictions_checks_unittest.py b/utils/ipc/mojo/public/tools/bindings/checks/mojom_restrictions_checks_unittest.py
new file mode 100644
index 0000000..a6cd71e
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/bindings/checks/mojom_restrictions_checks_unittest.py
@@ -0,0 +1,254 @@
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+import unittest
+
+import mojom.generate.check as check
+from mojom_bindings_generator import LoadChecks, _Generate
+from mojom_parser_test_case import MojomParserTestCase
+
+# Mojoms that we will use in multiple tests.
+basic_mojoms = {
+    'level.mojom':
+    """
+  module level;
+  enum Level {
+    kHighest,
+    kMiddle,
+    kLowest,
+  };
+  """,
+    'interfaces.mojom':
+    """
+  module interfaces;
+  import "level.mojom";
+  struct Foo {int32 bar;};
+  [RequireContext=level.Level.kHighest]
+  interface High {
+    DoFoo(Foo foo);
+  };
+  [RequireContext=level.Level.kMiddle]
+  interface Mid {
+    DoFoo(Foo foo);
+  };
+  [RequireContext=level.Level.kLowest]
+  interface Low {
+    DoFoo(Foo foo);
+  };
+  """
+}
+
+
+class FakeArgs:
+  """Fakes args to _Generate - intention is to do just enough to run checks"""
+
+  def __init__(self, tester, files=None):
+    """ `tester` is MojomParserTestCase for paths.
+        `files` will have tester path added."""
+    self.checks_string = 'restrictions'
+    self.depth = tester.GetPath('')
+    self.filelist = None
+    self.filename = [tester.GetPath(x) for x in files]
+    self.gen_directories = tester.GetPath('gen')
+    self.generators_string = ''
+    self.import_directories = []
+    self.output_dir = tester.GetPath('out')
+    self.scrambled_message_id_salt_paths = None
+    self.typemaps = []
+    self.variant = 'none'
+
+
+class MojoBindingsCheckTest(MojomParserTestCase):
+  def _WriteBasicMojoms(self):
+    for filename, contents in basic_mojoms.items():
+      self.WriteFile(filename, contents)
+    return list(basic_mojoms.keys())
+
+  def _ParseAndGenerate(self, mojoms):
+    self.ParseMojoms(mojoms)
+    args = FakeArgs(self, files=mojoms)
+    _Generate(args, {})
+
+  def testLoads(self):
+    """Validate that the check is registered under the expected name."""
+    check_modules = LoadChecks('restrictions')
+    self.assertTrue(check_modules['restrictions'])
+
+  def testValidAnnotations(self):
+    mojoms = self._WriteBasicMojoms()
+
+    a = 'a.mojom'
+    self.WriteFile(
+        a, """
+      module a;
+      import "level.mojom";
+      import "interfaces.mojom";
+
+      interface PassesHigh {
+        [AllowedContext=level.Level.kHighest]
+        DoHigh(pending_receiver<interfaces.High> hi);
+      };
+      interface PassesMedium {
+        [AllowedContext=level.Level.kMiddle]
+        DoMedium(pending_receiver<interfaces.Mid> hi);
+        [AllowedContext=level.Level.kMiddle]
+        DoMediumRem(pending_remote<interfaces.Mid> hi);
+        [AllowedContext=level.Level.kMiddle]
+        DoMediumAssoc(pending_associated_receiver<interfaces.Mid> hi);
+        [AllowedContext=level.Level.kMiddle]
+        DoMediumAssocRem(pending_associated_remote<interfaces.Mid> hi);
+      };
+      interface PassesLow {
+        [AllowedContext=level.Level.kLowest]
+        DoLow(pending_receiver<interfaces.Low> hi);
+      };
+
+      struct One { pending_receiver<interfaces.High> hi; };
+      struct Two { One one; };
+      interface PassesNestedHigh {
+        [AllowedContext=level.Level.kHighest]
+        DoNestedHigh(Two two);
+      };
+
+      // Allowed as PassesHigh is not itself restricted.
+      interface PassesPassesHigh {
+        DoPass(pending_receiver<PassesHigh> hiho);
+      };
+    """)
+    mojoms.append(a)
+    self._ParseAndGenerate(mojoms)
+
+  def _testThrows(self, filename, content, regexp):
+    mojoms = self._WriteBasicMojoms()
+    self.WriteFile(filename, content)
+    mojoms.append(filename)
+    with self.assertRaisesRegexp(check.CheckException, regexp):
+      self._ParseAndGenerate(mojoms)
+
+  def testMissingAnnotation(self):
+    contents = """
+      module b;
+      import "level.mojom";
+      import "interfaces.mojom";
+
+      interface PassesHigh {
+        // err: missing annotation.
+        DoHigh(pending_receiver<interfaces.High> hi);
+      };
+    """
+    self._testThrows('b.mojom', contents, 'require.*?AllowedContext')
+
+  def testAllowTooLow(self):
+    contents = """
+      module b;
+      import "level.mojom";
+      import "interfaces.mojom";
+
+      interface PassesHigh {
+        // err: level is worse than required.
+        [AllowedContext=level.Level.kMiddle]
+        DoHigh(pending_receiver<interfaces.High> hi);
+      };
+    """
+    self._testThrows('b.mojom', contents,
+                     'RequireContext=.*?kHighest > AllowedContext=.*?kMiddle')
+
+  def testWrongEnumInAllow(self):
+    contents = """
+      module b;
+      import "level.mojom";
+      import "interfaces.mojom";
+      enum Blah {
+        kZero,
+      };
+      interface PassesHigh {
+        // err: different enums.
+        [AllowedContext=Blah.kZero]
+        DoHigh(pending_receiver<interfaces.High> hi);
+      };
+    """
+    self._testThrows('b.mojom', contents, 'but one of kind')
+
+  def testNotAnEnumInAllow(self):
+    contents = """
+      module b;
+      import "level.mojom";
+      import "interfaces.mojom";
+      interface PassesHigh {
+        // err: not an enum.
+        [AllowedContext=doopdedoo.mojom.kWhatever]
+        DoHigh(pending_receiver<interfaces.High> hi);
+      };
+    """
+    self._testThrows('b.mojom', contents, 'not a valid enum value')
+
+  def testMissingAllowedForNestedStructs(self):
+    contents = """
+      module b;
+      import "level.mojom";
+      import "interfaces.mojom";
+      struct One { pending_receiver<interfaces.High> hi; };
+      struct Two { One one; };
+      interface PassesNestedHigh {
+        // err: missing annotation.
+        DoNestedHigh(Two two);
+      };
+    """
+    self._testThrows('b.mojom', contents, 'require.*?AllowedContext')
+
+  def testMissingAllowedForNestedUnions(self):
+    contents = """
+      module b;
+      import "level.mojom";
+      import "interfaces.mojom";
+      struct One { pending_receiver<interfaces.High> hi; };
+      struct Two { One one; };
+      union Three {One one; Two two; };
+      interface PassesNestedHigh {
+        // err: missing annotation.
+        DoNestedHigh(Three three);
+      };
+    """
+    self._testThrows('b.mojom', contents, 'require.*?AllowedContext')
+
+  def testMultipleInterfacesThrows(self):
+    contents = """
+      module b;
+      import "level.mojom";
+      import "interfaces.mojom";
+      struct One { pending_receiver<interfaces.High> hi; };
+      interface PassesMultipleInterfaces {
+        [AllowedContext=level.Level.kMiddle]
+        DoMultiple(
+          pending_remote<interfaces.Mid> mid,
+          pending_receiver<interfaces.High> hi,
+          One one
+        );
+      };
+    """
+    self._testThrows('b.mojom', contents,
+                     'RequireContext=.*?kHighest > AllowedContext=.*?kMiddle')
+
+  def testMultipleInterfacesAllowed(self):
+    """Multiple interfaces can be passed, all satisfy the level."""
+    mojoms = self._WriteBasicMojoms()
+
+    b = "b.mojom"
+    self.WriteFile(
+        b, """
+      module b;
+      import "level.mojom";
+      import "interfaces.mojom";
+      struct One { pending_receiver<interfaces.High> hi; };
+      interface PassesMultipleInterfaces {
+        [AllowedContext=level.Level.kHighest]
+        DoMultiple(
+          pending_receiver<interfaces.High> hi,
+          pending_remote<interfaces.Mid> mid,
+          One one
+        );
+      };
+    """)
+    mojoms.append(b)
+    self._ParseAndGenerate(mojoms)
diff --git a/utils/ipc/mojo/public/tools/bindings/chromium_bindings_configuration.gni b/utils/ipc/mojo/public/tools/bindings/chromium_bindings_configuration.gni
deleted file mode 100644
index d8a1387..0000000
--- a/utils/ipc/mojo/public/tools/bindings/chromium_bindings_configuration.gni
+++ /dev/null
@@ -1,51 +0,0 @@
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-_typemap_imports = [
-  "//chrome/chrome_cleaner/mojom/typemaps/typemaps.gni",
-  "//chrome/common/importer/typemaps.gni",
-  "//chrome/common/media_router/mojom/typemaps.gni",
-  "//chrome/typemaps.gni",
-  "//chromecast/typemaps.gni",
-  "//chromeos/typemaps.gni",
-  "//chromeos/components/multidevice/mojom/typemaps.gni",
-  "//chromeos/services/cros_healthd/public/mojom/typemaps.gni",
-  "//chromeos/services/device_sync/public/mojom/typemaps.gni",
-  "//chromeos/services/network_config/public/mojom/typemaps.gni",
-  "//chromeos/services/secure_channel/public/mojom/typemaps.gni",
-  "//components/arc/mojom/typemaps.gni",
-  "//components/chromeos_camera/common/typemaps.gni",
-  "//components/services/storage/public/cpp/filesystem/typemaps.gni",
-  "//components/sync/mojom/typemaps.gni",
-  "//components/typemaps.gni",
-  "//content/browser/typemaps.gni",
-  "//content/public/common/typemaps.gni",
-  "//sandbox/mac/mojom/typemaps.gni",
-  "//services/media_session/public/cpp/typemaps.gni",
-  "//services/proxy_resolver/public/cpp/typemaps.gni",
-  "//services/resource_coordinator/public/cpp/typemaps.gni",
-  "//services/service_manager/public/cpp/typemaps.gni",
-  "//services/tracing/public/mojom/typemaps.gni",
-]
-
-_typemaps = []
-foreach(typemap_import, _typemap_imports) {
-  # Avoid reassignment error by assigning to empty scope first.
-  _imported = {
-  }
-  _imported = read_file(typemap_import, "scope")
-  _typemaps += _imported.typemaps
-}
-
-typemaps = []
-foreach(typemap, _typemaps) {
-  typemaps += [
-    {
-      filename = typemap
-      config = read_file(typemap, "scope")
-    },
-  ]
-}
-
-component_macro_suffix = ""
diff --git a/utils/ipc/mojo/public/tools/bindings/compile_typescript.py b/utils/ipc/mojo/public/tools/bindings/compile_typescript.py
deleted file mode 100644
index a978901..0000000
--- a/utils/ipc/mojo/public/tools/bindings/compile_typescript.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# Copyright 2019 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-import os
-import sys
-import argparse
-
-_HERE_PATH = os.path.dirname(__file__)
-_SRC_PATH = os.path.normpath(os.path.join(_HERE_PATH, '..', '..', '..', '..'))
-
-sys.path.append(os.path.join(_SRC_PATH, 'third_party', 'node'))
-import node
-import node_modules
-
-def main(argv):
-  parser = argparse.ArgumentParser()
-  parser.add_argument('--tsconfig_path', required=True)
-  args = parser.parse_args(argv)
-
-  result = node.RunNode([node_modules.PathToTypescript()] +
-                        ['--project', args.tsconfig_path])
-  if len(result) != 0:
-    raise RuntimeError('Failed to compile Typescript: \n%s' % result)
-
-if __name__ == '__main__':
-  main(sys.argv[1:])
diff --git a/utils/ipc/mojo/public/tools/bindings/concatenate-files.py b/utils/ipc/mojo/public/tools/bindings/concatenate-files.py
index 48bc66f..4dd26d4 100755
--- a/utils/ipc/mojo/public/tools/bindings/concatenate-files.py
+++ b/utils/ipc/mojo/public/tools/bindings/concatenate-files.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright 2019 The Chromium Authors. All rights reserved.
+# Copyright 2019 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 #
@@ -15,6 +15,7 @@
 from __future__ import print_function
 
 import optparse
+import sys
 
 
 def Concatenate(filenames):
@@ -47,7 +48,7 @@
   parser.set_usage("""Concatenate several files into one.
       Equivalent to: cat file1 ... > target.""")
   (_options, args) = parser.parse_args()
-  exit(0 if Concatenate(args) else 1)
+  sys.exit(0 if Concatenate(args) else 1)
 
 
 if __name__ == "__main__":
diff --git a/utils/ipc/mojo/public/tools/bindings/concatenate_and_replace_closure_exports.py b/utils/ipc/mojo/public/tools/bindings/concatenate_and_replace_closure_exports.py
index be8985c..7d56c9f 100755
--- a/utils/ipc/mojo/public/tools/bindings/concatenate_and_replace_closure_exports.py
+++ b/utils/ipc/mojo/public/tools/bindings/concatenate_and_replace_closure_exports.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright 2018 The Chromium Authors. All rights reserved.
+# Copyright 2018 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -20,6 +20,7 @@
 
 import optparse
 import re
+import sys
 
 
 _MOJO_INTERNAL_MODULE_NAME = "mojo.internal"
@@ -31,10 +32,10 @@
     return
 
   if line.startswith("goog.provide"):
-    match = re.match("goog.provide\('([^']+)'\);", line)
+    match = re.match(r"goog.provide\('([^']+)'\);", line)
     if not match:
       print("Invalid goog.provide line in %s:\n%s" % (filename, line))
-      exit(1)
+      sys.exit(1)
 
     module_name = match.group(1)
     if module_name == _MOJO_INTERNAL_MODULE_NAME:
@@ -67,7 +68,8 @@
     Concatenate several files into one, stripping Closure provide and
     require directives along the way.""")
   (_, args) = parser.parse_args()
-  exit(0 if ConcatenateAndReplaceExports(args) else 1)
+  sys.exit(0 if ConcatenateAndReplaceExports(args) else 1)
+
 
 if __name__ == "__main__":
   main()
diff --git a/utils/ipc/mojo/public/tools/bindings/format_typemap_generator_args.py b/utils/ipc/mojo/public/tools/bindings/format_typemap_generator_args.py
deleted file mode 100755
index 7ac4af5..0000000
--- a/utils/ipc/mojo/public/tools/bindings/format_typemap_generator_args.py
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env python
-# Copyright 2016 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-
-from __future__ import print_function
-
-import sys
-
-# This utility converts mojom dependencies into their corresponding typemap
-# paths and formats them to be consumed by generate_type_mappings.py.
-
-
-def FormatTypemap(typemap_filename):
-  # A simple typemap is valid Python with a minor alteration.
-  with open(typemap_filename) as f:
-    typemap_content = f.read().replace('=\n', '=')
-  typemap = {}
-  exec typemap_content in typemap
-
-  for header in typemap.get('public_headers', []):
-    yield 'public_headers=%s' % header
-  for header in typemap.get('traits_headers', []):
-    yield 'traits_headers=%s' % header
-  for header in typemap.get('type_mappings', []):
-    yield 'type_mappings=%s' % header
-
-
-def main():
-  typemaps = sys.argv[1:]
-  print(' '.join('--start-typemap %s' % ' '.join(FormatTypemap(typemap))
-                 for typemap in typemaps))
-
-
-if __name__ == '__main__':
-  sys.exit(main())
diff --git a/utils/ipc/mojo/public/tools/bindings/gen_data_files_list.py b/utils/ipc/mojo/public/tools/bindings/gen_data_files_list.py
index 8b78d09..c6daff0 100644
--- a/utils/ipc/mojo/public/tools/bindings/gen_data_files_list.py
+++ b/utils/ipc/mojo/public/tools/bindings/gen_data_files_list.py
@@ -1,4 +1,4 @@
-# Copyright 2017 The Chromium Authors. All rights reserved.
+# Copyright 2017 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """Generates a list of all files in a directory.
diff --git a/utils/ipc/mojo/public/tools/bindings/generate_type_mappings.py b/utils/ipc/mojo/public/tools/bindings/generate_type_mappings.py
index a009664..4a53e2b 100755
--- a/utils/ipc/mojo/public/tools/bindings/generate_type_mappings.py
+++ b/utils/ipc/mojo/public/tools/bindings/generate_type_mappings.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright 2016 The Chromium Authors. All rights reserved.
+# Copyright 2016 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """Generates a JSON typemap from its command-line arguments and dependencies.
@@ -82,10 +82,12 @@
       for entry in config['types']:
         configs[entry['mojom']] = {
             'typename': entry['cpp'],
+            'forward_declaration': entry.get('forward_declaration', None),
             'public_headers': config.get('traits_headers', []),
             'traits_headers': config.get('traits_private_headers', []),
             'copyable_pass_by_value': entry.get('copyable_pass_by_value',
                                                 False),
+            'default_constructible': entry.get('default_constructible', True),
             'force_serialize': entry.get('force_serialize', False),
             'hashable': entry.get('hashable', False),
             'move_only': entry.get('move_only', False),
diff --git a/utils/ipc/mojo/public/tools/bindings/minify_with_terser.py b/utils/ipc/mojo/public/tools/bindings/minify_with_terser.py
new file mode 100755
index 0000000..cefee7a
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/bindings/minify_with_terser.py
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+#
+# This utility minifies JS files with terser.
+#
+# Instance of 'node' has no 'RunNode' member (no-member)
+# pylint: disable=no-member
+
+import argparse
+import os
+import sys
+
+_HERE_PATH = os.path.dirname(__file__)
+_SRC_PATH = os.path.normpath(os.path.join(_HERE_PATH, '..', '..', '..', '..'))
+_CWD = os.getcwd()
+sys.path.append(os.path.join(_SRC_PATH, 'third_party', 'node'))
+import node
+import node_modules
+
+
+def MinifyFile(input_file, output_file):
+  node.RunNode([
+      node_modules.PathToTerser(), input_file, '--mangle', '--compress',
+      '--comments', 'false', '--output', output_file
+  ])
+
+
+def main(argv):
+  parser = argparse.ArgumentParser()
+  parser.add_argument('--input', required=True)
+  parser.add_argument('--output', required=True)
+  args = parser.parse_args(argv)
+
+  # Delete the output file if it already exists. It may be a sym link to the
+  # input, because in non-optimized/pre-Terser builds the input file is copied
+  # to the output location with gn copy().
+  out_path = os.path.join(_CWD, args.output)
+  if (os.path.exists(out_path)):
+    os.remove(out_path)
+
+  MinifyFile(os.path.join(_CWD, args.input), out_path)
+
+
+if __name__ == '__main__':
+  main(sys.argv[1:])
diff --git a/utils/ipc/mojo/public/tools/bindings/mojom.gni b/utils/ipc/mojo/public/tools/bindings/mojom.gni
index fe2a1da..3f6e54e 100644
--- a/utils/ipc/mojo/public/tools/bindings/mojom.gni
+++ b/utils/ipc/mojo/public/tools/bindings/mojom.gni
@@ -1,25 +1,28 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import("//build/config/python.gni")
 import("//third_party/closure_compiler/closure_args.gni")
 import("//third_party/closure_compiler/compile_js.gni")
 import("//third_party/protobuf/proto_library.gni")
+import("//ui/webui/resources/tools/generate_grd.gni")
 import("//ui/webui/webui_features.gni")
 
+import("//build/config/cast.gni")
+
 # TODO(rockot): Maybe we can factor these dependencies out of //mojo. They're
 # used to conditionally enable message ID scrambling in a way which is
 # consistent across toolchains and which is affected by branded vs non-branded
 # Chrome builds. Ideally we could create some generic knobs here that could be
 # flipped elsewhere though.
 import("//build/config/chrome_build.gni")
-import("//build/config/chromecast_build.gni")
 import("//build/config/chromeos/ui_mode.gni")
+import("//build/config/features.gni")
 import("//build/config/nacl/config.gni")
 import("//build/toolchain/kythe.gni")
 import("//components/nacl/features.gni")
 import("//third_party/jinja2/jinja2.gni")
+import("//third_party/ply/ply.gni")
 import("//tools/ipc_fuzzer/ipc_fuzzer.gni")
 declare_args() {
   # Indicates whether typemapping should be supported in this build
@@ -34,21 +37,30 @@
 
   # Controls message ID scrambling behavior. If |true|, message IDs are
   # scrambled (i.e. randomized based on the contents of //chrome/VERSION) on
-  # non-Chrome OS desktop platforms. Set to |false| to disable message ID
-  # scrambling on all platforms.
-  enable_mojom_message_id_scrambling = true
-
-  # Enables Closure compilation of generated JS lite bindings. In environments
-  # where compilation is supported, any mojom target "foo" will also have a
-  # corresponding "foo_js_library_for_compile" target generated.
-  enable_mojom_closure_compile = enable_js_type_check && optimize_webui
-
-  # Enables generating Typescript bindings and compiling them to JS bindings.
-  enable_typescript_bindings = false
+  # non-Chrome OS desktop platforms. Enabled on official builds by default.
+  # Set to |true| to enable message ID scrambling on a specific build.
+  # See also `enable_scrambled_message_ids` below for more details.
+  enable_mojom_message_id_scrambling = is_official_build
 
   # Enables generating javascript fuzzing-related code and the bindings for the
   # MojoLPM fuzzer targets. Off by default.
   enable_mojom_fuzzer = false
+
+  # Enables Closure compilation of generated JS lite bindings. In environments
+  # where compilation is supported, any mojom target "foo" will also have a
+  # corresponding "foo_js_library_for_compile" target generated.
+  if (is_chromeos_ash) {
+    enable_mojom_closure_compile = enable_js_type_check && optimize_webui
+  }
+}
+
+# Closure libraries are needed for mojom_closure_compile, and when
+# js_type_check is enabled on Ash.
+if (is_chromeos_ash) {
+  generate_mojom_closure_libraries =
+      enable_mojom_closure_compile || enable_js_type_check
+} else {
+  generate_mojom_closure_libraries = false
 }
 
 # NOTE: We would like to avoid scrambling message IDs where it doesn't add
@@ -69,9 +81,8 @@
 # lacros-chrome switches to target_os="chromeos"
 enable_scrambled_message_ids =
     enable_mojom_message_id_scrambling &&
-    (is_mac || is_win ||
-     (is_linux && !is_chromeos_ash && !is_chromecast && !is_chromeos_lacros) ||
-     ((enable_nacl || is_nacl || is_nacl_nonsfi) &&
+    (is_mac || is_win || (is_linux && !is_castos) ||
+     ((enable_nacl || is_nacl) &&
       (target_os != "chromeos" && !chromeos_is_browser_only)))
 
 _mojom_tools_root = "//mojo/public/tools"
@@ -80,7 +91,9 @@
 mojom_parser_sources = [
   "$_mojom_library_root/__init__.py",
   "$_mojom_library_root/error.py",
+  "$_mojom_library_root/fileutil.py",
   "$_mojom_library_root/generate/__init__.py",
+  "$_mojom_library_root/generate/check.py",
   "$_mojom_library_root/generate/generator.py",
   "$_mojom_library_root/generate/module.py",
   "$_mojom_library_root/generate/pack.py",
@@ -88,21 +101,32 @@
   "$_mojom_library_root/generate/translate.py",
   "$_mojom_library_root/parse/__init__.py",
   "$_mojom_library_root/parse/ast.py",
+  "$_mojom_library_root/parse/conditional_features.py",
   "$_mojom_library_root/parse/lexer.py",
   "$_mojom_library_root/parse/parser.py",
+  "//tools/diagnosis/crbug_1001171.py",
 ]
 
 mojom_generator_root = "$_mojom_tools_root/bindings"
 mojom_generator_script = "$mojom_generator_root/mojom_bindings_generator.py"
 mojom_generator_sources =
     mojom_parser_sources + [
+      "$mojom_generator_root/checks/__init__.py",
+      "$mojom_generator_root/checks/mojom_attributes_check.py",
+      "$mojom_generator_root/checks/mojom_definitions_check.py",
+      "$mojom_generator_root/checks/mojom_interface_feature_check.py",
+      "$mojom_generator_root/checks/mojom_restrictions_check.py",
+      "$mojom_generator_root/generators/__init__.py",
       "$mojom_generator_root/generators/cpp_util.py",
       "$mojom_generator_root/generators/mojom_cpp_generator.py",
       "$mojom_generator_root/generators/mojom_java_generator.py",
-      "$mojom_generator_root/generators/mojom_mojolpm_generator.py",
       "$mojom_generator_root/generators/mojom_js_generator.py",
+      "$mojom_generator_root/generators/mojom_mojolpm_generator.py",
       "$mojom_generator_root/generators/mojom_ts_generator.py",
       "$mojom_generator_script",
+      "//build/action_helpers.py",
+      "//build/gn_helpers.py",
+      "//build/zip_helpers.py",
     ]
 
 if (enable_scrambled_message_ids) {
@@ -243,12 +267,16 @@
 #       |cpp_only| is set to true, it overrides this to prevent generation of
 #       Java bindings.
 #
-#   enable_fuzzing (optional)
+#   enable_js_fuzzing (optional)
+#       Enables generation of javascript fuzzing sources for the target if the
+#       global build arg |enable_mojom_fuzzer| is also set to |true|.
+#       Defaults to |true|. If JS fuzzing generation is enabled for a target,
+#       the target will always generate JS bindings even if |cpp_only| is set to
+#       |true|. See note above.
+#
+#   enable_mojolpm_fuzzing (optional)
 #       Enables generation of fuzzing sources for the target if the global build
-#       arg |enable_mojom_fuzzer| is also set to |true|. Defaults to |true|. If
-#       fuzzing generation is enabled for a target, the target will always
-#       generate JS bindings even if |cpp_only| is set to |true|. See note
-#       above.
+#       arg |enable_mojom_fuzzer| is also set to |true|. Defaults to |true|.
 #
 #   support_lazy_serialization (optional)
 #       If set to |true|, generated C++ bindings will effectively prefer to
@@ -310,8 +338,15 @@
 #       correct dependency order. Note that this only has an effect if
 #       the |enable_mojom_closure_compile| global arg is set to |true| as well.
 #
-#   use_typescript_sources (optional)
-#       Uses the Typescript generator to generate JavaScript bindings.
+#   generate_webui_js_bindings (optional)
+#       Generate WebUI bindings in JavaScript rather than TypeScript. Defaults
+#       to false. ChromeOS only parameter.
+#
+#   generate_legacy_js_bindings (optional)
+#       Generate js_data_deps target containing legacy JavaScript bindings files
+#       for Blink tests and other non-WebUI users when generating TypeScript
+#       bindings for WebUI. Ignored if generate_webui_js_bindings is set to
+#       true.
 #
 #   js_generate_struct_deserializers (optional)
 #       Generates JS deerialize methods for structs.
@@ -323,17 +358,23 @@
 #   webui_module_path (optional)
 #       The path or URL at which modules generated by this target will be
 #       accessible to WebUI pages. This may either be an absolute path or
-#       a full URL path starting with "chrome://resources/mojo".
+#       a full URL path starting with "chrome://resources/mojo". If this path
+#       is not specified, WebUI bindings will not be generated.
 #
 #       If an absolute path, a WebUI page may only import these modules if
-#       they are manually packaged and mapped independently by that page's
-#       WebUIDataSource. The mapped path must match the path given here.
+#       they are added to that page's data source (usually by adding the
+#       modules to the mojo_files list for build_webui(), or by listing the
+#       files as inputs to the page's ts_library() and/or generate_grd() build
+#       steps.
 #
 #       If this is is instead a URL string starting with
-#       "chrome://resources/mojo", the generated resources must be added to
-#       content_resources.grd and registered with
-#       content::SharedResourcesDataSource with a corresponding path, at which
-#       point they will be made available to all WebUI pages at the given URL.
+#       "chrome://resources/mojo", the resulting bindings files should
+#       be added to one of the lists in ui/webui/resources/mojo/BUILD.gn,
+#       at which point they will be made available to all WebUI pages at the
+#       given URL.
+#
+#       Note: WebUI module bindings are generated in TypeScript by default,
+#       unless |generate_webui_js_bindings| is specified as true.
 #
 # The following parameters are used to support the component build. They are
 # needed so that bindings which are linked with a component can use the same
@@ -402,16 +443,41 @@
 #             should be mapped in generated bindings. This is a string like
 #             "::base::Value" or "std::vector<::base::Value>".
 #
-#         move_only (optional)
-#             A boolean value (default false) which indicates whether the C++
-#             type is move-only. If true, generated bindings will pass the type
-#             by value and use std::move() at call sites.
-#
 #         copyable_pass_by_value (optional)
 #             A boolean value (default false) which effectively indicates
 #             whether the C++ type is very cheap to copy. If so, generated
 #             bindings will pass by value but not use std::move() at call sites.
 #
+#         default_constructible (optional)
+#             A boolean value (default true) which indicates whether the C++
+#             type is default constructible. If a C++ type is not default
+#             constructible (e.g. the implementor of the type prefers not to
+#             publicly expose a default constructor that creates an object in an
+#             invalid state), Mojo will instead construct C++ type with an
+#             argument of the type `mojo::DefaultConstruct::Tag` (essentially a
+#             passkey-like type specifically for this use case).
+#
+#         force_serialize (optional)
+#             A boolean value (default false) which disables lazy serialization
+#             of the typemapped type if lazy serialization is enabled for the
+#             mojom target applying this typemap.
+#
+#         forward_declaration (optional)
+#             A forward declaration of the C++ type, which bindings that don't
+#             need the full type definition can use to reduce the size of
+#             the generated code. This is a string like
+#             "namespace base { class Value; }".
+#
+#         hashable (optional)
+#             A boolean value (default false) indicating whether the C++ type is
+#             hashable. Set to true if true AND needed (i.e. you need to use the
+#             type as the key of a mojom map).
+#
+#         move_only (optional)
+#             A boolean value (default false) which indicates whether the C++
+#             type is move-only. If true, generated bindings will pass the type
+#             by value and use std::move() at call sites.
+#
 #         nullable_is_same_type (optional)
 #             A boolean value (default false) which indicates that the C++ type
 #             has some baked-in semantic notion of a "null" state. If true, the
@@ -421,16 +487,6 @@
 #             type with absl::optional, and null values are simply
 #             absl::nullopt.
 #
-#         hashable (optional)
-#             A boolean value (default false) indicating whether the C++ type is
-#             hashable. Set to true if true AND needed (i.e. you need to use the
-#             type as the key of a mojom map).
-#
-#         force_serialize (optional)
-#             A boolean value (default false) which disables lazy serialization
-#             of the typemapped type if lazy serialization is enabled for the
-#             mojom target applying this typemap.
-#
 # Additional typemap scope parameters:
 #
 #   traits_headers (optional)
@@ -621,20 +677,26 @@
   build_metadata_filename = "$target_gen_dir/$target_name.build_metadata"
   build_metadata = {
   }
-  build_metadata.sources = rebase_path(sources_list)
+  build_metadata.sources = rebase_path(sources_list, target_gen_dir)
   build_metadata.deps = []
   foreach(dep, all_deps) {
     dep_target_gen_dir = get_label_info(dep, "target_gen_dir")
     dep_name = get_label_info(dep, "name")
     build_metadata.deps +=
-        [ rebase_path("$dep_target_gen_dir/$dep_name.build_metadata") ]
+        [ rebase_path("$dep_target_gen_dir/$dep_name.build_metadata",
+                      target_gen_dir) ]
   }
   write_file(build_metadata_filename, build_metadata, "json")
 
-  generate_fuzzing =
-      (!defined(invoker.enable_fuzzing) || invoker.enable_fuzzing) &&
+  generate_js_fuzzing =
+      (!defined(invoker.enable_js_fuzzing) || invoker.enable_js_fuzzing) &&
       enable_mojom_fuzzer && (!defined(invoker.testonly) || !invoker.testonly)
 
+  generate_mojolpm_fuzzing =
+      (!defined(invoker.enable_mojolpm_fuzzing) ||
+       invoker.enable_mojolpm_fuzzing) && enable_mojom_fuzzer &&
+      (!defined(invoker.testonly) || !invoker.testonly)
+
   parser_target_name = "${target_name}__parser"
   parser_deps = []
   foreach(dep, all_deps) {
@@ -665,30 +727,34 @@
         "is_chromeos",
         "is_chromeos_ash",
       ]
+    } else if (is_chromeos_lacros) {
+      enabled_features += [
+        "is_chromeos",
+        "is_chromeos_lacros",
+      ]
     } else if (is_fuchsia) {
       enabled_features += [ "is_fuchsia" ]
     } else if (is_ios) {
       enabled_features += [ "is_ios" ]
-    } else if (is_linux || is_chromeos_lacros) {
+    } else if (is_linux) {
       enabled_features += [ "is_linux" ]
-      if (is_chromeos_lacros) {
-        enabled_features += [
-          "is_chromeos",
-          "is_chromeos_lacros",
-        ]
-      }
     } else if (is_mac) {
       enabled_features += [ "is_mac" ]
     } else if (is_win) {
       enabled_features += [ "is_win" ]
     }
 
-    # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-    python2_action(parser_target_name) {
+    if (is_apple) {
+      enabled_features += [ "is_apple" ]
+    }
+
+    action(parser_target_name) {
+      allow_remote = true
+      custom_processor = "mojom_parser"
       script = mojom_parser_script
-      inputs = mojom_parser_sources + [ build_metadata_filename ]
+      inputs = mojom_parser_sources + ply_sources + [ build_metadata_filename ]
       sources = sources_list
-      deps = parser_deps
+      public_deps = parser_deps
       outputs = []
       foreach(base_path, output_file_base_paths) {
         filename = get_path_info(base_path, "file")
@@ -698,31 +764,35 @@
 
       filelist = []
       foreach(source, sources_list) {
-        filelist += [ rebase_path(source) ]
+        filelist += [ rebase_path(source, root_build_dir) ]
       }
-      response_file_contents = filelist
+
+      # Workaround for https://github.com/ninja-build/ninja/issues/1966.
+      rsp_file = "$target_gen_dir/${target_name}.rsp"
+      write_file(rsp_file, filelist)
+      inputs += [ rsp_file ]
 
       args = [
         # Resolve relative input mojom paths against both the root src dir and
         # the root gen dir.
         "--input-root",
-        rebase_path("//."),
+        rebase_path("//.", root_build_dir),
         "--input-root",
-        rebase_path(root_gen_dir),
+        rebase_path(root_gen_dir, root_build_dir),
 
         "--output-root",
-        rebase_path(root_gen_dir),
+        rebase_path(root_gen_dir, root_build_dir),
 
-        "--mojom-file-list={{response_file_name}}",
+        "--mojom-file-list=" + rebase_path(rsp_file, root_build_dir),
 
         "--check-imports",
-        rebase_path(build_metadata_filename),
+        rebase_path(build_metadata_filename, root_build_dir),
       ]
 
       if (defined(invoker.input_root_override)) {
         args += [
           "--input-root",
-          rebase_path(invoker.input_root_override),
+          rebase_path(invoker.input_root_override, root_build_dir),
         ]
       }
 
@@ -738,6 +808,13 @@
           "--add-module-metadata",
           "webui_module_path=${invoker.webui_module_path}",
         ]
+        if (defined(invoker.generate_webui_js_bindings) &&
+            invoker.generate_webui_js_bindings) {
+          args += [
+            "--add-module-metadata",
+            "generate_webui_js=True",
+          ]
+        }
       }
     }
   }
@@ -819,11 +896,12 @@
       }
     }
 
-    # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-    python2_action(generator_cpp_message_ids_target_name) {
+    action(generator_cpp_message_ids_target_name) {
+      allow_remote = true
       script = mojom_generator_script
       inputs = mojom_generator_sources + jinja2_sources
-      sources = sources_list
+      sources = sources_list +
+                [ "$root_gen_dir/mojo/public/tools/bindings/cpp_templates.zip" ]
       deps = [
         ":$parser_target_name",
         "//mojo/public/tools/bindings:precompile_templates",
@@ -835,16 +913,22 @@
       args = common_generator_args
       filelist = []
       foreach(source, sources_list) {
-        filelist += [ rebase_path("$source", root_build_dir) ]
+        filelist += [ rebase_path(source, root_build_dir) ]
       }
       foreach(base_path, output_file_base_paths) {
+        filename = get_path_info(base_path, "file")
+        dirname = get_path_info(base_path, "dir")
+        inputs += [ "$root_gen_dir/$dirname/${filename}-module" ]
         outputs += [ "$root_gen_dir/$base_path-shared-message-ids.h" ]
       }
 
-      response_file_contents = filelist
+      # Workaround for https://github.com/ninja-build/ninja/issues/1966.
+      rsp_file = "$target_gen_dir/${target_name}.rsp"
+      write_file(rsp_file, filelist)
+      inputs += [ rsp_file ]
 
       args += [
-        "--filelist={{response_file_name}}",
+        "--filelist=" + rebase_path(rsp_file, root_build_dir),
         "--generate_non_variant_code",
         "--generate_message_ids",
         "-g",
@@ -860,12 +944,13 @@
 
     generator_shared_target_name = "${target_name}_shared__generator"
 
-    # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-    python2_action(generator_shared_target_name) {
+    action(generator_shared_target_name) {
+      allow_remote = true
       visibility = [ ":*" ]
       script = mojom_generator_script
       inputs = mojom_generator_sources + jinja2_sources
-      sources = sources_list
+      sources = sources_list +
+                [ "$root_gen_dir/mojo/public/tools/bindings/cpp_templates.zip" ]
       deps = [
         ":$parser_target_name",
         "//mojo/public/tools/bindings:precompile_templates",
@@ -878,10 +963,16 @@
       args = common_generator_args
       filelist = []
       foreach(source, sources_list) {
-        filelist += [ rebase_path("$source", root_build_dir) ]
+        filelist += [ rebase_path(source, root_build_dir) ]
       }
       foreach(base_path, output_file_base_paths) {
+        # Need the mojom-module as an input to this action.
+        filename = get_path_info(base_path, "file")
+        dirname = get_path_info(base_path, "dir")
+        inputs += [ "$root_gen_dir/$dirname/${filename}-module" ]
+
         outputs += [
+          "$root_gen_dir/$base_path-features.h",
           "$root_gen_dir/$base_path-params-data.h",
           "$root_gen_dir/$base_path-shared-internal.h",
           "$root_gen_dir/$base_path-shared.cc",
@@ -889,10 +980,13 @@
         ]
       }
 
-      response_file_contents = filelist
+      # Workaround for https://github.com/ninja-build/ninja/issues/1966.
+      rsp_file = "$target_gen_dir/${target_name}.rsp"
+      write_file(rsp_file, filelist)
+      inputs += [ rsp_file ]
 
       args += [
-        "--filelist={{response_file_name}}",
+        "--filelist=" + rebase_path(rsp_file, root_build_dir),
         "--generate_non_variant_code",
         "-g",
         "c++",
@@ -923,12 +1017,14 @@
     if (defined(invoker.testonly)) {
       testonly = invoker.testonly
     }
+    configs += [ "//build/config/compiler:wexit_time_destructors" ]
     deps = []
     public_deps = []
     if (output_file_base_paths != []) {
       sources = []
       foreach(base_path, output_file_base_paths) {
         sources += [
+          "$root_gen_dir/$base_path-features.h",
           "$root_gen_dir/$base_path-params-data.h",
           "$root_gen_dir/$base_path-shared-internal.h",
           "$root_gen_dir/$base_path-shared.cc",
@@ -972,7 +1068,7 @@
     }
   }
 
-  if (generate_fuzzing) {
+  if (generate_mojolpm_fuzzing) {
     # This block generates the proto files used for the MojoLPM fuzzer,
     # and the corresponding proto targets that will be linked in the fuzzer
     # targets. These are independent of the typemappings, and can be done
@@ -981,11 +1077,15 @@
     generator_mojolpm_proto_target_name =
         "${target_name}_mojolpm_proto_generator"
 
-    # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-    python2_action(generator_mojolpm_proto_target_name) {
+    action(generator_mojolpm_proto_target_name) {
+      allow_remote = true
       script = mojom_generator_script
       inputs = mojom_generator_sources + jinja2_sources
-      sources = invoker.sources
+      sources =
+          invoker.sources + [
+            "$root_gen_dir/mojo/public/tools/bindings/cpp_templates.zip",
+            "$root_gen_dir/mojo/public/tools/bindings/mojolpm_templates.zip",
+          ]
       deps = [
         ":$parser_target_name",
         "//mojo/public/tools/bindings:precompile_templates",
@@ -994,15 +1094,37 @@
       outputs = []
       args = common_generator_args
       filelist = []
-      foreach(source, invoker.sources) {
-        filelist += [ rebase_path("$source", root_build_dir) ]
+
+      # Split the input into generated and non-generated source files. They
+      # need to be processed separately.
+      gen_dir_path_wildcard = get_path_info("//", "gen_dir") + "/*"
+      non_gen_sources =
+          filter_exclude(invoker.sources, [ gen_dir_path_wildcard ])
+      gen_sources = filter_include(invoker.sources, [ gen_dir_path_wildcard ])
+
+      foreach(source, non_gen_sources) {
+        filelist += [ rebase_path(source, root_build_dir) ]
+        inputs += [ "$target_gen_dir/$source-module" ]
         outputs += [ "$target_gen_dir/$source.mojolpm.proto" ]
       }
 
-      response_file_contents = filelist
+      foreach(source, gen_sources) {
+        filelist += [ rebase_path(source, root_build_dir) ]
+
+        # For generated files, we assume they're in the target_gen_dir or a
+        # sub-folder of it. Rebase the path so we can get the relative location.
+        source_file = rebase_path(source, target_gen_dir)
+        inputs += [ "$target_gen_dir/$source_file-module" ]
+        outputs += [ "$target_gen_dir/$source_file.mojolpm.proto" ]
+      }
+
+      # Workaround for https://github.com/ninja-build/ninja/issues/1966.
+      rsp_file = "$target_gen_dir/${target_name}.rsp"
+      write_file(rsp_file, filelist)
+      inputs += [ rsp_file ]
 
       args += [
-        "--filelist={{response_file_name}}",
+        "--filelist=" + rebase_path(rsp_file, root_build_dir),
         "--generate_non_variant_code",
         "-g",
         "mojolpm",
@@ -1014,9 +1136,20 @@
       proto_library(mojolpm_proto_target_name) {
         testonly = true
         generate_python = false
+
+        # Split the input into generated and non-generated source files. They
+        # need to be processed separately.
+        gen_dir_path_wildcard = get_path_info("//", "gen_dir") + "/*"
+        non_gen_sources =
+            filter_exclude(invoker.sources, [ gen_dir_path_wildcard ])
+        gen_sources = filter_include(invoker.sources, [ gen_dir_path_wildcard ])
         sources = process_file_template(
-                invoker.sources,
+                non_gen_sources,
                 [ "{{source_gen_dir}}/{{source_file_part}}.mojolpm.proto" ])
+        sources += process_file_template(
+                gen_sources,
+                [ "{{source_dir}}/{{source_file_part}}.mojolpm.proto" ])
+
         import_dirs = [ "//" ]
         proto_in_dir = "${root_gen_dir}"
         proto_out_dir = "."
@@ -1055,7 +1188,7 @@
     component_macro_suffix = ""
   }
   if ((!defined(invoker.disable_variants) || !invoker.disable_variants) &&
-      !is_ios) {
+      use_blink) {
     blink_variant = {
       variant = "blink"
       component_macro_suffix = "_BLINK"
@@ -1149,39 +1282,6 @@
             "${bindings_configuration.component_macro_suffix}_IMPL" ]
     }
 
-    export_args = []
-    export_args_overridden = false
-    if (defined(bindings_configuration.for_blink) &&
-        bindings_configuration.for_blink) {
-      if (defined(invoker.export_class_attribute_blink)) {
-        export_args_overridden = true
-        export_args += [
-          "--export_attribute",
-          invoker.export_class_attribute_blink,
-          "--export_header",
-          invoker.export_header_blink,
-        ]
-      }
-    } else if (defined(invoker.export_class_attribute)) {
-      export_args_overridden = true
-      export_args += [
-        "--export_attribute",
-        invoker.export_class_attribute,
-        "--export_header",
-        invoker.export_header,
-      ]
-    }
-
-    if (!export_args_overridden && defined(invoker.component_macro_prefix)) {
-      export_args += [
-        "--export_attribute",
-        "COMPONENT_EXPORT(${invoker.component_macro_prefix}" +
-            "${bindings_configuration.component_macro_suffix})",
-        "--export_header",
-        "base/component_export.h",
-      ]
-    }
-
     generate_java = false
     if (!cpp_only && defined(invoker.generate_java)) {
       generate_java = invoker.generate_java
@@ -1190,6 +1290,38 @@
     type_mappings_path =
         "$target_gen_dir/${target_name}${variant_suffix}__type_mappings"
     if (sources_list != []) {
+      export_args = []
+      export_args_overridden = false
+      if (defined(bindings_configuration.for_blink) &&
+          bindings_configuration.for_blink) {
+        if (defined(invoker.export_class_attribute_blink)) {
+          export_args_overridden = true
+          export_args += [
+            "--export_attribute",
+            invoker.export_class_attribute_blink,
+            "--export_header",
+            invoker.export_header_blink,
+          ]
+        }
+      } else if (defined(invoker.export_class_attribute)) {
+        export_args_overridden = true
+        export_args += [
+          "--export_attribute",
+          invoker.export_class_attribute,
+          "--export_header",
+          invoker.export_header,
+        ]
+      }
+      if (!export_args_overridden && defined(invoker.component_macro_prefix)) {
+        export_args += [
+          "--export_attribute",
+          "COMPONENT_EXPORT(${invoker.component_macro_prefix}" +
+              "${bindings_configuration.component_macro_suffix})",
+          "--export_header",
+          "base/component_export.h",
+        ]
+      }
+
       generator_cpp_output_suffixes = []
       variant_dash_suffix = ""
       if (defined(variant)) {
@@ -1198,7 +1330,6 @@
       generator_cpp_output_suffixes += [
         "${variant_dash_suffix}-forward.h",
         "${variant_dash_suffix}-import-headers.h",
-        "${variant_dash_suffix}-test-utils.cc",
         "${variant_dash_suffix}-test-utils.h",
         "${variant_dash_suffix}.cc",
         "${variant_dash_suffix}.h",
@@ -1207,16 +1338,28 @@
       generator_target_name = "${target_name}${variant_suffix}__generator"
 
       # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-      python2_action(generator_target_name) {
+      action(generator_target_name) {
+        allow_remote = true
         visibility = [ ":*" ]
         script = mojom_generator_script
         inputs = mojom_generator_sources + jinja2_sources
-        sources = sources_list
+        sources =
+            sources_list + [
+              "$root_gen_dir/mojo/public/tools/bindings/cpp_templates.zip",
+              type_mappings_path,
+            ]
+        if (generate_mojolpm_fuzzing &&
+            !defined(bindings_configuration.variant)) {
+          sources += [
+            "$root_gen_dir/mojo/public/tools/bindings/mojolpm_templates.zip",
+          ]
+        }
         deps = [
           ":$parser_target_name",
           ":$type_mappings_target_name",
           "//mojo/public/tools/bindings:precompile_templates",
         ]
+
         if (defined(invoker.parser_deps)) {
           deps += invoker.parser_deps
         }
@@ -1224,18 +1367,22 @@
         args = common_generator_args + export_args
         filelist = []
         foreach(source, sources_list) {
-          filelist += [ rebase_path("$source", root_build_dir) ]
+          filelist += [ rebase_path(source, root_build_dir) ]
         }
         foreach(base_path, output_file_base_paths) {
+          filename = get_path_info(base_path, "file")
+          dirname = get_path_info(base_path, "dir")
+          inputs += [ "$root_gen_dir/$dirname/${filename}-module" ]
+
           outputs += [
             "$root_gen_dir/${base_path}${variant_dash_suffix}-forward.h",
             "$root_gen_dir/${base_path}${variant_dash_suffix}-import-headers.h",
-            "$root_gen_dir/${base_path}${variant_dash_suffix}-test-utils.cc",
             "$root_gen_dir/${base_path}${variant_dash_suffix}-test-utils.h",
             "$root_gen_dir/${base_path}${variant_dash_suffix}.cc",
             "$root_gen_dir/${base_path}${variant_dash_suffix}.h",
           ]
-          if (generate_fuzzing && !defined(bindings_configuration.variant)) {
+          if (generate_mojolpm_fuzzing &&
+              !defined(bindings_configuration.variant)) {
             outputs += [
               "$root_gen_dir/${base_path}${variant_dash_suffix}-mojolpm.cc",
               "$root_gen_dir/${base_path}${variant_dash_suffix}-mojolpm.h",
@@ -1243,14 +1390,17 @@
           }
         }
 
-        response_file_contents = filelist
-
+        # Workaround for https://github.com/ninja-build/ninja/issues/1966.
+        rsp_file = "$target_gen_dir/${target_name}.rsp"
+        write_file(rsp_file, filelist)
+        inputs += [ rsp_file ]
         args += [
-          "--filelist={{response_file_name}}",
+          "--filelist=" + rebase_path("$rsp_file", root_build_dir),
           "-g",
         ]
 
-        if (generate_fuzzing && !defined(bindings_configuration.variant)) {
+        if (generate_mojolpm_fuzzing &&
+            !defined(bindings_configuration.variant)) {
           args += [ "c++,mojolpm" ]
         } else {
           args += [ "c++" ]
@@ -1294,6 +1444,8 @@
               "--extra_cpp_template_paths",
               rebase_path(extra_cpp_template, root_build_dir),
             ]
+            inputs += [ extra_cpp_template ]
+
             assert(
                 get_path_info(extra_cpp_template, "extension") == "tmpl",
                 "--extra_cpp_template_paths only accepts template files ending in extension .tmpl")
@@ -1306,62 +1458,6 @@
       }
     }
 
-    if (generate_fuzzing && !defined(variant)) {
-      # This block contains the C++ targets for the MojoLPM fuzzer, we need to
-      # do this here so that we can use the typemap configuration for the
-      # empty-variant Mojo target.
-
-      mojolpm_target_name = "${target_name}_mojolpm"
-      mojolpm_generator_target_name = "${target_name}__generator"
-      source_set(mojolpm_target_name) {
-        # There are still a few missing header dependencies between mojo targets
-        # with typemaps and the dependencies of their typemap headers. It would
-        # be good to enable include checking for these in the future though.
-        check_includes = false
-        testonly = true
-        if (defined(invoker.sources)) {
-          sources = process_file_template(
-                  invoker.sources,
-                  [
-                    "{{source_gen_dir}}/{{source_file_part}}-mojolpm.cc",
-                    "{{source_gen_dir}}/{{source_file_part}}-mojolpm.h",
-                  ])
-          deps = []
-        } else {
-          sources = []
-          deps = []
-        }
-
-        public_deps = [
-          ":$generator_shared_target_name",
-
-          # NB: hardcoded dependency on the no-variant variant generator, since
-          # mojolpm only uses the no-variant type.
-          ":$mojolpm_generator_target_name",
-          ":$mojolpm_proto_target_name",
-          "//base",
-          "//mojo/public/tools/fuzzers:mojolpm",
-        ]
-
-        foreach(d, all_deps) {
-          # Resolve the name, so that a target //mojo/something becomes
-          # //mojo/something:something and we can append variant_suffix to
-          # get the cpp dependency name.
-          full_name = get_label_info("$d", "label_no_toolchain")
-          public_deps += [ "${full_name}_mojolpm" ]
-        }
-
-        foreach(config, cpp_typemap_configs) {
-          if (defined(config.traits_deps)) {
-            deps += config.traits_deps
-          }
-          if (defined(config.traits_public_deps)) {
-            public_deps += config.traits_public_deps
-          }
-        }
-      }
-    }
-
     # Write the typemapping configuration for this target out to a file to be
     # validated by a Python script. This helps catch mistakes that can't
     # be caught by logic in GN.
@@ -1389,20 +1485,20 @@
     write_file(_typemap_config_filename, _rebased_typemap_configs, "json")
     _mojom_target_name = target_name
 
-    # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-    python2_action(_typemap_validator_target_name) {
+    action(_typemap_validator_target_name) {
+      allow_remote = true
       script = "$mojom_generator_root/validate_typemap_config.py"
       inputs = [ _typemap_config_filename ]
       outputs = [ _typemap_stamp_filename ]
       args = [
         get_label_info(_mojom_target_name, "label_no_toolchain"),
-        rebase_path(_typemap_config_filename),
-        rebase_path(_typemap_stamp_filename),
+        rebase_path(_typemap_config_filename, root_build_dir),
+        rebase_path(_typemap_stamp_filename, root_build_dir),
       ]
     }
 
-    # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-    python2_action(type_mappings_target_name) {
+    action(type_mappings_target_name) {
+      allow_remote = true
       inputs =
           mojom_generator_sources + jinja2_sources + [ _typemap_stamp_filename ]
       outputs = [ type_mappings_path ]
@@ -1413,6 +1509,7 @@
         rebase_path(type_mappings_path, root_build_dir),
       ]
 
+      sources = []
       foreach(d, all_deps) {
         name = get_label_info(d, "label_no_toolchain")
         toolchain = get_label_info(d, "toolchain")
@@ -1422,12 +1519,11 @@
         dependency_output_dir =
             get_label_info(dependency_output, "target_gen_dir")
         dependency_name = get_label_info(dependency_output, "name")
-        dependency_path =
-            rebase_path("$dependency_output_dir/${dependency_name}",
-                        root_build_dir)
+        dependency_path = "$dependency_output_dir/${dependency_name}"
+        sources += [ dependency_path ]
         args += [
           "--dependency",
-          dependency_path,
+          rebase_path(dependency_path, root_build_dir),
         ]
       }
 
@@ -1485,11 +1581,15 @@
       if (defined(output_name_override)) {
         output_name = output_name_override
       }
-      visibility = output_visibility + [ ":$output_target_name" ]
+      visibility = output_visibility + [
+                     ":$output_target_name",
+                     ":${target_name}_mojolpm",
+                   ]
       if (defined(invoker.testonly)) {
         testonly = invoker.testonly
       }
       defines = export_defines
+      configs += [ "//build/config/compiler:wexit_time_destructors" ]
       configs += extra_configs
       if (output_file_base_paths != []) {
         sources = []
@@ -1578,13 +1678,81 @@
       }
     }
 
+    if (generate_mojolpm_fuzzing && !defined(variant)) {
+      # This block contains the C++ targets for the MojoLPM fuzzer, we need to
+      # do this here so that we can use the typemap configuration for the
+      # empty-variant Mojo target.
+
+      mojolpm_target_name = "${target_name}_mojolpm"
+      mojolpm_generator_target_name = "${target_name}__generator"
+      source_set(mojolpm_target_name) {
+        # There are still a few missing header dependencies between mojo targets
+        # with typemaps and the dependencies of their typemap headers. It would
+        # be good to enable include checking for these in the future though.
+        check_includes = false
+        testonly = true
+        if (defined(invoker.sources)) {
+          # Split the input into generated and non-generated source files. They
+          # need to be processed separately.
+          gen_dir_path_wildcard = get_path_info("//", "gen_dir") + "/*"
+          non_gen_sources =
+              filter_exclude(invoker.sources, [ gen_dir_path_wildcard ])
+          gen_sources =
+              filter_include(invoker.sources, [ gen_dir_path_wildcard ])
+          sources = process_file_template(
+                  non_gen_sources,
+                  [
+                    "{{source_gen_dir}}/{{source_file_part}}-mojolpm.cc",
+                    "{{source_gen_dir}}/{{source_file_part}}-mojolpm.h",
+                  ])
+          sources += process_file_template(
+                  gen_sources,
+                  [
+                    "{{source_dir}}/{{source_file_part}}-mojolpm.cc",
+                    "{{source_dir}}/{{source_file_part}}-mojolpm.h",
+                  ])
+          deps = [ ":$output_target_name" ]
+        } else {
+          sources = []
+          deps = []
+        }
+
+        public_deps = [
+          ":$generator_shared_target_name",
+
+          # NB: hardcoded dependency on the no-variant variant generator, since
+          # mojolpm only uses the no-variant type.
+          ":$mojolpm_generator_target_name",
+          ":$mojolpm_proto_target_name",
+          "//base",
+          "//mojo/public/tools/fuzzers:mojolpm",
+        ]
+
+        foreach(d, all_deps) {
+          # Resolve the name, so that a target //mojo/something becomes
+          # //mojo/something:something and we can append variant_suffix to
+          # get the cpp dependency name.
+          full_name = get_label_info("$d", "label_no_toolchain")
+          public_deps += [ "${full_name}_mojolpm" ]
+        }
+
+        foreach(config, cpp_typemap_configs) {
+          if (defined(config.traits_deps)) {
+            deps += config.traits_deps
+          }
+          if (defined(config.traits_public_deps)) {
+            public_deps += config.traits_public_deps
+          }
+        }
+      }
+    }
+
     if (generate_java && is_android) {
       import("//build/config/android/rules.gni")
 
       java_generator_target_name = target_name + "_java__generator"
       if (sources_list != []) {
-        # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-        python2_action(java_generator_target_name) {
+        action(java_generator_target_name) {
           script = mojom_generator_script
           inputs = mojom_generator_sources + jinja2_sources
           sources = sources_list
@@ -1597,7 +1765,7 @@
           args = common_generator_args
           filelist = []
           foreach(source, sources_list) {
-            filelist += [ rebase_path("$source", root_build_dir) ]
+            filelist += [ rebase_path(source, root_build_dir) ]
           }
           foreach(base_path, output_file_base_paths) {
             outputs += [ "$root_gen_dir/$base_path.srcjar" ]
@@ -1624,8 +1792,7 @@
 
       java_srcjar_target_name = target_name + "_java_sources"
 
-      # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-      python2_action(java_srcjar_target_name) {
+      action(java_srcjar_target_name) {
         script = "//build/android/gyp/zip.py"
         inputs = []
         if (output_file_base_paths != []) {
@@ -1651,7 +1818,6 @@
       android_library(java_target_name) {
         forward_variables_from(invoker, [ "enable_bytecode_checks" ])
         deps = [
-          "//base:base_java",
           "//mojo/public/java:bindings_java",
           "//mojo/public/java:system_java",
           "//third_party/androidx:androidx_annotation_annotation_java",
@@ -1673,21 +1839,36 @@
     }
   }
 
-  use_typescript_for_target =
-      enable_typescript_bindings && defined(invoker.use_typescript_sources) &&
-      invoker.use_typescript_sources
-
-  if (!use_typescript_for_target && defined(invoker.use_typescript_sources)) {
-    not_needed(invoker, [ "use_typescript_sources" ])
+  if (defined(invoker.generate_webui_js_bindings)) {
+    assert(is_chromeos_ash,
+           "generate_webui_js_bindings can only be used on ChromeOS Ash")
+    assert(invoker.generate_webui_js_bindings,
+           "generate_webui_js_bindings should be set to true or removed")
   }
 
-  if ((generate_fuzzing || !defined(invoker.cpp_only) || !invoker.cpp_only) &&
-      !use_typescript_for_target) {
+  use_typescript_for_target = defined(invoker.webui_module_path) &&
+                              !defined(invoker.generate_webui_js_bindings)
+
+  generate_legacy_js = !use_typescript_for_target ||
+                       (defined(invoker.generate_legacy_js_bindings) &&
+                        invoker.generate_legacy_js_bindings)
+
+  if (!use_typescript_for_target &&
+      defined(invoker.generate_legacy_js_bindings)) {
+    not_needed(invoker, [ "generate_legacy_js_bindings" ])
+  }
+
+  # Targets needed by both TS and JS bindings targets. These are needed
+  # unconditionally for JS bindings targets, and are needed for TS bindings
+  # targets when generate_legacy_js_bindings is true. This option is provided
+  # since the legacy bindings are needed by Blink tests and non-Chromium users,
+  # which are not expected to migrate to modules or TypeScript.
+  if (generate_legacy_js && (generate_js_fuzzing ||
+                             !defined(invoker.cpp_only) || !invoker.cpp_only)) {
     if (sources_list != []) {
       generator_js_target_name = "${target_name}_js__generator"
 
-      # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-      python2_action(generator_js_target_name) {
+      action(generator_js_target_name) {
         script = mojom_generator_script
         inputs = mojom_generator_sources + jinja2_sources
         sources = sources_list
@@ -1702,19 +1883,18 @@
         args = common_generator_args
         filelist = []
         foreach(source, sources_list) {
-          filelist += [ rebase_path("$source", root_build_dir) ]
+          filelist += [ rebase_path(source, root_build_dir) ]
         }
         foreach(base_path, output_file_base_paths) {
           outputs += [
             "$root_gen_dir/$base_path.js",
-            "$root_gen_dir/$base_path.externs.js",
             "$root_gen_dir/$base_path.m.js",
             "$root_gen_dir/$base_path-lite.js",
-            "$root_gen_dir/$base_path.html",
             "$root_gen_dir/$base_path-lite-for-compile.js",
           ]
 
-          if (defined(invoker.webui_module_path)) {
+          if (defined(invoker.webui_module_path) &&
+              !use_typescript_for_target) {
             outputs += [ "$root_gen_dir/mojom-webui/$base_path-webui.js" ]
           }
         }
@@ -1725,7 +1905,6 @@
           "--filelist={{response_file_name}}",
           "-g",
           "javascript",
-          "--js_bindings_mode=new",
         ]
 
         if (defined(invoker.js_generate_struct_deserializers) &&
@@ -1739,7 +1918,7 @@
           args += message_scrambling_args
         }
 
-        if (generate_fuzzing) {
+        if (generate_js_fuzzing) {
           args += [ "--generate_fuzzing" ]
         }
       }
@@ -1783,31 +1962,13 @@
         data_deps += [ "${full_name}_js_data_deps" ]
       }
     }
+  }
 
-    js_library_target_name = "${target_name}_js_library"
-    if (sources_list != []) {
-      js_library(js_library_target_name) {
-        extra_public_deps = [ ":$generator_js_target_name" ]
-        sources = []
-        foreach(base_path, output_file_base_paths) {
-          sources += [ "$root_gen_dir/${base_path}-lite.js" ]
-        }
-        externs_list = [
-          "${externs_path}/mojo_core.js",
-          "${externs_path}/pending.js",
-        ]
-
-        deps = []
-        foreach(d, all_deps) {
-          full_name = get_label_info(d, "label_no_toolchain")
-          deps += [ "${full_name}_js_library" ]
-        }
-      }
-    } else {
-      group(js_library_target_name) {
-      }
-    }
-
+  # js_library() closure compiler targets, primarily used on ChromeOS. Only
+  # generate these targets if the mojom target is not C++ only and is not using
+  # TypeScript.
+  if (generate_mojom_closure_libraries &&
+      (!defined(invoker.cpp_only) || !invoker.cpp_only) && generate_legacy_js) {
     js_library_for_compile_target_name = "${target_name}_js_library_for_compile"
     if (sources_list != []) {
       js_library(js_library_for_compile_target_name) {
@@ -1834,35 +1995,9 @@
       }
     }
 
-    js_modules_target_name = "${target_name}_js_modules"
-    if (sources_list != []) {
-      js_library(js_modules_target_name) {
-        extra_public_deps = [ ":$generator_js_target_name" ]
-        sources = []
-        foreach(base_path, output_file_base_paths) {
-          sources += [ "$root_gen_dir/${base_path}.m.js" ]
-        }
-        externs_list = [
-          "${externs_path}/mojo_core.js",
-          "${externs_path}/pending.js",
-        ]
-        if (defined(invoker.disallow_native_types) &&
-            invoker.disallow_native_types) {
-          deps = []
-        } else {
-          deps = [ "//mojo/public/js:bindings_uncompiled" ]
-        }
-        foreach(d, all_deps) {
-          full_name = get_label_info(d, "label_no_toolchain")
-          deps += [ "${full_name}_js_modules" ]
-        }
-      }
-    } else {
-      group(js_modules_target_name) {
-      }
-    }
-
-    if (defined(invoker.webui_module_path)) {
+    # WebUI specific closure targets, not needed by targets that are generating
+    # TypeScript WebUI bindings or by legacy-only targets.
+    if (defined(invoker.webui_module_path) && !use_typescript_for_target) {
       webui_js_target_name = "${target_name}_webui_js"
       if (sources_list != []) {
         js_library(webui_js_target_name) {
@@ -1890,46 +2025,38 @@
         group(webui_js_target_name) {
         }
       }
+
+      webui_grdp_target_name = "${target_name}_webui_grdp"
+      out_grd = "$target_gen_dir/${target_name}_webui_resources.grdp"
+      grd_prefix = "${target_name}_webui"
+      generate_grd(webui_grdp_target_name) {
+        grd_prefix = grd_prefix
+        out_grd = out_grd
+
+        deps = [ ":$webui_js_target_name" ]
+
+        input_files = []
+        foreach(base_path, output_file_base_paths) {
+          input_files += [ "${base_path}-webui.js" ]
+        }
+
+        input_files_base_dir =
+            rebase_path("$root_gen_dir/mojom-webui", "$root_build_dir")
+      }
     }
   }
-  if ((generate_fuzzing || !defined(invoker.cpp_only) || !invoker.cpp_only) &&
-      use_typescript_for_target) {
-    generator_js_target_names = []
-    source_filelist = []
-    foreach(source, sources_list) {
-      source_filelist += [ rebase_path("$source", root_build_dir) ]
-    }
-
-    dependency_types = [
-      {
-        name = "regular"
-        ts_extension = ".ts"
-        js_extension = ".js"
-      },
-      {
-        name = "es_modules"
-        ts_extension = ".m.ts"
-        js_extension = ".m.js"
-      },
-    ]
-
-    foreach(dependency_type, dependency_types) {
-      ts_outputs = []
-      js_outputs = []
-
-      foreach(base_path, output_file_base_paths) {
-        ts_outputs +=
-            [ "$root_gen_dir/$base_path-lite${dependency_type.ts_extension}" ]
-        js_outputs +=
-            [ "$root_gen_dir/$base_path-lite${dependency_type.js_extension}" ]
+  if ((generate_js_fuzzing || !defined(invoker.cpp_only) ||
+       !invoker.cpp_only) && use_typescript_for_target) {
+    if (sources_list != []) {
+      source_filelist = []
+      foreach(source, sources_list) {
+        source_filelist += [ rebase_path(source, root_build_dir) ]
       }
 
       # Generate Typescript bindings.
-      generator_ts_target_name =
-          "${target_name}_${dependency_type.name}__ts__generator"
+      generator_ts_target_name = "${target_name}_ts__generator"
 
-      # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-      python2_action(generator_ts_target_name) {
+      action(generator_ts_target_name) {
         script = mojom_generator_script
         inputs = mojom_generator_sources + jinja2_sources
         sources = sources_list
@@ -1938,7 +2065,10 @@
           "//mojo/public/tools/bindings:precompile_templates",
         ]
 
-        outputs = ts_outputs
+        outputs = []
+        foreach(base_path, output_file_base_paths) {
+          outputs += [ "$root_gen_dir/$base_path-webui.ts" ]
+        }
         args = common_generator_args
         response_file_contents = source_filelist
 
@@ -1948,98 +2078,21 @@
           "typescript",
         ]
 
-        if (dependency_type.name == "es_modules") {
-          args += [ "--ts_use_es_modules" ]
+        if (!defined(invoker.scramble_message_ids) ||
+            invoker.scramble_message_ids) {
+          inputs += message_scrambling_inputs
+          args += message_scrambling_args
         }
 
-        # TODO(crbug.com/1007587): Support scramble_message_ids.
+        if (defined(invoker.js_generate_struct_deserializers) &&
+            invoker.js_generate_struct_deserializers) {
+          args += [ "--js_generate_struct_deserializers" ]
+        }
+
+        # TODO(crbug.com/1007587): Support scramble_message_ids if above is
+        # insufficient.
         # TODO(crbug.com/1007591): Support generate_fuzzing.
       }
-
-      # Create tsconfig.json for the generated Typescript.
-      tsconfig_filename =
-          "$target_gen_dir/$target_name-${dependency_type.name}-tsconfig.json"
-      tsconfig = {
-      }
-      tsconfig.compilerOptions = {
-        composite = true
-        target = "es6"
-        module = "es6"
-        lib = [
-          "es6",
-          "esnext.bigint",
-        ]
-        strict = true
-      }
-      tsconfig.files = []
-      foreach(base_path, output_file_base_paths) {
-        tsconfig.files += [ rebase_path(
-                "$root_gen_dir/$base_path-lite${dependency_type.ts_extension}",
-                target_gen_dir,
-                root_gen_dir) ]
-      }
-      tsconfig.references = []
-
-      # Get tsconfigs for deps.
-      foreach(d, all_deps) {
-        dep_target_gen_dir = rebase_path(get_label_info(d, "target_gen_dir"))
-        dep_name = get_label_info(d, "name")
-        reference = {
-        }
-        reference.path = "$dep_target_gen_dir/$dep_name-${dependency_type.name}-tsconfig.json"
-        tsconfig.references += [ reference ]
-      }
-      write_file(tsconfig_filename, tsconfig, "json")
-
-      # Compile previously generated Typescript to Javascript.
-      generator_js_target_name =
-          "${target_name}_${dependency_type.name}__js__generator"
-      generator_js_target_names += [ generator_js_target_name ]
-
-      # TODO(crbug.com/1194274): Investigate nondeterminism in Py3 builds.
-      python2_action(generator_js_target_name) {
-        script = "$mojom_generator_root/compile_typescript.py"
-        sources = ts_outputs
-        outputs = js_outputs
-        public_deps = [ ":$generator_ts_target_name" ]
-        foreach(d, all_deps) {
-          full_name = get_label_info(d, "label_no_toolchain")
-          public_deps +=
-              [ "${full_name}_${dependency_type.name}__js__generator" ]
-        }
-
-        absolute_tsconfig_path =
-            rebase_path(tsconfig_filename, "", target_gen_dir)
-        args = [ "--tsconfig_path=$absolute_tsconfig_path" ]
-      }
-    }
-
-    js_target_name = target_name + "_js"
-    group(js_target_name) {
-      public_deps = []
-      if (sources_list != []) {
-        foreach(generator_js_target_name, generator_js_target_names) {
-          public_deps += [ ":$generator_js_target_name" ]
-        }
-      }
-
-      foreach(d, all_deps) {
-        full_name = get_label_info(d, "label_no_toolchain")
-        public_deps += [ "${full_name}_js" ]
-      }
-    }
-
-    group(js_data_deps_target_name) {
-      data = js_outputs
-      deps = []
-      foreach(generator_js_target_name, generator_js_target_names) {
-        deps += [ ":$generator_js_target_name" ]
-      }
-      data_deps = []
-      foreach(d, all_deps) {
-        full_name = get_label_info(d, "label_no_toolchain")
-        data_deps += [ "${full_name}_js_data_deps" ]
-      }
     }
   }
 }
diff --git a/utils/ipc/mojo/public/tools/bindings/mojom_bindings_generator.py b/utils/ipc/mojo/public/tools/bindings/mojom_bindings_generator.py
index da9efc7..8c641c2 100755
--- a/utils/ipc/mojo/public/tools/bindings/mojom_bindings_generator.py
+++ b/utils/ipc/mojo/public/tools/bindings/mojom_bindings_generator.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright 2013 The Chromium Authors. All rights reserved.
+# Copyright 2013 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -57,10 +57,17 @@
     "typescript": "mojom_ts_generator",
 }
 
+_BUILTIN_CHECKS = {
+    "attributes": "mojom_attributes_check",
+    "definitions": "mojom_definitions_check",
+    "features": "mojom_interface_feature_check",
+    "restrictions": "mojom_restrictions_check",
+}
+
 
 def LoadGenerators(generators_string):
   if not generators_string:
-    return []  # No generators.
+    return {}  # No generators.
 
   generators = {}
   for generator_name in [s.strip() for s in generators_string.split(",")]:
@@ -74,6 +81,21 @@
   return generators
 
 
+def LoadChecks(checks_string):
+  if not checks_string:
+    return {}  # No checks.
+
+  checks = {}
+  for check_name in [s.strip() for s in checks_string.split(",")]:
+    check = check_name.lower()
+    if check not in _BUILTIN_CHECKS:
+      print("Unknown check name %s" % check_name)
+      sys.exit(1)
+    check_module = importlib.import_module("checks.%s" % _BUILTIN_CHECKS[check])
+    checks[check] = check_module
+  return checks
+
+
 def MakeImportStackMessage(imported_filename_stack):
   """Make a (human-readable) message listing a chain of imports. (Returned
   string begins with a newline (if nonempty) and does not end with one.)"""
@@ -82,7 +104,7 @@
                     zip(imported_filename_stack[1:], imported_filename_stack)]))
 
 
-class RelativePath(object):
+class RelativePath:
   """Represents a path relative to the source tree or generated output dir."""
 
   def __init__(self, path, source_root, output_dir):
@@ -142,7 +164,7 @@
     return f.read()
 
 
-class MojomProcessor(object):
+class MojomProcessor:
   """Takes parsed mojom modules and generates language bindings from them.
 
   Attributes:
@@ -169,8 +191,8 @@
     if 'c++' in self._typemap:
       self._typemap['mojolpm'] = self._typemap['c++']
 
-  def _GenerateModule(self, args, remaining_args, generator_modules,
-                      rel_filename, imported_filename_stack):
+  def _GenerateModule(self, args, remaining_args, check_modules,
+                      generator_modules, rel_filename, imported_filename_stack):
     # Return the already-generated module.
     if rel_filename.path in self._processed_files:
       return self._processed_files[rel_filename.path]
@@ -190,12 +212,16 @@
       ScrambleMethodOrdinals(module.interfaces, salt)
 
     if self._should_generate(rel_filename.path):
+      # Run checks on module first.
+      for check_module in check_modules.values():
+        checker = check_module.Check(module)
+        checker.CheckModule()
+      # Then run generation.
       for language, generator_module in generator_modules.items():
         generator = generator_module.Generator(
             module, args.output_dir, typemap=self._typemap.get(language, {}),
             variant=args.variant, bytecode_path=args.bytecode_path,
             for_blink=args.for_blink,
-            js_bindings_mode=args.js_bindings_mode,
             js_generate_struct_deserializers=\
                     args.js_generate_struct_deserializers,
             export_attribute=args.export_attribute,
@@ -234,6 +260,7 @@
       args.import_directories[idx] = RelativePath(tokens[0], args.depth,
                                                   args.output_dir)
   generator_modules = LoadGenerators(args.generators_string)
+  check_modules = LoadChecks(args.checks_string)
 
   fileutil.EnsureDirectoryExists(args.output_dir)
 
@@ -246,7 +273,7 @@
 
   for filename in args.filename:
     processor._GenerateModule(
-        args, remaining_args, generator_modules,
+        args, remaining_args, check_modules, generator_modules,
         RelativePath(filename, args.depth, args.output_dir), [])
 
   return 0
@@ -286,6 +313,12 @@
                                metavar="GENERATORS",
                                default="c++,javascript,java,mojolpm",
                                help="comma-separated list of generators")
+  generate_parser.add_argument("-c",
+                               "--checks",
+                               dest="checks_string",
+                               metavar="CHECKS",
+                               default=",".join(_BUILTIN_CHECKS.keys()),
+                               help="comma-separated list of checks")
   generate_parser.add_argument(
       "--gen_dir", dest="gen_directories", action="append", metavar="directory",
       default=[], help="add a directory to be searched for the syntax trees.")
@@ -309,11 +342,6 @@
                                help="Use WTF types as generated types for mojo "
                                "string/array/map.")
   generate_parser.add_argument(
-      "--js_bindings_mode", choices=["new", "old"], default="old",
-      help="This option only affects the JavaScript bindings. The value could "
-      "be \"new\" to generate new-style lite JS bindings in addition to the "
-      "old, or \"old\" to only generate old bindings.")
-  generate_parser.add_argument(
       "--js_generate_struct_deserializers", action="store_true",
       help="Generate javascript deserialize methods for structs in "
       "mojom-lite.js file")
@@ -387,4 +415,10 @@
 
 if __name__ == "__main__":
   with crbug_1001171.DumpStateOnLookupError():
-    sys.exit(main())
+    ret = main()
+    # Exit without running GC, which can save multiple seconds due to the large
+    # number of object created. But flush is necessary as os._exit doesn't do
+    # that.
+    sys.stdout.flush()
+    sys.stderr.flush()
+    os._exit(ret)
diff --git a/utils/ipc/mojo/public/tools/bindings/mojom_bindings_generator_unittest.py b/utils/ipc/mojo/public/tools/bindings/mojom_bindings_generator_unittest.py
index bddbe3f..761922b 100644
--- a/utils/ipc/mojo/public/tools/bindings/mojom_bindings_generator_unittest.py
+++ b/utils/ipc/mojo/public/tools/bindings/mojom_bindings_generator_unittest.py
@@ -1,4 +1,4 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -8,13 +8,13 @@
 from mojom_bindings_generator import ScrambleMethodOrdinals
 
 
-class FakeIface(object):
+class FakeIface:
   def __init__(self):
     self.mojom_name = None
     self.methods = None
 
 
-class FakeMethod(object):
+class FakeMethod:
   def __init__(self, explicit_ordinal=None):
     self.explicit_ordinal = explicit_ordinal
     self.ordinal = explicit_ordinal
diff --git a/utils/ipc/mojo/public/tools/bindings/mojom_types_downgrader.py b/utils/ipc/mojo/public/tools/bindings/mojom_types_downgrader.py
deleted file mode 100755
index 15f0e3b..0000000
--- a/utils/ipc/mojo/public/tools/bindings/mojom_types_downgrader.py
+++ /dev/null
@@ -1,119 +0,0 @@
-#!/usr/bin/env python
-# Copyright 2020 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-"""Downgrades *.mojom files to the old mojo types for remotes and receivers."""
-
-import argparse
-import fnmatch
-import os
-import re
-import shutil
-import sys
-import tempfile
-
-# List of patterns and replacements to match and use against the contents of a
-# mojo file. Each replacement string will be used with Python string's format()
-# function, so the '{}' substring is used to mark where the mojo type should go.
-_MOJO_REPLACEMENTS = {
-    r'pending_remote': r'{}',
-    r'pending_receiver': r'{}&',
-    r'pending_associated_remote': r'associated {}',
-    r'pending_associated_receiver': r'associated {}&',
-}
-
-# Pre-compiled regular expression that matches against any of the replacements.
-_REGEXP_PATTERN = re.compile(
-    r'|'.join(
-        ['{}\s*<\s*(.*?)\s*>'.format(k) for k in _MOJO_REPLACEMENTS.keys()]),
-    flags=re.DOTALL)
-
-
-def ReplaceFunction(match_object):
-  """Returns the right replacement for the string matched against the regexp."""
-  for index, (match, repl) in enumerate(_MOJO_REPLACEMENTS.items(), 1):
-    if match_object.group(0).startswith(match):
-      return repl.format(match_object.group(index))
-
-
-def DowngradeFile(path, output_dir=None):
-  """Downgrades the mojom file specified by |path| to the old mojo types.
-
-  Optionally pass |output_dir| to place the result under a separate output
-  directory, preserving the relative path to the file included in |path|.
-  """
-  # Use a temporary file to dump the new contents after replacing the patterns.
-  with open(path) as src_mojo_file:
-    with tempfile.NamedTemporaryFile(mode='w', delete=False) as tmp_mojo_file:
-      tmp_contents = _REGEXP_PATTERN.sub(ReplaceFunction, src_mojo_file.read())
-      tmp_mojo_file.write(tmp_contents)
-
-  # Files should be placed in the desired output directory
-  if output_dir:
-    output_filepath = os.path.join(output_dir, os.path.basename(path))
-    if not os.path.exists(output_dir):
-      os.makedirs(output_dir)
-  else:
-    output_filepath = path
-
-  # Write the new contents preserving the original file's attributes.
-  shutil.copystat(path, tmp_mojo_file.name)
-  shutil.move(tmp_mojo_file.name, output_filepath)
-
-  # Make sure to "touch" the new file so that access, modify and change times
-  # are always newer than the source file's, otherwise Modify time will be kept
-  # as per the call to shutil.copystat(), causing unnecessary generations of the
-  # output file in subsequent builds due to ninja considering it dirty.
-  os.utime(output_filepath, None)
-
-
-def DowngradeDirectory(path, output_dir=None):
-  """Downgrades mojom files inside directory |path| to the old mojo types.
-
-  Optionally pass |output_dir| to place the result under a separate output
-  directory, preserving the relative path to the file included in |path|.
-  """
-  # We don't have recursive glob.glob() nor pathlib.Path.rglob() in Python 2.7
-  mojom_filepaths = []
-  for dir_path, _, filenames in os.walk(path):
-    for filename in fnmatch.filter(filenames, "*mojom"):
-      mojom_filepaths.append(os.path.join(dir_path, filename))
-
-  for path in mojom_filepaths:
-    absolute_dirpath = os.path.dirname(os.path.abspath(path))
-    if output_dir:
-      dest_dirpath = output_dir + absolute_dirpath
-    else:
-      dest_dirpath = absolute_dirpath
-    DowngradeFile(path, dest_dirpath)
-
-
-def DowngradePath(src_path, output_dir=None):
-  """Downgrades the mojom files pointed by |src_path| to the old mojo types.
-
-  Optionally pass |output_dir| to place the result under a separate output
-  directory, preserving the relative path to the file included in |path|.
-  """
-  if os.path.isdir(src_path):
-    DowngradeDirectory(src_path, output_dir)
-  elif os.path.isfile(src_path):
-    DowngradeFile(src_path, output_dir)
-  else:
-    print(">>> {} not pointing to a valid file or directory".format(src_path))
-    sys.exit(1)
-
-
-def main():
-  parser = argparse.ArgumentParser(
-      description="Downgrade *.mojom files to use the old mojo types.")
-  parser.add_argument(
-      "srcpath", help="path to the file or directory to apply the conversion")
-  parser.add_argument(
-      "--outdir", help="the directory to place the converted file(s) under")
-  args = parser.parse_args()
-
-  DowngradePath(args.srcpath, args.outdir)
-
-
-if __name__ == "__main__":
-  sys.exit(main())
diff --git a/utils/ipc/mojo/public/tools/bindings/validate_typemap_config.py b/utils/ipc/mojo/public/tools/bindings/validate_typemap_config.py
index f1783d5..6bb7a20 100755
--- a/utils/ipc/mojo/public/tools/bindings/validate_typemap_config.py
+++ b/utils/ipc/mojo/public/tools/bindings/validate_typemap_config.py
@@ -1,5 +1,5 @@
 #!/usr/bin/env python
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -17,7 +17,8 @@
   ])
   _SUPPORTED_TYPE_KEYS = set([
       'mojom', 'cpp', 'copyable_pass_by_value', 'force_serialize', 'hashable',
-      'move_only', 'nullable_is_same_type'
+      'move_only', 'nullable_is_same_type', 'forward_declaration',
+      'default_constructible'
   ])
   with open(config_filename, 'r') as f:
     for config in json.load(f):
diff --git a/utils/ipc/mojo/public/tools/mojom/BUILD.gn b/utils/ipc/mojo/public/tools/mojom/BUILD.gn
new file mode 100644
index 0000000..eafb95a
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/mojom/BUILD.gn
@@ -0,0 +1,18 @@
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+group("tests") {
+  data = [
+    "check_stable_mojom_compatibility_unittest.py",
+    "check_stable_mojom_compatibility.py",
+    "const_unittest.py",
+    "enum_unittest.py",
+    "feature_unittest.py",
+    "mojom_parser_test_case.py",
+    "mojom_parser_unittest.py",
+    "mojom_parser.py",
+    "stable_attribute_unittest.py",
+    "version_compatibility_unittest.py",
+  ]
+}
diff --git a/utils/ipc/mojo/public/tools/mojom/check_stable_mojom_compatibility.py b/utils/ipc/mojo/public/tools/mojom/check_stable_mojom_compatibility.py
index 08bd672..35cd1cf 100755
--- a/utils/ipc/mojo/public/tools/mojom/check_stable_mojom_compatibility.py
+++ b/utils/ipc/mojo/public/tools/mojom/check_stable_mojom_compatibility.py
@@ -1,5 +1,5 @@
-#!/usr/bin/env python
-# Copyright 2020 The Chromium Authors. All rights reserved.
+#!/usr/bin/env python3
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """Verifies backward-compatibility of mojom type changes.
@@ -12,20 +12,18 @@
 breaking changes to stable mojoms."""
 
 import argparse
-import errno
 import io
 import json
 import os
 import os.path
-import shutil
-import six
 import sys
-import tempfile
 
 from mojom.generate import module
 from mojom.generate import translate
 from mojom.parse import parser
 
+# pylint: disable=raise-missing-from
+
 
 class ParseError(Exception):
   pass
@@ -41,6 +39,8 @@
   transitive closure of a mojom's input dependencies all at once.
   """
 
+  translate.is_running_backwards_compatibility_check_hack = True
+
   # First build a map of all files covered by the delta
   affected_files = set()
   old_files = {}
@@ -73,11 +73,35 @@
     try:
       ast = parser.Parse(contents, mojom)
     except Exception as e:
-      six.reraise(
-          ParseError,
-          'encountered exception {0} while parsing {1}'.format(e, mojom),
-          sys.exc_info()[2])
+      raise ParseError('encountered exception {0} while parsing {1}'.format(
+          e, mojom))
+
+    # Files which are generated at compile time can't be checked by this script
+    # (at the moment) since they may not exist in the output directory.
+    generated_files_to_skip = {
+        ('third_party/blink/public/mojom/runtime_feature_state/'
+         'runtime_feature.mojom'),
+        ('third_party/blink/public/mojom/origin_trial_feature/'
+         'origin_trial_feature.mojom'),
+    }
+
+    ast.import_list.items = [
+        x for x in ast.import_list.items
+        if x.import_filename not in generated_files_to_skip
+    ]
+
     for imp in ast.import_list:
+      if (not file_overrides.get(imp.import_filename)
+          and not os.path.exists(os.path.join(root, imp.import_filename))):
+        # Speculatively construct a path prefix to locate the import_filename
+        mojom_path = os.path.dirname(os.path.normpath(mojom)).split(os.sep)
+        test_prefix = ''
+        for path_component in mojom_path:
+          test_prefix = os.path.join(test_prefix, path_component)
+          test_import_filename = os.path.join(test_prefix, imp.import_filename)
+          if os.path.exists(os.path.join(root, test_import_filename)):
+            imp.import_filename = test_import_filename
+            break
       parseMojom(imp.import_filename, file_overrides, override_modules)
 
     # Now that the transitive set of dependencies has been imported and parsed
@@ -89,10 +113,10 @@
     modules[mojom] = translate.OrderedModule(ast, mojom, all_modules)
 
   old_modules = {}
-  for mojom in old_files.keys():
+  for mojom in old_files:
     parseMojom(mojom, old_files, old_modules)
   new_modules = {}
-  for mojom in new_files.keys():
+  for mojom in new_files:
     parseMojom(mojom, new_files, new_modules)
 
   # At this point we have a complete set of translated Modules from both the
@@ -132,12 +156,21 @@
           'can be deleted by a subsequent change.' % qualified_name)
 
     checker = module.BackwardCompatibilityChecker()
-    if not checker.IsBackwardCompatible(new_types[new_name], kind):
-      raise Exception('Stable type %s appears to have changed in a way which '
-                      'breaks backward-compatibility. Please fix!\n\nIf you '
-                      'believe this assessment to be incorrect, please file a '
-                      'Chromium bug against the "Internals>Mojo>Bindings" '
-                      'component.' % qualified_name)
+    try:
+      if not checker.IsBackwardCompatible(new_types[new_name], kind):
+        raise Exception(
+            'Stable type %s appears to have changed in a way which '
+            'breaks backward-compatibility. Please fix!\n\nIf you '
+            'believe this assessment to be incorrect, please file a '
+            'Chromium bug against the "Internals>Mojo>Bindings" '
+            'component.' % qualified_name)
+    except Exception as e:
+      raise Exception(
+          'Stable type %s appears to have changed in a way which '
+          'breaks backward-compatibility: \n\n%s.\nPlease fix!\n\nIf you '
+          'believe this assessment to be incorrect, please file a '
+          'Chromium bug against the "Internals>Mojo>Bindings" '
+          'component.' % (qualified_name, e))
 
 
 def Run(command_line, delta=None):
diff --git a/utils/ipc/mojo/public/tools/mojom/check_stable_mojom_compatibility_unittest.py b/utils/ipc/mojo/public/tools/mojom/check_stable_mojom_compatibility_unittest.py
index 9f51ea7..06769c9 100755
--- a/utils/ipc/mojo/public/tools/mojom/check_stable_mojom_compatibility_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/check_stable_mojom_compatibility_unittest.py
@@ -1,5 +1,5 @@
-#!/usr/bin/env python
-# Copyright 2020 The Chromium Authors. All rights reserved.
+#!/usr/bin/env python3
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -15,7 +15,7 @@
 from mojom.generate import module
 
 
-class Change(object):
+class Change:
   """Helper to clearly define a mojom file delta to be analyzed."""
 
   def __init__(self, filename, old=None, new=None):
@@ -28,7 +28,7 @@
 
 class UnchangedFile(Change):
   def __init__(self, filename, contents):
-    super(UnchangedFile, self).__init__(filename, old=contents, new=contents)
+    super().__init__(filename, old=contents, new=contents)
 
 
 class CheckStableMojomCompatibilityTest(unittest.TestCase):
@@ -258,3 +258,82 @@
                [Stable] struct T { foo.S s; int32 x; };
                """)
     ])
+
+  def testWithPartialImport(self):
+    """The compatibility checking tool correctly parses imports with partial
+    paths."""
+    self.assertBackwardCompatible([
+        UnchangedFile('foo/foo.mojom', 'module foo; [Stable] struct S {};'),
+        Change('foo/bar.mojom',
+               old="""\
+               module bar;
+               import "foo/foo.mojom";
+               [Stable] struct T { foo.S s; };
+               """,
+               new="""\
+               module bar;
+               import "foo.mojom";
+               [Stable] struct T { foo.S s; };
+               """)
+    ])
+
+    self.assertBackwardCompatible([
+        UnchangedFile('foo/foo.mojom', 'module foo; [Stable] struct S {};'),
+        Change('foo/bar.mojom',
+               old="""\
+               module bar;
+               import "foo.mojom";
+               [Stable] struct T { foo.S s; };
+               """,
+               new="""\
+               module bar;
+               import "foo/foo.mojom";
+               [Stable] struct T { foo.S s; };
+               """)
+    ])
+
+    self.assertNotBackwardCompatible([
+        UnchangedFile('foo/foo.mojom', 'module foo; [Stable] struct S {};'),
+        Change('bar/bar.mojom',
+               old="""\
+               module bar;
+               import "foo/foo.mojom";
+               [Stable] struct T { foo.S s; };
+               """,
+               new="""\
+               module bar;
+               import "foo.mojom";
+               [Stable] struct T { foo.S s; };
+               """)
+    ])
+
+    self.assertNotBackwardCompatible([
+        UnchangedFile('foo/foo.mojom', 'module foo; [Stable] struct S {};'),
+        Change('bar/bar.mojom',
+               old="""\
+               module bar;
+               import "foo.mojom";
+               [Stable] struct T { foo.S s; };
+               """,
+               new="""\
+               module bar;
+               import "foo/foo.mojom";
+               [Stable] struct T { foo.S s; };
+               """)
+    ])
+
+  def testNewEnumDefault(self):
+    # Should be backwards compatible since it does not affect the wire format.
+    # This specific case also checks that the backwards compatibility checker
+    # does not throw an error due to the older version of the enum not
+    # specifying [Default].
+    self.assertBackwardCompatible([
+        Change('foo/foo.mojom',
+               old='[Extensible] enum E { One };',
+               new='[Extensible] enum E { [Default] One };')
+    ])
+    self.assertBackwardCompatible([
+        Change('foo/foo.mojom',
+               old='[Extensible] enum E { [Default] One, Two, };',
+               new='[Extensible] enum E { One, [Default] Two, };')
+    ])
diff --git a/utils/ipc/mojo/public/tools/mojom/const_unittest.py b/utils/ipc/mojo/public/tools/mojom/const_unittest.py
index cb42dfa..e8ed36a 100644
--- a/utils/ipc/mojo/public/tools/mojom/const_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/const_unittest.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
diff --git a/utils/ipc/mojo/public/tools/mojom/enum_unittest.py b/utils/ipc/mojo/public/tools/mojom/enum_unittest.py
index d900507..9269cde 100644
--- a/utils/ipc/mojo/public/tools/mojom/enum_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/enum_unittest.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -90,3 +90,31 @@
     self.assertEqual('F', b.enums[0].mojom_name)
     self.assertEqual('kFoo', b.enums[0].fields[0].mojom_name)
     self.assertEqual(37, b.enums[0].fields[0].numeric_value)
+
+  def testEnumAttributesAreEnums(self):
+    """Verifies that enum values in attributes are really enum types."""
+    a_mojom = 'a.mojom'
+    self.WriteFile(a_mojom, 'module a; enum E { kFoo, kBar };')
+    b_mojom = 'b.mojom'
+    self.WriteFile(
+        b_mojom, 'module b;'
+        'import "a.mojom";'
+        '[MooCow=a.E.kFoo]'
+        'interface Foo { Foo(); };')
+    self.ParseMojoms([a_mojom, b_mojom])
+    b = self.LoadModule(b_mojom)
+    self.assertEqual(b.interfaces[0].attributes['MooCow'].mojom_name, 'kFoo')
+
+  def testConstantAttributes(self):
+    """Verifies that constants as attributes are translated to the constant."""
+    a_mojom = 'a.mojom'
+    self.WriteFile(
+        a_mojom, 'module a;'
+        'enum E { kFoo, kBar };'
+        'const E kB = E.kFoo;'
+        '[Attr=kB] interface Hello { Foo(); };')
+    self.ParseMojoms([a_mojom])
+    a = self.LoadModule(a_mojom)
+    self.assertEqual(a.interfaces[0].attributes['Attr'].mojom_name, 'kB')
+    self.assertEquals(a.interfaces[0].attributes['Attr'].value.mojom_name,
+                      'kFoo')
diff --git a/utils/ipc/mojo/public/tools/mojom/feature_unittest.py b/utils/ipc/mojo/public/tools/mojom/feature_unittest.py
new file mode 100644
index 0000000..5f014e1
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/mojom/feature_unittest.py
@@ -0,0 +1,84 @@
+# Copyright 2023 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from mojom_parser_test_case import MojomParserTestCase
+
+
+class FeatureTest(MojomParserTestCase):
+  """Tests feature parsing behavior."""
+  def testFeatureOff(self):
+    """Verifies basic parsing of feature types."""
+    types = self.ExtractTypes("""
+      // e.g. BASE_DECLARE_FEATURE(kFeature);
+      [AttributeOne=ValueOne]
+      feature kFeature {
+        // BASE_FEATURE(kFeature,"MyFeature",
+        //     base::FEATURE_DISABLED_BY_DEFAULT);
+        const string name = "MyFeature";
+        const bool default_state = false;
+      };
+    """)
+    self.assertEqual('name', types['kFeature'].constants[0].mojom_name)
+    self.assertEqual('"MyFeature"', types['kFeature'].constants[0].value)
+    self.assertEqual('default_state', types['kFeature'].constants[1].mojom_name)
+    self.assertEqual('false', types['kFeature'].constants[1].value)
+
+  def testFeatureOn(self):
+    """Verifies basic parsing of feature types."""
+    types = self.ExtractTypes("""
+      // e.g. BASE_DECLARE_FEATURE(kFeature);
+      feature kFeature {
+        // BASE_FEATURE(kFeature,"MyFeature",
+        //     base::FEATURE_ENABLED_BY_DEFAULT);
+        const string name = "MyFeature";
+        const bool default_state = true;
+      };
+    """)
+    self.assertEqual('name', types['kFeature'].constants[0].mojom_name)
+    self.assertEqual('"MyFeature"', types['kFeature'].constants[0].value)
+    self.assertEqual('default_state', types['kFeature'].constants[1].mojom_name)
+    self.assertEqual('true', types['kFeature'].constants[1].value)
+
+  def testFeatureWeakKeyword(self):
+    """Verifies that `feature` is a weak keyword."""
+    types = self.ExtractTypes("""
+      // e.g. BASE_DECLARE_FEATURE(kFeature);
+      [AttributeOne=ValueOne]
+      feature kFeature {
+        // BASE_FEATURE(kFeature,"MyFeature",
+        //     base::FEATURE_DISABLED_BY_DEFAULT);
+        const string name = "MyFeature";
+        const bool default_state = false;
+      };
+      struct MyStruct {
+         bool feature = true;
+      };
+      interface InterfaceName {
+         Method(string feature) => (int32 feature);
+      };
+    """)
+    self.assertEqual('name', types['kFeature'].constants[0].mojom_name)
+    self.assertEqual('"MyFeature"', types['kFeature'].constants[0].value)
+    self.assertEqual('default_state', types['kFeature'].constants[1].mojom_name)
+    self.assertEqual('false', types['kFeature'].constants[1].value)
+
+  def testFeatureAttributesAreFeatures(self):
+    """Verifies that feature values in attributes are really feature types."""
+    a_mojom = 'a.mojom'
+    self.WriteFile(
+        a_mojom, 'module a;'
+        'feature F { const string name = "f";'
+        'const bool default_state = false; };')
+    b_mojom = 'b.mojom'
+    self.WriteFile(
+        b_mojom, 'module b;'
+        'import "a.mojom";'
+        'feature G'
+        '{const string name = "g"; const bool default_state = false;};'
+        '[Attri=a.F] interface Foo { Foo(); };'
+        '[Boink=G] interface Bar {};')
+    self.ParseMojoms([a_mojom, b_mojom])
+    b = self.LoadModule(b_mojom)
+    self.assertEqual(b.interfaces[0].attributes['Attri'].mojom_name, 'F')
+    self.assertEqual(b.interfaces[1].attributes['Boink'].mojom_name, 'G')
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/BUILD.gn b/utils/ipc/mojo/public/tools/mojom/mojom/BUILD.gn
index 51facc0..a0edf0e 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/BUILD.gn
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/BUILD.gn
@@ -1,4 +1,4 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -8,6 +8,7 @@
     "error.py",
     "fileutil.py",
     "generate/__init__.py",
+    "generate/check.py",
     "generate/generator.py",
     "generate/module.py",
     "generate/pack.py",
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/error.py b/utils/ipc/mojo/public/tools/mojom/mojom/error.py
index 8a1e03d..dd53b83 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/error.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/error.py
@@ -1,4 +1,4 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/fileutil.py b/utils/ipc/mojo/public/tools/mojom/mojom/fileutil.py
index bf626f5..124f12c 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/fileutil.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/fileutil.py
@@ -1,9 +1,8 @@
-# Copyright 2015 The Chromium Authors. All rights reserved.
+# Copyright 2015 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
 import errno
-import imp
 import os.path
 import sys
 
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/fileutil_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom/fileutil_unittest.py
index ff5753a..c93d228 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/fileutil_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/fileutil_unittest.py
@@ -1,20 +1,17 @@
-# Copyright 2015 The Chromium Authors. All rights reserved.
+# Copyright 2015 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import imp
 import os.path
 import shutil
-import sys
 import tempfile
 import unittest
 
 from mojom import fileutil
 
-
 class FileUtilTest(unittest.TestCase):
   def testEnsureDirectoryExists(self):
-    """Test that EnsureDirectoryExists fuctions correctly."""
+    """Test that EnsureDirectoryExists functions correctly."""
 
     temp_dir = tempfile.mkdtemp()
     try:
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/check.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/check.py
new file mode 100644
index 0000000..1efe202
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/check.py
@@ -0,0 +1,26 @@
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+"""Code shared by the various pre-generation mojom checkers."""
+
+
+class CheckException(Exception):
+  def __init__(self, module, message):
+    self.module = module
+    self.message = message
+    super().__init__(self.message)
+
+  def __str__(self):
+    return "Failed mojo pre-generation check for {}:\n{}".format(
+        self.module.path, self.message)
+
+
+class Check:
+  def __init__(self, module):
+    self.module = module
+
+  def CheckModule(self):
+    """ Subclass should return True if its Checks pass, and throw an
+    exception otherwise. CheckModule will be called immediately before
+    mojom.generate.Generator.GenerateFiles()"""
+    raise NotImplementedError("Subclasses must override/implement this method")
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/constant_resolver.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/constant_resolver.py
deleted file mode 100644
index 0dfd996..0000000
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/constant_resolver.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# Copyright 2015 The Chromium Authors. All rights reserved.
-# Use of this source code is governed by a BSD-style license that can be
-# found in the LICENSE file.
-"""Resolves the values used for constants and enums."""
-
-from itertools import ifilter
-
-from mojom.generate import module as mojom
-
-
-def ResolveConstants(module, expression_to_text):
-  in_progress = set()
-  computed = set()
-
-  def GetResolvedValue(named_value):
-    assert isinstance(named_value, (mojom.EnumValue, mojom.ConstantValue))
-    if isinstance(named_value, mojom.EnumValue):
-      field = next(
-          ifilter(lambda field: field.name == named_value.name,
-                  named_value.enum.fields), None)
-      if not field:
-        raise RuntimeError(
-            'Unable to get computed value for field %s of enum %s' %
-            (named_value.name, named_value.enum.name))
-      if field not in computed:
-        ResolveEnum(named_value.enum)
-      return field.resolved_value
-    else:
-      ResolveConstant(named_value.constant)
-      named_value.resolved_value = named_value.constant.resolved_value
-      return named_value.resolved_value
-
-  def ResolveConstant(constant):
-    if constant in computed:
-      return
-    if constant in in_progress:
-      raise RuntimeError('Circular dependency for constant: %s' % constant.name)
-    in_progress.add(constant)
-    if isinstance(constant.value, (mojom.EnumValue, mojom.ConstantValue)):
-      resolved_value = GetResolvedValue(constant.value)
-    else:
-      resolved_value = expression_to_text(constant.value)
-    constant.resolved_value = resolved_value
-    in_progress.remove(constant)
-    computed.add(constant)
-
-  def ResolveEnum(enum):
-    def ResolveEnumField(enum, field, default_value):
-      if field in computed:
-        return
-      if field in in_progress:
-        raise RuntimeError('Circular dependency for enum: %s' % enum.name)
-      in_progress.add(field)
-      if field.value:
-        if isinstance(field.value, mojom.EnumValue):
-          resolved_value = GetResolvedValue(field.value)
-        elif isinstance(field.value, str):
-          resolved_value = int(field.value, 0)
-        else:
-          raise RuntimeError('Unexpected value: %s' % field.value)
-      else:
-        resolved_value = default_value
-      field.resolved_value = resolved_value
-      in_progress.remove(field)
-      computed.add(field)
-
-    current_value = 0
-    for field in enum.fields:
-      ResolveEnumField(enum, field, current_value)
-      current_value = field.resolved_value + 1
-
-  for constant in module.constants:
-    ResolveConstant(constant)
-
-  for enum in module.enums:
-    ResolveEnum(enum)
-
-  for struct in module.structs:
-    for constant in struct.constants:
-      ResolveConstant(constant)
-    for enum in struct.enums:
-      ResolveEnum(enum)
-    for field in struct.fields:
-      if isinstance(field.default, (mojom.ConstantValue, mojom.EnumValue)):
-        field.default.resolved_value = GetResolvedValue(field.default)
-
-  for interface in module.interfaces:
-    for constant in interface.constants:
-      ResolveConstant(constant)
-    for enum in interface.enums:
-      ResolveEnum(enum)
-
-  return module
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/generator.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/generator.py
index 4a1c73f..96fe3a2 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/generator.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/generator.py
@@ -1,4 +1,4 @@
-# Copyright 2013 The Chromium Authors. All rights reserved.
+# Copyright 2013 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """Code shared by the various language-specific code generators."""
@@ -97,7 +97,7 @@
   return _ToSnakeCase(identifier, upper=False)
 
 
-class Stylizer(object):
+class Stylizer:
   """Stylizers specify naming rules to map mojom names to names in generated
   code. For example, if you would like method_name in mojom to be mapped to
   MethodName in the generated code, you need to define a subclass of Stylizer
@@ -130,6 +130,9 @@
   def StylizeEnum(self, mojom_name):
     return mojom_name
 
+  def StylizeFeature(self, mojom_name):
+    return mojom_name
+
   def StylizeModule(self, mojom_namespace):
     return mojom_namespace
 
@@ -233,7 +236,7 @@
     _AddInterfaceComputedData(interface)
 
 
-class Generator(object):
+class Generator:
   # Pass |output_dir| to emit files to disk. Omit |output_dir| to echo all
   # files to stdout.
   def __init__(self,
@@ -243,7 +246,6 @@
                variant=None,
                bytecode_path=None,
                for_blink=False,
-               js_bindings_mode="new",
                js_generate_struct_deserializers=False,
                export_attribute=None,
                export_header=None,
@@ -262,7 +264,6 @@
     self.variant = variant
     self.bytecode_path = bytecode_path
     self.for_blink = for_blink
-    self.js_bindings_mode = js_bindings_mode
     self.js_generate_struct_deserializers = js_generate_struct_deserializers
     self.export_attribute = export_attribute
     self.export_header = export_header
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/generator_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/generator_unittest.py
index 32c884a..7143e07 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/generator_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/generator_unittest.py
@@ -1,13 +1,12 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import imp
+import importlib.util
 import os.path
 import sys
 import unittest
 
-
 def _GetDirAbove(dirname):
   """Returns the directory "above" this file containing |dirname| (which must
   also be "above" this file)."""
@@ -20,12 +19,11 @@
 
 
 try:
-  imp.find_module("mojom")
+  importlib.util.find_spec("mojom")
 except ImportError:
   sys.path.append(os.path.join(_GetDirAbove("pylib"), "pylib"))
 from mojom.generate import generator
 
-
 class StringManipulationTest(unittest.TestCase):
   """generator contains some string utilities, this tests only those."""
 
@@ -69,6 +67,5 @@
     self.assertEquals("SNAKE_D3D11_CASE",
                       generator.ToUpperSnakeCase("snakeD3d11Case"))
 
-
 if __name__ == "__main__":
   unittest.main()
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/module.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/module.py
index 9bdb28e..ca71059 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/module.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/module.py
@@ -1,4 +1,4 @@
-# Copyright 2013 The Chromium Authors. All rights reserved.
+# Copyright 2013 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -12,15 +12,14 @@
 # method = interface.AddMethod('Tat', 0)
 # method.AddParameter('baz', 0, mojom.INT32)
 
-import sys
-if sys.version_info.major == 2:
-  import cPickle as pickle
-else:
-  import pickle
+import pickle
+from collections import OrderedDict
 from uuid import UUID
 
+# pylint: disable=raise-missing-from
 
-class BackwardCompatibilityChecker(object):
+
+class BackwardCompatibilityChecker:
   """Used for memoization while recursively checking two type definitions for
   backward-compatibility."""
 
@@ -64,23 +63,20 @@
     return obj.Repr(as_ref=as_ref)
   # Since we cannot implement Repr for existing container types, we
   # handle them here.
-  elif isinstance(obj, list):
+  if isinstance(obj, list):
     if not obj:
       return '[]'
-    else:
-      return ('[\n%s\n]' % (',\n'.join(
-          '    %s' % Repr(elem, as_ref).replace('\n', '\n    ')
-          for elem in obj)))
-  elif isinstance(obj, dict):
+    return ('[\n%s\n]' %
+            (',\n'.join('    %s' % Repr(elem, as_ref).replace('\n', '\n    ')
+                        for elem in obj)))
+  if isinstance(obj, dict):
     if not obj:
       return '{}'
-    else:
-      return ('{\n%s\n}' % (',\n'.join(
-          '    %s: %s' % (Repr(key, as_ref).replace('\n', '\n    '),
-                          Repr(val, as_ref).replace('\n', '\n    '))
-          for key, val in obj.items())))
-  else:
-    return repr(obj)
+    return ('{\n%s\n}' % (',\n'.join('    %s: %s' %
+                                     (Repr(key, as_ref).replace('\n', '\n    '),
+                                      Repr(val, as_ref).replace('\n', '\n    '))
+                                     for key, val in obj.items())))
+  return repr(obj)
 
 
 def GenericRepr(obj, names):
@@ -104,7 +100,7 @@
       ReprIndent(name, as_ref) for (name, as_ref) in names.items()))
 
 
-class Kind(object):
+class Kind:
   """Kind represents a type (e.g. int8, string).
 
   Attributes:
@@ -112,16 +108,43 @@
     module: {Module} The defining module. Set to None for built-in types.
     parent_kind: The enclosing type. For example, an enum defined
         inside an interface has that interface as its parent. May be None.
+    is_nullable: True if the type is nullable.
   """
 
-  def __init__(self, spec=None, module=None):
+  def __init__(self, spec=None, is_nullable=False, module=None):
     self.spec = spec
     self.module = module
     self.parent_kind = None
+    self.is_nullable = is_nullable
+    self.shared_definition = {}
+
+  @classmethod
+  def AddSharedProperty(cls, name):
+    """Adds a property |name| to |cls|, which accesses the corresponding item in
+       |shared_definition|.
+
+       The reason of adding such indirection is to enable sharing definition
+       between a reference kind and its nullable variation. For example:
+         a = Struct('test_struct_1')
+         b = a.MakeNullableKind()
+         a.name = 'test_struct_2'
+         print(b.name)  # Outputs 'test_struct_2'.
+    """
+    def Get(self):
+      try:
+        return self.shared_definition[name]
+      except KeyError:  # Must raise AttributeError if property doesn't exist.
+        raise AttributeError
+
+    def Set(self, value):
+      self.shared_definition[name] = value
+
+    setattr(cls, name, property(Get, Set))
 
   def Repr(self, as_ref=True):
     # pylint: disable=unused-argument
-    return '<%s spec=%r>' % (self.__class__.__name__, self.spec)
+    return '<%s spec=%r is_nullable=%r>' % (self.__class__.__name__, self.spec,
+                                            self.is_nullable)
 
   def __repr__(self):
     # Gives us a decent __repr__ for all kinds.
@@ -130,7 +153,8 @@
   def __eq__(self, rhs):
     # pylint: disable=unidiomatic-typecheck
     return (type(self) == type(rhs)
-            and (self.spec, self.parent_kind) == (rhs.spec, rhs.parent_kind))
+            and (self.spec, self.parent_kind, self.is_nullable)
+            == (rhs.spec, rhs.parent_kind, rhs.is_nullable))
 
   def __hash__(self):
     # TODO(crbug.com/1060471): Remove this and other __hash__ methods on Kind
@@ -138,32 +162,113 @@
     # some primitive Kinds as dict keys. The default hash (object identity)
     # breaks these dicts when a pickled Module instance is unpickled and used
     # during a subsequent run of the parser.
-    return hash((self.spec, self.parent_kind))
+    return hash((self.spec, self.parent_kind, self.is_nullable))
 
   # pylint: disable=unused-argument
   def IsBackwardCompatible(self, rhs, checker):
     return self == rhs
 
 
+class ValueKind(Kind):
+  """ValueKind represents values that aren't reference kinds.
+
+  The primary difference is the wire representation for nullable value kinds
+  still reserves space for the value type itself, even if that value itself
+  is logically null.
+  """
+  def __init__(self, spec=None, is_nullable=False, module=None):
+    assert spec is None or is_nullable == spec.startswith('?')
+    Kind.__init__(self, spec, is_nullable, module)
+
+  def MakeNullableKind(self):
+    assert not self.is_nullable
+
+    if self == BOOL:
+      return NULLABLE_BOOL
+    if self == INT8:
+      return NULLABLE_INT8
+    if self == INT16:
+      return NULLABLE_INT16
+    if self == INT32:
+      return NULLABLE_INT32
+    if self == INT64:
+      return NULLABLE_INT64
+    if self == UINT8:
+      return NULLABLE_UINT8
+    if self == UINT16:
+      return NULLABLE_UINT16
+    if self == UINT32:
+      return NULLABLE_UINT32
+    if self == UINT64:
+      return NULLABLE_UINT64
+    if self == FLOAT:
+      return NULLABLE_FLOAT
+    if self == DOUBLE:
+      return NULLABLE_DOUBLE
+
+    nullable_kind = type(self)()
+    nullable_kind.shared_definition = self.shared_definition
+    if self.spec is not None:
+      nullable_kind.spec = '?' + self.spec
+    nullable_kind.is_nullable = True
+    nullable_kind.parent_kind = self.parent_kind
+    nullable_kind.module = self.module
+
+    return nullable_kind
+
+  def MakeUnnullableKind(self):
+    assert self.is_nullable
+
+    if self == NULLABLE_BOOL:
+      return BOOL
+    if self == NULLABLE_INT8:
+      return INT8
+    if self == NULLABLE_INT16:
+      return INT16
+    if self == NULLABLE_INT32:
+      return INT32
+    if self == NULLABLE_INT64:
+      return INT64
+    if self == NULLABLE_UINT8:
+      return UINT8
+    if self == NULLABLE_UINT16:
+      return UINT16
+    if self == NULLABLE_UINT32:
+      return UINT32
+    if self == NULLABLE_UINT64:
+      return UINT64
+    if self == NULLABLE_FLOAT:
+      return FLOAT
+    if self == NULLABLE_DOUBLE:
+      return DOUBLE
+
+    nullable_kind = type(self)()
+    nullable_kind.shared_definition = self.shared_definition
+    if self.spec is not None:
+      nullable_kind.spec = self.spec[1:]
+    nullable_kind.is_nullable = False
+    nullable_kind.parent_kind = self.parent_kind
+    nullable_kind.module = self.module
+
+    return nullable_kind
+
+  def __eq__(self, rhs):
+    return (isinstance(rhs, ValueKind) and super().__eq__(rhs))
+
+  def __hash__(self):  # pylint: disable=useless-super-delegation
+    return super().__hash__()
+
+
 class ReferenceKind(Kind):
   """ReferenceKind represents pointer and handle types.
 
   A type is nullable if null (for pointer types) or invalid handle (for handle
   types) is a legal value for the type.
-
-  Attributes:
-    is_nullable: True if the type is nullable.
   """
 
   def __init__(self, spec=None, is_nullable=False, module=None):
     assert spec is None or is_nullable == spec.startswith('?')
-    Kind.__init__(self, spec, module)
-    self.is_nullable = is_nullable
-    self.shared_definition = {}
-
-  def Repr(self, as_ref=True):
-    return '<%s spec=%r is_nullable=%r>' % (self.__class__.__name__, self.spec,
-                                            self.is_nullable)
+    Kind.__init__(self, spec, is_nullable, module)
 
   def MakeNullableKind(self):
     assert not self.is_nullable
@@ -193,55 +298,65 @@
 
     return nullable_kind
 
-  @classmethod
-  def AddSharedProperty(cls, name):
-    """Adds a property |name| to |cls|, which accesses the corresponding item in
-       |shared_definition|.
+  def MakeUnnullableKind(self):
+    assert self.is_nullable
 
-       The reason of adding such indirection is to enable sharing definition
-       between a reference kind and its nullable variation. For example:
-         a = Struct('test_struct_1')
-         b = a.MakeNullableKind()
-         a.name = 'test_struct_2'
-         print(b.name)  # Outputs 'test_struct_2'.
-    """
+    if self == NULLABLE_STRING:
+      return STRING
+    if self == NULLABLE_HANDLE:
+      return HANDLE
+    if self == NULLABLE_DCPIPE:
+      return DCPIPE
+    if self == NULLABLE_DPPIPE:
+      return DPPIPE
+    if self == NULLABLE_MSGPIPE:
+      return MSGPIPE
+    if self == NULLABLE_SHAREDBUFFER:
+      return SHAREDBUFFER
+    if self == NULLABLE_PLATFORMHANDLE:
+      return PLATFORMHANDLE
 
-    def Get(self):
-      try:
-        return self.shared_definition[name]
-      except KeyError:  # Must raise AttributeError if property doesn't exist.
-        raise AttributeError
+    unnullable_kind = type(self)()
+    unnullable_kind.shared_definition = self.shared_definition
+    if self.spec is not None:
+      assert self.spec[0] == '?'
+      unnullable_kind.spec = self.spec[1:]
+    unnullable_kind.is_nullable = False
+    unnullable_kind.parent_kind = self.parent_kind
+    unnullable_kind.module = self.module
 
-    def Set(self, value):
-      self.shared_definition[name] = value
-
-    setattr(cls, name, property(Get, Set))
+    return unnullable_kind
 
   def __eq__(self, rhs):
-    return (isinstance(rhs, ReferenceKind)
-            and super(ReferenceKind, self).__eq__(rhs)
-            and self.is_nullable == rhs.is_nullable)
+    return (isinstance(rhs, ReferenceKind) and super().__eq__(rhs))
 
-  def __hash__(self):
-    return hash((super(ReferenceKind, self).__hash__(), self.is_nullable))
-
-  def IsBackwardCompatible(self, rhs, checker):
-    return (super(ReferenceKind, self).IsBackwardCompatible(rhs, checker)
-            and self.is_nullable == rhs.is_nullable)
+  def __hash__(self):  # pylint: disable=useless-super-delegation
+    return super().__hash__()
 
 
 # Initialize the set of primitive types. These can be accessed by clients.
-BOOL = Kind('b')
-INT8 = Kind('i8')
-INT16 = Kind('i16')
-INT32 = Kind('i32')
-INT64 = Kind('i64')
-UINT8 = Kind('u8')
-UINT16 = Kind('u16')
-UINT32 = Kind('u32')
-UINT64 = Kind('u64')
-FLOAT = Kind('f')
-DOUBLE = Kind('d')
+BOOL = ValueKind('b')
+INT8 = ValueKind('i8')
+INT16 = ValueKind('i16')
+INT32 = ValueKind('i32')
+INT64 = ValueKind('i64')
+UINT8 = ValueKind('u8')
+UINT16 = ValueKind('u16')
+UINT32 = ValueKind('u32')
+UINT64 = ValueKind('u64')
+FLOAT = ValueKind('f')
+DOUBLE = ValueKind('d')
+NULLABLE_BOOL = ValueKind('?b', True)
+NULLABLE_INT8 = ValueKind('?i8', True)
+NULLABLE_INT16 = ValueKind('?i16', True)
+NULLABLE_INT32 = ValueKind('?i32', True)
+NULLABLE_INT64 = ValueKind('?i64', True)
+NULLABLE_UINT8 = ValueKind('?u8', True)
+NULLABLE_UINT16 = ValueKind('?u16', True)
+NULLABLE_UINT32 = ValueKind('?u32', True)
+NULLABLE_UINT64 = ValueKind('?u64', True)
+NULLABLE_FLOAT = ValueKind('?f', True)
+NULLABLE_DOUBLE = ValueKind('?d', True)
 STRING = ReferenceKind('s')
 HANDLE = ReferenceKind('h')
 DCPIPE = ReferenceKind('h:d:c')
@@ -270,6 +385,17 @@
     UINT64,
     FLOAT,
     DOUBLE,
+    NULLABLE_BOOL,
+    NULLABLE_INT8,
+    NULLABLE_INT16,
+    NULLABLE_INT32,
+    NULLABLE_INT64,
+    NULLABLE_UINT8,
+    NULLABLE_UINT16,
+    NULLABLE_UINT32,
+    NULLABLE_UINT64,
+    NULLABLE_FLOAT,
+    NULLABLE_DOUBLE,
     STRING,
     HANDLE,
     DCPIPE,
@@ -291,12 +417,17 @@
 ATTRIBUTE_EXTENSIBLE = 'Extensible'
 ATTRIBUTE_NO_INTERRUPT = 'NoInterrupt'
 ATTRIBUTE_STABLE = 'Stable'
+ATTRIBUTE_SUPPORTS_URGENT = 'SupportsUrgent'
 ATTRIBUTE_SYNC = 'Sync'
 ATTRIBUTE_UNLIMITED_SIZE = 'UnlimitedSize'
 ATTRIBUTE_UUID = 'Uuid'
+ATTRIBUTE_SERVICE_SANDBOX = 'ServiceSandbox'
+ATTRIBUTE_REQUIRE_CONTEXT = 'RequireContext'
+ATTRIBUTE_ALLOWED_CONTEXT = 'AllowedContext'
+ATTRIBUTE_RUNTIME_FEATURE = 'RuntimeFeature'
 
 
-class NamedValue(object):
+class NamedValue:
   def __init__(self, module, parent_kind, mojom_name):
     self.module = module
     self.parent_kind = parent_kind
@@ -316,7 +447,7 @@
     return hash((self.parent_kind, self.mojom_name))
 
 
-class BuiltinValue(object):
+class BuiltinValue:
   def __init__(self, value):
     self.value = value
 
@@ -350,7 +481,7 @@
     return self.field.name
 
 
-class Constant(object):
+class Constant:
   def __init__(self, mojom_name=None, kind=None, value=None, parent_kind=None):
     self.mojom_name = mojom_name
     self.name = None
@@ -368,7 +499,7 @@
                                        rhs.parent_kind))
 
 
-class Field(object):
+class Field:
   def __init__(self,
                mojom_name=None,
                kind=None,
@@ -414,7 +545,18 @@
 
 
 class UnionField(Field):
-  pass
+  def __init__(self,
+               mojom_name=None,
+               kind=None,
+               ordinal=None,
+               default=None,
+               attributes=None):
+    Field.__init__(self, mojom_name, kind, ordinal, default, attributes)
+
+  @property
+  def is_default(self):
+    return self.attributes.get(ATTRIBUTE_DEFAULT, False) \
+        if self.attributes else False
 
 
 def _IsFieldBackwardCompatible(new_field, old_field, checker):
@@ -424,6 +566,38 @@
   return checker.IsBackwardCompatible(new_field.kind, old_field.kind)
 
 
+class Feature(ReferenceKind):
+  """A runtime enabled feature defined from mojom.
+
+  Attributes:
+    mojom_name: {str} The name of the feature type as defined in mojom.
+    name: {str} The stylized name. (Note: not the "name" used by FeatureList.)
+    constants: {List[Constant]} The constants defined in the feature scope.
+    attributes: {dict} Additional information about the feature.
+  """
+
+  Kind.AddSharedProperty('mojom_name')
+  Kind.AddSharedProperty('name')
+  Kind.AddSharedProperty('constants')
+  Kind.AddSharedProperty('attributes')
+
+  def __init__(self, mojom_name=None, module=None, attributes=None):
+    if mojom_name is not None:
+      spec = 'x:' + mojom_name
+    else:
+      spec = None
+    ReferenceKind.__init__(self, spec, False, module)
+    self.mojom_name = mojom_name
+    self.name = None
+    self.constants = []
+    self.attributes = attributes
+
+  def Stylize(self, stylizer):
+    self.name = stylizer.StylizeFeature(self.mojom_name)
+    for constant in self.constants:
+      constant.Stylize(stylizer)
+
+
 class Struct(ReferenceKind):
   """A struct with typed fields.
 
@@ -441,14 +615,14 @@
         if it's a native struct.
   """
 
-  ReferenceKind.AddSharedProperty('mojom_name')
-  ReferenceKind.AddSharedProperty('name')
-  ReferenceKind.AddSharedProperty('native_only')
-  ReferenceKind.AddSharedProperty('custom_serializer')
-  ReferenceKind.AddSharedProperty('fields')
-  ReferenceKind.AddSharedProperty('enums')
-  ReferenceKind.AddSharedProperty('constants')
-  ReferenceKind.AddSharedProperty('attributes')
+  Kind.AddSharedProperty('mojom_name')
+  Kind.AddSharedProperty('name')
+  Kind.AddSharedProperty('native_only')
+  Kind.AddSharedProperty('custom_serializer')
+  Kind.AddSharedProperty('fields')
+  Kind.AddSharedProperty('enums')
+  Kind.AddSharedProperty('constants')
+  Kind.AddSharedProperty('attributes')
 
   def __init__(self, mojom_name=None, module=None, attributes=None):
     if mojom_name is not None:
@@ -470,12 +644,11 @@
       return '<%s mojom_name=%r module=%s>' % (self.__class__.__name__,
                                                self.mojom_name,
                                                Repr(self.module, as_ref=True))
-    else:
-      return GenericRepr(self, {
-          'mojom_name': False,
-          'fields': False,
-          'module': True
-      })
+    return GenericRepr(self, {
+        'mojom_name': False,
+        'fields': False,
+        'module': True
+    })
 
   def AddField(self,
                mojom_name,
@@ -496,13 +669,13 @@
     for constant in self.constants:
       constant.Stylize(stylizer)
 
-  def IsBackwardCompatible(self, older_struct, checker):
-    """This struct is backward-compatible with older_struct if and only if all
-    of the following conditions hold:
+  def IsBackwardCompatible(self, rhs, checker):
+    """This struct is backward-compatible with rhs (older_struct) if and only if
+    all of the following conditions hold:
       - Any newly added field is tagged with a [MinVersion] attribute specifying
         a version number greater than all previously used [MinVersion]
         attributes within the struct.
-      - All fields present in older_struct remain present in the new struct,
+      - All fields present in rhs remain present in the new struct,
         with the same ordinal position, same optional or non-optional status,
         same (or backward-compatible) type and where applicable, the same
         [MinVersion] attribute value.
@@ -521,7 +694,7 @@
       return fields_by_ordinal
 
     new_fields = buildOrdinalFieldMap(self)
-    old_fields = buildOrdinalFieldMap(older_struct)
+    old_fields = buildOrdinalFieldMap(rhs)
     if len(new_fields) < len(old_fields):
       # At least one field was removed, which is not OK.
       return False
@@ -574,11 +747,18 @@
       prefix = self.module.GetNamespacePrefix()
     return '%s%s' % (prefix, self.mojom_name)
 
+  def _tuple(self):
+    return (self.mojom_name, self.native_only, self.fields, self.constants,
+            self.attributes)
+
   def __eq__(self, rhs):
-    return (isinstance(rhs, Struct) and
-            (self.mojom_name, self.native_only, self.fields, self.constants,
-             self.attributes) == (rhs.mojom_name, rhs.native_only, rhs.fields,
-                                  rhs.constants, rhs.attributes))
+    return isinstance(rhs, Struct) and self._tuple() == rhs._tuple()
+
+  def __lt__(self, rhs):
+    if not isinstance(self, type(rhs)):
+      return str(type(self)) < str(type(rhs))
+
+    return self._tuple() < rhs._tuple()
 
   def __hash__(self):
     return id(self)
@@ -595,10 +775,11 @@
         which Java class name to use to represent it in the generated
         bindings.
   """
-  ReferenceKind.AddSharedProperty('mojom_name')
-  ReferenceKind.AddSharedProperty('name')
-  ReferenceKind.AddSharedProperty('fields')
-  ReferenceKind.AddSharedProperty('attributes')
+  Kind.AddSharedProperty('mojom_name')
+  Kind.AddSharedProperty('name')
+  Kind.AddSharedProperty('fields')
+  Kind.AddSharedProperty('attributes')
+  Kind.AddSharedProperty('default_field')
 
   def __init__(self, mojom_name=None, module=None, attributes=None):
     if mojom_name is not None:
@@ -610,14 +791,14 @@
     self.name = None
     self.fields = []
     self.attributes = attributes
+    self.default_field = None
 
   def Repr(self, as_ref=True):
     if as_ref:
       return '<%s spec=%r is_nullable=%r fields=%s>' % (
           self.__class__.__name__, self.spec, self.is_nullable, Repr(
               self.fields))
-    else:
-      return GenericRepr(self, {'fields': True, 'is_nullable': False})
+    return GenericRepr(self, {'fields': True, 'is_nullable': False})
 
   def AddField(self, mojom_name, kind, ordinal=None, attributes=None):
     field = UnionField(mojom_name, kind, ordinal, None, attributes)
@@ -629,13 +810,13 @@
     for field in self.fields:
       field.Stylize(stylizer)
 
-  def IsBackwardCompatible(self, older_union, checker):
-    """This union is backward-compatible with older_union if and only if all
-    of the following conditions hold:
+  def IsBackwardCompatible(self, rhs, checker):
+    """This union is backward-compatible with rhs (older_union) if and only if
+    all of the following conditions hold:
       - Any newly added field is tagged with a [MinVersion] attribute specifying
         a version number greater than all previously used [MinVersion]
         attributes within the union.
-      - All fields present in older_union remain present in the new union,
+      - All fields present in rhs remain present in the new union,
         with the same ordinal value, same optional or non-optional status,
         same (or backward-compatible) type, and where applicable, the same
         [MinVersion] attribute value.
@@ -651,7 +832,7 @@
       return fields_by_ordinal
 
     new_fields = buildOrdinalFieldMap(self)
-    old_fields = buildOrdinalFieldMap(older_union)
+    old_fields = buildOrdinalFieldMap(rhs)
     if len(new_fields) < len(old_fields):
       # At least one field was removed, which is not OK.
       return False
@@ -678,6 +859,11 @@
     return True
 
   @property
+  def extensible(self):
+    return self.attributes.get(ATTRIBUTE_EXTENSIBLE, False) \
+        if self.attributes else False
+
+  @property
   def stable(self):
     return self.attributes.get(ATTRIBUTE_STABLE, False) \
         if self.attributes else False
@@ -690,10 +876,17 @@
       prefix = self.module.GetNamespacePrefix()
     return '%s%s' % (prefix, self.mojom_name)
 
+  def _tuple(self):
+    return (self.mojom_name, self.fields, self.attributes)
+
   def __eq__(self, rhs):
-    return (isinstance(rhs, Union) and
-            (self.mojom_name, self.fields,
-             self.attributes) == (rhs.mojom_name, rhs.fields, rhs.attributes))
+    return isinstance(rhs, Union) and self._tuple() == rhs._tuple()
+
+  def __lt__(self, rhs):
+    if not isinstance(self, type(rhs)):
+      return str(type(self)) < str(type(rhs))
+
+    return self._tuple() < rhs._tuple()
 
   def __hash__(self):
     return id(self)
@@ -707,8 +900,8 @@
     length: The number of elements. None if unknown.
   """
 
-  ReferenceKind.AddSharedProperty('kind')
-  ReferenceKind.AddSharedProperty('length')
+  Kind.AddSharedProperty('kind')
+  Kind.AddSharedProperty('length')
 
   def __init__(self, kind=None, length=None):
     if kind is not None:
@@ -728,12 +921,11 @@
       return '<%s spec=%r is_nullable=%r kind=%s length=%r>' % (
           self.__class__.__name__, self.spec, self.is_nullable, Repr(
               self.kind), self.length)
-    else:
-      return GenericRepr(self, {
-          'kind': True,
-          'length': False,
-          'is_nullable': False
-      })
+    return GenericRepr(self, {
+        'kind': True,
+        'length': False,
+        'is_nullable': False
+    })
 
   def __eq__(self, rhs):
     return (isinstance(rhs, Array)
@@ -754,8 +946,8 @@
     key_kind: {Kind} The type of the keys. May be None.
     value_kind: {Kind} The type of the elements. May be None.
   """
-  ReferenceKind.AddSharedProperty('key_kind')
-  ReferenceKind.AddSharedProperty('value_kind')
+  Kind.AddSharedProperty('key_kind')
+  Kind.AddSharedProperty('value_kind')
 
   def __init__(self, key_kind=None, value_kind=None):
     if (key_kind is not None and value_kind is not None):
@@ -780,8 +972,7 @@
       return '<%s spec=%r is_nullable=%r key_kind=%s value_kind=%s>' % (
           self.__class__.__name__, self.spec, self.is_nullable,
           Repr(self.key_kind), Repr(self.value_kind))
-    else:
-      return GenericRepr(self, {'key_kind': True, 'value_kind': True})
+    return GenericRepr(self, {'key_kind': True, 'value_kind': True})
 
   def __eq__(self, rhs):
     return (isinstance(rhs, Map) and
@@ -797,7 +988,7 @@
 
 
 class PendingRemote(ReferenceKind):
-  ReferenceKind.AddSharedProperty('kind')
+  Kind.AddSharedProperty('kind')
 
   def __init__(self, kind=None):
     if kind is not None:
@@ -822,7 +1013,7 @@
 
 
 class PendingReceiver(ReferenceKind):
-  ReferenceKind.AddSharedProperty('kind')
+  Kind.AddSharedProperty('kind')
 
   def __init__(self, kind=None):
     if kind is not None:
@@ -847,7 +1038,7 @@
 
 
 class PendingAssociatedRemote(ReferenceKind):
-  ReferenceKind.AddSharedProperty('kind')
+  Kind.AddSharedProperty('kind')
 
   def __init__(self, kind=None):
     if kind is not None:
@@ -873,7 +1064,7 @@
 
 
 class PendingAssociatedReceiver(ReferenceKind):
-  ReferenceKind.AddSharedProperty('kind')
+  Kind.AddSharedProperty('kind')
 
   def __init__(self, kind=None):
     if kind is not None:
@@ -899,7 +1090,7 @@
 
 
 class InterfaceRequest(ReferenceKind):
-  ReferenceKind.AddSharedProperty('kind')
+  Kind.AddSharedProperty('kind')
 
   def __init__(self, kind=None):
     if kind is not None:
@@ -923,7 +1114,7 @@
 
 
 class AssociatedInterfaceRequest(ReferenceKind):
-  ReferenceKind.AddSharedProperty('kind')
+  Kind.AddSharedProperty('kind')
 
   def __init__(self, kind=None):
     if kind is not None:
@@ -949,7 +1140,7 @@
             self.kind, rhs.kind)
 
 
-class Parameter(object):
+class Parameter:
   def __init__(self,
                mojom_name=None,
                kind=None,
@@ -983,7 +1174,7 @@
                                       rhs.default, rhs.attributes))
 
 
-class Method(object):
+class Method:
   def __init__(self, interface, mojom_name, ordinal=None, attributes=None):
     self.interface = interface
     self.mojom_name = mojom_name
@@ -999,12 +1190,11 @@
   def Repr(self, as_ref=True):
     if as_ref:
       return '<%s mojom_name=%r>' % (self.__class__.__name__, self.mojom_name)
-    else:
-      return GenericRepr(self, {
-          'mojom_name': False,
-          'parameters': True,
-          'response_parameters': True
-      })
+    return GenericRepr(self, {
+        'mojom_name': False,
+        'parameters': True,
+        'response_parameters': True
+    })
 
   def AddParameter(self,
                    mojom_name,
@@ -1061,21 +1251,49 @@
     return self.attributes.get(ATTRIBUTE_UNLIMITED_SIZE) \
         if self.attributes else False
 
+  @property
+  def allowed_context(self):
+    return self.attributes.get(ATTRIBUTE_ALLOWED_CONTEXT) \
+        if self.attributes else None
+
+  @property
+  def supports_urgent(self):
+    return self.attributes.get(ATTRIBUTE_SUPPORTS_URGENT) \
+        if self.attributes else None
+
+  @property
+  def runtime_feature(self):
+    if not self.attributes:
+      return None
+    runtime_feature = self.attributes.get(ATTRIBUTE_RUNTIME_FEATURE, None)
+    if runtime_feature is None:
+      return None
+    if not isinstance(runtime_feature, Feature):
+      raise Exception("RuntimeFeature attribute on %s must be a feature." %
+                      self.name)
+    return runtime_feature
+
+  def _tuple(self):
+    return (self.mojom_name, self.ordinal, self.parameters,
+            self.response_parameters, self.attributes)
+
   def __eq__(self, rhs):
-    return (isinstance(rhs, Method) and
-            (self.mojom_name, self.ordinal, self.parameters,
-             self.response_parameters,
-             self.attributes) == (rhs.mojom_name, rhs.ordinal, rhs.parameters,
-                                  rhs.response_parameters, rhs.attributes))
+    return isinstance(rhs, Method) and self._tuple() == rhs._tuple()
+
+  def __lt__(self, rhs):
+    if not isinstance(self, type(rhs)):
+      return str(type(self)) < str(type(rhs))
+
+    return self._tuple() < rhs._tuple()
 
 
 class Interface(ReferenceKind):
-  ReferenceKind.AddSharedProperty('mojom_name')
-  ReferenceKind.AddSharedProperty('name')
-  ReferenceKind.AddSharedProperty('methods')
-  ReferenceKind.AddSharedProperty('enums')
-  ReferenceKind.AddSharedProperty('constants')
-  ReferenceKind.AddSharedProperty('attributes')
+  Kind.AddSharedProperty('mojom_name')
+  Kind.AddSharedProperty('name')
+  Kind.AddSharedProperty('methods')
+  Kind.AddSharedProperty('enums')
+  Kind.AddSharedProperty('constants')
+  Kind.AddSharedProperty('attributes')
 
   def __init__(self, mojom_name=None, module=None, attributes=None):
     if mojom_name is not None:
@@ -1093,12 +1311,11 @@
   def Repr(self, as_ref=True):
     if as_ref:
       return '<%s mojom_name=%r>' % (self.__class__.__name__, self.mojom_name)
-    else:
-      return GenericRepr(self, {
-          'mojom_name': False,
-          'attributes': False,
-          'methods': False
-      })
+    return GenericRepr(self, {
+        'mojom_name': False,
+        'attributes': False,
+        'methods': False
+    })
 
   def AddMethod(self, mojom_name, ordinal=None, attributes=None):
     method = Method(self, mojom_name, ordinal, attributes)
@@ -1114,10 +1331,10 @@
     for constant in self.constants:
       constant.Stylize(stylizer)
 
-  def IsBackwardCompatible(self, older_interface, checker):
-    """This interface is backward-compatible with older_interface if and only
-    if all of the following conditions hold:
-      - All defined methods in older_interface (when identified by ordinal) have
+  def IsBackwardCompatible(self, rhs, checker):
+    """This interface is backward-compatible with rhs (older_interface) if and
+    only if all of the following conditions hold:
+      - All defined methods in rhs (when identified by ordinal) have
         backward-compatible definitions in this interface. For each method this
         means:
           - The parameter list is backward-compatible, according to backward-
@@ -1131,7 +1348,7 @@
             rules for structs.
       - All newly introduced methods in this interface have a [MinVersion]
         attribute specifying a version greater than any method in
-        older_interface.
+        rhs.
     """
 
     def buildOrdinalMethodMap(interface):
@@ -1144,7 +1361,7 @@
       return methods_by_ordinal
 
     new_methods = buildOrdinalMethodMap(self)
-    old_methods = buildOrdinalMethodMap(older_interface)
+    old_methods = buildOrdinalMethodMap(rhs)
     max_old_min_version = 0
     for ordinal, old_method in old_methods.items():
       new_method = new_methods.get(ordinal)
@@ -1187,6 +1404,39 @@
     return True
 
   @property
+  def service_sandbox(self):
+    if not self.attributes:
+      return None
+    service_sandbox = self.attributes.get(ATTRIBUTE_SERVICE_SANDBOX, None)
+    if service_sandbox is None:
+      return None
+    # Constants are only allowed to refer to an enum here, so replace.
+    if isinstance(service_sandbox, Constant):
+      service_sandbox = service_sandbox.value
+    if not isinstance(service_sandbox, EnumValue):
+      raise Exception("ServiceSandbox attribute on %s must be an enum value." %
+                      self.module.name)
+    return service_sandbox
+
+  @property
+  def runtime_feature(self):
+    if not self.attributes:
+      return None
+    runtime_feature = self.attributes.get(ATTRIBUTE_RUNTIME_FEATURE, None)
+    if runtime_feature is None:
+      return None
+    if not isinstance(runtime_feature, Feature):
+      raise Exception("RuntimeFeature attribute on %s must be a feature." %
+                      self.name)
+    return runtime_feature
+
+  @property
+  def require_context(self):
+    if not self.attributes:
+      return None
+    return self.attributes.get(ATTRIBUTE_REQUIRE_CONTEXT, None)
+
+  @property
   def stable(self):
     return self.attributes.get(ATTRIBUTE_STABLE, False) \
         if self.attributes else False
@@ -1199,11 +1449,18 @@
       prefix = self.module.GetNamespacePrefix()
     return '%s%s' % (prefix, self.mojom_name)
 
+  def _tuple(self):
+    return (self.mojom_name, self.methods, self.enums, self.constants,
+            self.attributes)
+
   def __eq__(self, rhs):
-    return (isinstance(rhs, Interface)
-            and (self.mojom_name, self.methods, self.enums, self.constants,
-                 self.attributes) == (rhs.mojom_name, rhs.methods, rhs.enums,
-                                      rhs.constants, rhs.attributes))
+    return isinstance(rhs, Interface) and self._tuple() == rhs._tuple()
+
+  def __lt__(self, rhs):
+    if not isinstance(self, type(rhs)):
+      return str(type(self)) < str(type(rhs))
+
+    return self._tuple() < rhs._tuple()
 
   @property
   def uuid(self):
@@ -1224,7 +1481,7 @@
 
 
 class AssociatedInterface(ReferenceKind):
-  ReferenceKind.AddSharedProperty('kind')
+  Kind.AddSharedProperty('kind')
 
   def __init__(self, kind=None):
     if kind is not None:
@@ -1249,7 +1506,7 @@
                           self.kind, rhs.kind)
 
 
-class EnumField(object):
+class EnumField:
   def __init__(self,
                mojom_name=None,
                value=None,
@@ -1281,16 +1538,25 @@
                                          rhs.attributes, rhs.numeric_value))
 
 
-class Enum(Kind):
+class Enum(ValueKind):
+  Kind.AddSharedProperty('mojom_name')
+  Kind.AddSharedProperty('name')
+  Kind.AddSharedProperty('native_only')
+  Kind.AddSharedProperty('fields')
+  Kind.AddSharedProperty('attributes')
+  Kind.AddSharedProperty('min_value')
+  Kind.AddSharedProperty('max_value')
+  Kind.AddSharedProperty('default_field')
+
   def __init__(self, mojom_name=None, module=None, attributes=None):
-    self.mojom_name = mojom_name
-    self.name = None
-    self.native_only = False
     if mojom_name is not None:
       spec = 'x:' + mojom_name
     else:
       spec = None
-    Kind.__init__(self, spec, module)
+    ValueKind.__init__(self, spec, False, module)
+    self.mojom_name = mojom_name
+    self.name = None
+    self.native_only = False
     self.fields = []
     self.attributes = attributes
     self.min_value = None
@@ -1300,8 +1566,7 @@
   def Repr(self, as_ref=True):
     if as_ref:
       return '<%s mojom_name=%r>' % (self.__class__.__name__, self.mojom_name)
-    else:
-      return GenericRepr(self, {'mojom_name': False, 'fields': False})
+    return GenericRepr(self, {'mojom_name': False, 'fields': False})
 
   def Stylize(self, stylizer):
     self.name = stylizer.StylizeEnum(self.mojom_name)
@@ -1327,14 +1592,14 @@
     return '%s%s' % (prefix, self.mojom_name)
 
   # pylint: disable=unused-argument
-  def IsBackwardCompatible(self, older_enum, checker):
-    """This enum is backward-compatible with older_enum if and only if one of
-    the following conditions holds:
+  def IsBackwardCompatible(self, rhs, checker):
+    """This enum is backward-compatible with rhs (older_enum) if and only if one
+    of the following conditions holds:
         - Neither enum is [Extensible] and both have the exact same set of valid
           numeric values. Field names and aliases for the same numeric value do
           not affect compatibility.
-        - older_enum is [Extensible], and for every version defined by
-          older_enum, this enum has the exact same set of valid numeric values.
+        - rhs is [Extensible], and for every version defined by
+          rhs, this enum has the exact same set of valid numeric values.
     """
 
     def buildVersionFieldMap(enum):
@@ -1345,32 +1610,49 @@
         fields_by_min_version[field.min_version].add(field.numeric_value)
       return fields_by_min_version
 
-    old_fields = buildVersionFieldMap(older_enum)
+    old_fields = buildVersionFieldMap(rhs)
     new_fields = buildVersionFieldMap(self)
 
-    if new_fields.keys() != old_fields.keys() and not older_enum.extensible:
-      return False
+    if new_fields.keys() != old_fields.keys() and not rhs.extensible:
+      raise Exception("Non-extensible enum cannot be modified")
 
     for min_version, valid_values in old_fields.items():
-      if (min_version not in new_fields
-          or new_fields[min_version] != valid_values):
-        return False
+      if min_version not in new_fields:
+        raise Exception('New values added to an extensible enum '
+                        'do not specify MinVersion: %s' % new_fields)
 
+      if (new_fields[min_version] != valid_values):
+        if (len(new_fields[min_version]) < len(valid_values)):
+          raise Exception('Removing values for an existing MinVersion %s '
+                          'is not allowed' % min_version)
+
+        raise Exception(
+            'New values don\'t match old values'
+            'for an existing MinVersion %s,'
+            ' please specify MinVersion equal to "Next version" '
+            'in the enum description'
+            ' for the following values:\n%s' %
+            (min_version, new_fields[min_version].difference(valid_values)))
     return True
 
+  def _tuple(self):
+    return (self.mojom_name, self.native_only, self.fields, self.attributes,
+            self.min_value, self.max_value, self.default_field)
+
   def __eq__(self, rhs):
-    return (isinstance(rhs, Enum) and
-            (self.mojom_name, self.native_only, self.fields, self.attributes,
-             self.min_value, self.max_value,
-             self.default_field) == (rhs.mojom_name, rhs.native_only,
-                                     rhs.fields, rhs.attributes, rhs.min_value,
-                                     rhs.max_value, rhs.default_field))
+    return isinstance(rhs, Enum) and self._tuple() == rhs._tuple()
+
+  def __lt__(self, rhs):
+    if not isinstance(self, type(rhs)):
+      return str(type(self)) < str(type(rhs))
+
+    return self._tuple() < rhs._tuple()
 
   def __hash__(self):
     return id(self)
 
 
-class Module(object):
+class Module:
   def __init__(self, path=None, mojom_namespace=None, attributes=None):
     self.path = path
     self.mojom_namespace = mojom_namespace
@@ -1379,24 +1661,26 @@
     self.unions = []
     self.interfaces = []
     self.enums = []
+    self.features = []
     self.constants = []
-    self.kinds = {}
+    self.kinds = OrderedDict()
     self.attributes = attributes
     self.imports = []
-    self.imported_kinds = {}
-    self.metadata = {}
+    self.imported_kinds = OrderedDict()
+    self.metadata = OrderedDict()
 
   def __repr__(self):
     # Gives us a decent __repr__ for modules.
     return self.Repr()
 
   def __eq__(self, rhs):
-    return (isinstance(rhs, Module) and
-            (self.path, self.attributes, self.mojom_namespace, self.imports,
-             self.constants, self.enums, self.structs, self.unions,
-             self.interfaces) == (rhs.path, rhs.attributes, rhs.mojom_namespace,
-                                  rhs.imports, rhs.constants, rhs.enums,
-                                  rhs.structs, rhs.unions, rhs.interfaces))
+    return (isinstance(rhs, Module)
+            and (self.path, self.attributes, self.mojom_namespace, self.imports,
+                 self.constants, self.enums, self.structs, self.unions,
+                 self.interfaces, self.features)
+            == (rhs.path, rhs.attributes, rhs.mojom_namespace, rhs.imports,
+                rhs.constants, rhs.enums, rhs.structs, rhs.unions,
+                rhs.interfaces, rhs.features))
 
   def __hash__(self):
     return id(self)
@@ -1405,16 +1689,16 @@
     if as_ref:
       return '<%s path=%r mojom_namespace=%r>' % (
           self.__class__.__name__, self.path, self.mojom_namespace)
-    else:
-      return GenericRepr(
-          self, {
-              'path': False,
-              'mojom_namespace': False,
-              'attributes': False,
-              'structs': False,
-              'interfaces': False,
-              'unions': False
-          })
+    return GenericRepr(
+        self, {
+            'path': False,
+            'mojom_namespace': False,
+            'attributes': False,
+            'structs': False,
+            'interfaces': False,
+            'unions': False,
+            'features': False,
+        })
 
   def GetNamespacePrefix(self):
     return '%s.' % self.mojom_namespace if self.mojom_namespace else ''
@@ -1434,6 +1718,11 @@
     self.unions.append(union)
     return union
 
+  def AddFeature(self, mojom_name, attributes=None):
+    feature = Feature(mojom_name, self, attributes)
+    self.features.append(feature)
+    return feature
+
   def Stylize(self, stylizer):
     self.namespace = stylizer.StylizeModule(self.mojom_namespace)
     for struct in self.structs:
@@ -1446,12 +1735,14 @@
       enum.Stylize(stylizer)
     for constant in self.constants:
       constant.Stylize(stylizer)
+    for feature in self.features:
+      feature.Stylize(stylizer)
 
     for imported_module in self.imports:
       imported_module.Stylize(stylizer)
 
   def Dump(self, f):
-    pickle.dump(self, f, 2)
+    pickle.dump(self, f)
 
   @classmethod
   def Load(cls, f):
@@ -1461,15 +1752,15 @@
 
 
 def IsBoolKind(kind):
-  return kind.spec == BOOL.spec
+  return kind.spec == BOOL.spec or kind.spec == NULLABLE_BOOL.spec
 
 
 def IsFloatKind(kind):
-  return kind.spec == FLOAT.spec
+  return kind.spec == FLOAT.spec or kind.spec == NULLABLE_FLOAT.spec
 
 
 def IsDoubleKind(kind):
-  return kind.spec == DOUBLE.spec
+  return kind.spec == DOUBLE.spec or kind.spec == NULLABLE_DOUBLE.spec
 
 
 def IsIntegralKind(kind):
@@ -1477,7 +1768,14 @@
           or kind.spec == INT16.spec or kind.spec == INT32.spec
           or kind.spec == INT64.spec or kind.spec == UINT8.spec
           or kind.spec == UINT16.spec or kind.spec == UINT32.spec
-          or kind.spec == UINT64.spec)
+          or kind.spec == UINT64.spec or kind.spec == NULLABLE_BOOL.spec
+          or kind.spec == NULLABLE_INT8.spec or kind.spec == NULLABLE_INT16.spec
+          or kind.spec == NULLABLE_INT32.spec
+          or kind.spec == NULLABLE_INT64.spec
+          or kind.spec == NULLABLE_UINT8.spec
+          or kind.spec == NULLABLE_UINT16.spec
+          or kind.spec == NULLABLE_UINT32.spec
+          or kind.spec == NULLABLE_UINT64.spec)
 
 
 def IsStringKind(kind):
@@ -1522,6 +1820,10 @@
   return isinstance(kind, Array)
 
 
+def IsFeatureKind(kind):
+  return isinstance(kind, Feature)
+
+
 def IsInterfaceKind(kind):
   return isinstance(kind, Interface)
 
@@ -1558,12 +1860,16 @@
   return isinstance(kind, Enum)
 
 
+def IsValueKind(kind):
+  return isinstance(kind, ValueKind)
+
+
 def IsReferenceKind(kind):
   return isinstance(kind, ReferenceKind)
 
 
 def IsNullableKind(kind):
-  return IsReferenceKind(kind) and kind.is_nullable
+  return kind.is_nullable
 
 
 def IsMapKind(kind):
@@ -1664,11 +1970,8 @@
   return _AnyMethodParameterRecursive(method, IsInterfaceKind)
 
 
-def HasSyncMethods(interface):
-  for method in interface.methods:
-    if method.sync:
-      return True
-  return False
+def GetSyncMethodOrdinals(interface):
+  return [method.ordinal for method in interface.methods if method.sync]
 
 
 def HasUninterruptableMethods(interface):
@@ -1700,18 +2003,17 @@
     checked.add(kind.spec)
     if IsStructKind(kind):
       return any(Check(field.kind) for field in kind.fields)
-    elif IsUnionKind(kind):
+    if IsUnionKind(kind):
       return any(Check(field.kind) for field in kind.fields)
-    elif IsAnyHandleKind(kind):
+    if IsAnyHandleKind(kind):
       return True
-    elif IsAnyInterfaceKind(kind):
+    if IsAnyInterfaceKind(kind):
       return True
-    elif IsArrayKind(kind):
+    if IsArrayKind(kind):
       return Check(kind.kind)
-    elif IsMapKind(kind):
+    if IsMapKind(kind):
       return Check(kind.key_kind) or Check(kind.value_kind)
-    else:
-      return False
+    return False
 
   return Check(kind)
 
@@ -1738,21 +2040,20 @@
     checked.add(kind.spec)
     if IsEnumKind(kind):
       return kind.native_only
-    elif IsStructKind(kind):
+    if IsStructKind(kind):
       if kind.native_only:
         return True
       if any(enum.native_only for enum in kind.enums):
         return True
       return any(Check(field.kind) for field in kind.fields)
-    elif IsUnionKind(kind):
+    if IsUnionKind(kind):
       return any(Check(field.kind) for field in kind.fields)
-    elif IsInterfaceKind(kind):
+    if IsInterfaceKind(kind):
       return any(enum.native_only for enum in kind.enums)
-    elif IsArrayKind(kind):
+    if IsArrayKind(kind):
       return Check(kind.kind)
-    elif IsMapKind(kind):
+    if IsMapKind(kind):
       return Check(kind.key_kind) or Check(kind.value_kind)
-    else:
-      return False
+    return False
 
   return Check(kind)
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/module_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/module_unittest.py
index e8fd493..2a4e852 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/module_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/module_unittest.py
@@ -1,4 +1,4 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/pack.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/pack.py
index 88b77c9..6124042 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/pack.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/pack.py
@@ -1,7 +1,8 @@
-# Copyright 2013 The Chromium Authors. All rights reserved.
+# Copyright 2013 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import copy
 from mojom.generate import module as mojom
 
 # This module provides a mechanism for determining the packed order and offsets
@@ -15,7 +16,7 @@
 HEADER_SIZE = 8
 
 
-class PackedField(object):
+class PackedField:
   kind_to_size = {
       mojom.BOOL: 1,
       mojom.INT8: 1,
@@ -75,18 +76,55 @@
       return 8
     return cls.GetSizeForKind(kind)
 
-  def __init__(self, field, index, ordinal):
+  def __init__(self,
+               field,
+               index,
+               ordinal,
+               original_field=None,
+               sub_ordinal=None,
+               linked_value_packed_field=None):
     """
     Args:
       field: the original field.
       index: the position of the original field in the struct.
       ordinal: the ordinal of the field for serialization.
+      original_field: See below.
+      sub_ordinal: See below.
+      linked_value_packed_field: See below.
+
+    original_field, sub_ordinal, and linked_value_packed_field are used to
+    support nullable ValueKind fields. For legacy reasons, nullable ValueKind
+    fields actually generate two PackedFields. This allows:
+
+    - backwards compatibility prior to Mojo support for nullable ValueKinds.
+    - correct packing of fields for the aforementioned backwards compatibility.
+
+    When translating Fields to PackedFields, the original field is turned into
+    two PackedFields: the first PackedField always has type mojom.BOOL, while
+    the second PackedField has the non-nullable version of the field's kind.
+
+    When constructing these PackedFields, original_field references the field
+    as defined in the mojom; the name as defined in the mojom will be used for
+    all layers above the wire/data layer.
+
+    sub_ordinal is used to sort the two PackedFields correctly with respect to
+    each other: the first mojom.BOOL field always has sub_ordinal 0, while the
+    second field always has sub_ordinal 1.
+
+    Finally, linked_value_packed_field is used by the serialization and
+    deserialization helpers, which generally just iterate over a PackedStruct's
+    PackedField's in ordinal order. This allows the helpers to easily reference
+    any related PackedFields rather than having to lookup related PackedFields
+    by index while iterating.
     """
     self.field = field
     self.index = index
     self.ordinal = ordinal
-    self.size = self.GetSizeForKind(field.kind)
-    self.alignment = self.GetAlignmentForKind(field.kind)
+    self.original_field = original_field
+    self.sub_ordinal = sub_ordinal
+    self.linked_value_packed_field = linked_value_packed_field
+    self.size = self.GetSizeForKind(self.field.kind)
+    self.alignment = self.GetAlignmentForKind(self.field.kind)
     self.offset = None
     self.bit = None
     self.min_version = None
@@ -120,7 +158,33 @@
   return offset + pad
 
 
-class PackedStruct(object):
+def IsNullableValueKindPackedField(field):
+  """Returns true if `field` is derived from a nullable ValueKind field.
+
+  Nullable ValueKind fields often require special handling in the bindings due
+  to the way the implementation is constrained for wire compatibility.
+  """
+  assert isinstance(field, PackedField)
+  return field.sub_ordinal is not None
+
+
+def IsPrimaryNullableValueKindPackedField(field):
+  """Returns true if `field` is derived from a nullable ValueKind mojom field
+  and is the "primary" field.
+
+  The primary field is a bool PackedField that controls if the field should be
+  considered as present or not; it will have a reference to the PackedField that
+  holds the actual value representation if considered present.
+
+  Bindings code that translates between the wire protocol and the higher layers
+  can use this to simplify mapping multiple PackedFields to the single field
+  that is logically exposed to bindings consumers.
+  """
+  assert isinstance(field, PackedField)
+  return field.linked_value_packed_field is not None
+
+
+class PackedStruct:
   def __init__(self, struct):
     self.struct = struct
     # |packed_fields| contains all the fields, in increasing offset order.
@@ -139,9 +203,41 @@
     for index, field in enumerate(struct.fields):
       if field.ordinal is not None:
         ordinal = field.ordinal
-      src_fields.append(PackedField(field, index, ordinal))
+      # Nullable value types are a bit weird: they generate two PackedFields
+      # despite being a single ValueKind. This is for wire compatibility to
+      # ease the transition from legacy mojom syntax where nullable value types
+      # were not supported.
+      if isinstance(field.kind, mojom.ValueKind) and field.kind.is_nullable:
+        # The suffixes intentionally use Unicode codepoints which are considered
+        # valid C++/Java/JavaScript identifiers, yet are unlikely to be used in
+        # actual user code.
+        has_value_field = copy.copy(field)
+        has_value_field.name = f'{field.mojom_name}_$flag'
+        has_value_field.kind = mojom.BOOL
+
+        value_field = copy.copy(field)
+        value_field.name = f'{field.mojom_name}_$value'
+        value_field.kind = field.kind.MakeUnnullableKind()
+
+        value_packed_field = PackedField(value_field,
+                                         index,
+                                         ordinal,
+                                         original_field=field,
+                                         sub_ordinal=1,
+                                         linked_value_packed_field=None)
+        has_value_packed_field = PackedField(
+            has_value_field,
+            index,
+            ordinal,
+            original_field=field,
+            sub_ordinal=0,
+            linked_value_packed_field=value_packed_field)
+        src_fields.append(has_value_packed_field)
+        src_fields.append(value_packed_field)
+      else:
+        src_fields.append(PackedField(field, index, ordinal))
       ordinal += 1
-    src_fields.sort(key=lambda field: field.ordinal)
+    src_fields.sort(key=lambda field: (field.ordinal, field.sub_ordinal))
 
     # Set |min_version| for each field.
     next_min_version = 0
@@ -156,10 +252,11 @@
       if (packed_field.min_version != 0
           and mojom.IsReferenceKind(packed_field.field.kind)
           and not packed_field.field.kind.is_nullable):
-        raise Exception("Non-nullable fields are only allowed in version 0 of "
-                        "a struct. %s.%s is defined with [MinVersion=%d]." %
-                        (self.struct.name, packed_field.field.name,
-                         packed_field.min_version))
+        raise Exception(
+            "Non-nullable reference fields are only allowed in version 0 of a "
+            "struct. %s.%s is defined with [MinVersion=%d]." %
+            (self.struct.name, packed_field.field.name,
+             packed_field.min_version))
 
     src_field = src_fields[0]
     src_field.offset = 0
@@ -186,7 +283,7 @@
         dst_fields.append(src_field)
 
 
-class ByteInfo(object):
+class ByteInfo:
   def __init__(self):
     self.is_padding = False
     self.packed_fields = []
@@ -214,10 +311,11 @@
   return byte_info
 
 
-class VersionInfo(object):
-  def __init__(self, version, num_fields, num_bytes):
+class VersionInfo:
+  def __init__(self, version, num_fields, num_packed_fields, num_bytes):
     self.version = version
     self.num_fields = num_fields
+    self.num_packed_fields = num_packed_fields
     self.num_bytes = num_bytes
 
 
@@ -235,24 +333,35 @@
   versions = []
   last_version = 0
   last_num_fields = 0
+  last_num_packed_fields = 0
   last_payload_size = 0
 
   for packed_field in packed_struct.packed_fields_in_ordinal_order:
     if packed_field.min_version != last_version:
       versions.append(
-          VersionInfo(last_version, last_num_fields,
+          VersionInfo(last_version, last_num_fields, last_num_packed_fields,
                       last_payload_size + HEADER_SIZE))
       last_version = packed_field.min_version
 
-    last_num_fields += 1
+    # Nullable numeric fields (e.g. `int32?`) expand to two packed fields, so to
+    # avoid double-counting, only increment if the field is:
+    # - not used for representing a nullable value kind field, or
+    # - the primary field representing the nullable value kind field.
+    last_num_fields += 1 if (
+        not IsNullableValueKindPackedField(packed_field)
+        or IsPrimaryNullableValueKindPackedField(packed_field)) else 0
+
+    last_num_packed_fields += 1
+
     # The fields are iterated in ordinal order here. However, the size of a
     # version is determined by the last field of that version in pack order,
     # instead of ordinal order. Therefore, we need to calculate the max value.
-    last_payload_size = max(
-        GetPayloadSizeUpToField(packed_field), last_payload_size)
+    last_payload_size = max(GetPayloadSizeUpToField(packed_field),
+                            last_payload_size)
 
-  assert len(versions) == 0 or last_num_fields != versions[-1].num_fields
+  assert len(
+      versions) == 0 or last_num_packed_fields != versions[-1].num_packed_fields
   versions.append(
-      VersionInfo(last_version, last_num_fields,
+      VersionInfo(last_version, last_num_fields, last_num_packed_fields,
                   last_payload_size + HEADER_SIZE))
   return versions
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/pack_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/pack_unittest.py
index 98c705a..7d8e4e0 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/pack_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/pack_unittest.py
@@ -1,4 +1,4 @@
-# Copyright 2013 The Chromium Authors. All rights reserved.
+# Copyright 2013 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -205,6 +205,34 @@
     self.assertEqual(4, versions[2].num_fields)
     self.assertEqual(32, versions[2].num_bytes)
 
+  def testGetVersionInfoPackedStruct(self):
+    """Tests that pack.GetVersionInfo() correctly sets version, num_fields,
+    and num_packed_fields for a packed struct.
+    """
+    struct = mojom.Struct('test')
+    struct.AddField('field_0', mojom.BOOL, ordinal=0)
+    struct.AddField('field_1',
+                    mojom.NULLABLE_BOOL,
+                    ordinal=1,
+                    attributes={'MinVersion': 1})
+    struct.AddField('field_2',
+                    mojom.NULLABLE_BOOL,
+                    ordinal=2,
+                    attributes={'MinVersion': 2})
+    ps = pack.PackedStruct(struct)
+    versions = pack.GetVersionInfo(ps)
+
+    self.assertEqual(3, len(versions))
+    self.assertEqual(0, versions[0].version)
+    self.assertEqual(1, versions[1].version)
+    self.assertEqual(2, versions[2].version)
+    self.assertEqual(1, versions[0].num_fields)
+    self.assertEqual(2, versions[1].num_fields)
+    self.assertEqual(3, versions[2].num_fields)
+    self.assertEqual(1, versions[0].num_packed_fields)
+    self.assertEqual(3, versions[1].num_packed_fields)
+    self.assertEqual(5, versions[2].num_packed_fields)
+
   def testInterfaceAlignment(self):
     """Tests that interfaces are aligned on 4-byte boundaries, although the size
     of an interface is 8 bytes.
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/template_expander.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/template_expander.py
index 0da9005..807e2a4 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/template_expander.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/template_expander.py
@@ -1,4 +1,4 @@
-# Copyright 2013 The Chromium Authors. All rights reserved.
+# Copyright 2013 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/translate.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/translate.py
index 7580b78..83bb297 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/translate.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/translate.py
@@ -1,4 +1,4 @@
-# Copyright 2013 The Chromium Authors. All rights reserved.
+# Copyright 2013 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """Convert parse tree to AST.
@@ -12,17 +12,294 @@
 import itertools
 import os
 import re
-import sys
 
+from collections import OrderedDict
 from mojom.generate import generator
 from mojom.generate import module as mojom
 from mojom.parse import ast
 
 
-def _IsStrOrUnicode(x):
-  if sys.version_info[0] < 3:
-    return isinstance(x, (unicode, str))
-  return isinstance(x, str)
+is_running_backwards_compatibility_check_hack = False
+
+### DO NOT ADD ENTRIES TO THIS LIST. ###
+_EXTENSIBLE_ENUMS_MISSING_DEFAULT = (
+    'x:arc.keymaster.mojom.Algorithm',
+    'x:arc.keymaster.mojom.Digest',
+    'x:arc.keymaster.mojom.SignatureResult',
+    'x:arc.mojom.AccessibilityActionType',
+    'x:arc.mojom.AccessibilityBooleanProperty',
+    'x:arc.mojom.AccessibilityEventIntListProperty',
+    'x:arc.mojom.AccessibilityEventIntProperty',
+    'x:arc.mojom.AccessibilityEventStringProperty',
+    'x:arc.mojom.AccessibilityEventType',
+    'x:arc.mojom.AccessibilityFilterType',
+    'x:arc.mojom.AccessibilityIntListProperty',
+    'x:arc.mojom.AccessibilityIntProperty',
+    'x:arc.mojom.AccessibilityLiveRegionType',
+    'x:arc.mojom.AccessibilityNotificationStateType',
+    'x:arc.mojom.AccessibilityRangeType',
+    'x:arc.mojom.AccessibilitySelectionMode',
+    'x:arc.mojom.AccessibilityStringListProperty',
+    'x:arc.mojom.AccessibilityStringProperty',
+    'x:arc.mojom.AccessibilityWindowBooleanProperty',
+    'x:arc.mojom.AccessibilityWindowIntListProperty',
+    'x:arc.mojom.AccessibilityWindowIntProperty',
+    'x:arc.mojom.AccessibilityWindowStringProperty',
+    'x:arc.mojom.AccessibilityWindowType',
+    'x:arc.mojom.AccountCheckStatus',
+    'x:arc.mojom.AccountUpdateType',
+    'x:arc.mojom.ActionType',
+    'x:arc.mojom.Algorithm',
+    'x:arc.mojom.AndroidIdSource',
+    'x:arc.mojom.AnrSource',
+    'x:arc.mojom.AnrType',
+    'x:arc.mojom.AppDiscoveryRequestState',
+    'x:arc.mojom.AppKillType',
+    'x:arc.mojom.AppPermission',
+    'x:arc.mojom.AppPermissionGroup',
+    'x:arc.mojom.AppReinstallState',
+    'x:arc.mojom.AppShortcutItemType',
+    'x:arc.mojom.ArcAuthCodeStatus',
+    'x:arc.mojom.ArcClipboardDragDropEvent',
+    'x:arc.mojom.ArcCorePriAbiMigEvent',
+    'x:arc.mojom.ArcDnsQuery',
+    'x:arc.mojom.ArcImageCopyPasteCompatAction',
+    'x:arc.mojom.ArcNetworkError',
+    'x:arc.mojom.ArcNetworkEvent',
+    'x:arc.mojom.ArcNotificationEvent',
+    'x:arc.mojom.ArcNotificationExpandState',
+    'x:arc.mojom.ArcNotificationPriority',
+    'x:arc.mojom.ArcNotificationRemoteInputState',
+    'x:arc.mojom.ArcNotificationShownContents',
+    'x:arc.mojom.ArcNotificationStyle',
+    'x:arc.mojom.ArcNotificationType',
+    'x:arc.mojom.ArcPipEvent',
+    'x:arc.mojom.ArcResizeLockState',
+    'x:arc.mojom.ArcSignInSuccess',
+    'x:arc.mojom.ArcTimerResult',
+    'x:arc.mojom.AudioSwitch',
+    'x:arc.mojom.BluetoothAclState',
+    'x:arc.mojom.BluetoothAdapterState',
+    'x:arc.mojom.BluetoothAdvertisingDataType',
+    'x:arc.mojom.BluetoothBondState',
+    'x:arc.mojom.BluetoothDeviceType',
+    'x:arc.mojom.BluetoothDiscoveryState',
+    'x:arc.mojom.BluetoothGattDBAttributeType',
+    'x:arc.mojom.BluetoothGattStatus',
+    'x:arc.mojom.BluetoothPropertyType',
+    'x:arc.mojom.BluetoothScanMode',
+    'x:arc.mojom.BluetoothSdpAttributeType',
+    'x:arc.mojom.BluetoothSocketType',
+    'x:arc.mojom.BluetoothStatus',
+    'x:arc.mojom.BootType',
+    'x:arc.mojom.CaptionTextShadowType',
+    'x:arc.mojom.ChangeType',
+    'x:arc.mojom.ChromeAccountType',
+    'x:arc.mojom.ChromeApp',
+    'x:arc.mojom.ChromePage',
+    'x:arc.mojom.ClockId',
+    'x:arc.mojom.CloudProvisionFlowError',
+    'x:arc.mojom.CommandResultType',
+    'x:arc.mojom.CompanionLibApiId',
+    'x:arc.mojom.ConnectionStateType',
+    'x:arc.mojom.ContentChangeType',
+    'x:arc.mojom.CpuRestrictionState',
+    'x:arc.mojom.CursorCoordinateSpace',
+    'x:arc.mojom.DataRestoreStatus',
+    'x:arc.mojom.DecoderStatus',
+    'x:arc.mojom.DeviceType',
+    'x:arc.mojom.Digest',
+    'x:arc.mojom.DisplayWakeLockType',
+    'x:arc.mojom.EapMethod',
+    'x:arc.mojom.EapPhase2Method',
+    'x:arc.mojom.FileSelectorEventType',
+    'x:arc.mojom.GMSCheckInError',
+    'x:arc.mojom.GMSSignInError',
+    'x:arc.mojom.GeneralSignInError',
+    'x:arc.mojom.GetNetworksRequestType',
+    'x:arc.mojom.HalPixelFormat',
+    'x:arc.mojom.IPAddressType',
+    'x:arc.mojom.InstallErrorReason',
+    'x:arc.mojom.KeyFormat',
+    'x:arc.mojom.KeyManagement',
+    'x:arc.mojom.KeyPurpose',
+    'x:arc.mojom.KeymasterError',
+    'x:arc.mojom.MainAccountHashMigrationStatus',
+    'x:arc.mojom.MainAccountResolutionStatus',
+    'x:arc.mojom.ManagementChangeStatus',
+    'x:arc.mojom.ManagementState',
+    'x:arc.mojom.MessageCenterVisibility',
+    'x:arc.mojom.MetricsType',
+    'x:arc.mojom.MountEvent',
+    'x:arc.mojom.NativeBridgeType',
+    'x:arc.mojom.NetworkResult',
+    'x:arc.mojom.NetworkType',
+    'x:arc.mojom.OemCryptoAlgorithm',
+    'x:arc.mojom.OemCryptoCipherMode',
+    'x:arc.mojom.OemCryptoHdcpCapability',
+    'x:arc.mojom.OemCryptoLicenseType',
+    'x:arc.mojom.OemCryptoPrivateKey',
+    'x:arc.mojom.OemCryptoProvisioningMethod',
+    'x:arc.mojom.OemCryptoResult',
+    'x:arc.mojom.OemCryptoRsaPaddingScheme',
+    'x:arc.mojom.OemCryptoUsageEntryStatus',
+    'x:arc.mojom.Padding',
+    'x:arc.mojom.PaiFlowState',
+    'x:arc.mojom.PatternType',
+    'x:arc.mojom.PressureLevel',
+    'x:arc.mojom.PrintColorMode',
+    'x:arc.mojom.PrintContentType',
+    'x:arc.mojom.PrintDuplexMode',
+    'x:arc.mojom.PrinterStatus',
+    'x:arc.mojom.ProcessState',
+    'x:arc.mojom.PurchaseState',
+    'x:arc.mojom.ReauthReason',
+    'x:arc.mojom.ScaleFactor',
+    'x:arc.mojom.SecurityType',
+    'x:arc.mojom.SegmentStyle',
+    'x:arc.mojom.SelectFilesActionType',
+    'x:arc.mojom.SetNativeChromeVoxResponse',
+    'x:arc.mojom.ShowPackageInfoPage',
+    'x:arc.mojom.SpanType',
+    'x:arc.mojom.SupportedLinkChangeSource',
+    'x:arc.mojom.TetheringClientState',
+    'x:arc.mojom.TextInputType',
+    'x:arc.mojom.TtsEventType',
+    'x:arc.mojom.VideoCodecProfile',
+    'x:arc.mojom.VideoDecodeAccelerator.Result',
+    'x:arc.mojom.VideoEncodeAccelerator.Error',
+    'x:arc.mojom.VideoFrameStorageType',
+    'x:arc.mojom.VideoPixelFormat',
+    'x:arc.mojom.WakefulnessMode',
+    'x:arc.mojom.WebApkInstallResult',
+    'x:ash.ime.mojom.InputFieldType',
+    'x:ash.ime.mojom.PersonalizationMode',
+    'x:ash.language.mojom.FeatureId',
+    'x:blink.mojom.ScrollRestorationType',
+    'x:chromeos.cdm.mojom.CdmKeyStatus',
+    'x:chromeos.cdm.mojom.CdmMessageType',
+    'x:chromeos.cdm.mojom.CdmSessionType',
+    'x:chromeos.cdm.mojom.DecryptStatus',
+    'x:chromeos.cdm.mojom.EmeInitDataType',
+    'x:chromeos.cdm.mojom.EncryptionScheme',
+    'x:chromeos.cdm.mojom.HdcpVersion',
+    'x:chromeos.cdm.mojom.OutputProtection.LinkType',
+    'x:chromeos.cdm.mojom.OutputProtection.ProtectionType',
+    'x:chromeos.cdm.mojom.PromiseException',
+    'x:chromeos.cfm.mojom.EnqueuePriority',
+    'x:chromeos.cfm.mojom.LoggerErrorCode',
+    'x:chromeos.cfm.mojom.LoggerState',
+    'x:chromeos.cros_healthd.mojom.CryptoAlgorithm',
+    'x:chromeos.cros_healthd.mojom.EncryptionState',
+    'x:chromeos.machine_learning.mojom.AnnotationUsecase',
+    'x:chromeos.machine_learning.mojom.BuiltinModelId',
+    'x:chromeos.machine_learning.mojom.CreateGraphExecutorResult',
+    'x:chromeos.machine_learning.mojom.DocumentScannerResultStatus',
+    'x:chromeos.machine_learning.mojom.EndpointReason',
+    'x:chromeos.machine_learning.mojom.EndpointerType',
+    'x:chromeos.machine_learning.mojom.ExecuteResult',
+    'x:chromeos.machine_learning.mojom.GrammarCheckerResult.Status',
+    'x:chromeos.machine_learning.mojom.HandwritingRecognizerResult.Status',
+    'x:chromeos.machine_learning.mojom.LoadHandwritingModelResult',
+    'x:chromeos.machine_learning.mojom.LoadModelResult',
+    'x:chromeos.machine_learning.mojom.Rotation',
+    'x:chromeos.network_config.mojom.ConnectionStateType',
+    'x:chromeos.network_config.mojom.DeviceStateType',
+    'x:chromeos.network_config.mojom.IPConfigType',
+    'x:chromeos.network_config.mojom.NetworkType',
+    'x:chromeos.network_config.mojom.OncSource',
+    'x:chromeos.network_config.mojom.PolicySource',
+    'x:chromeos.network_config.mojom.PortalState',
+    'x:chromeos.wilco_dtc_supportd.mojom.WilcoDtcSupportdEvent',
+    'x:chromeos.wilco_dtc_supportd.mojom.WilcoDtcSupportdWebRequestHttpMethod',
+    'x:chromeos.wilco_dtc_supportd.mojom.WilcoDtcSupportdWebRequestStatus',
+    'x:cros.mojom.CameraClientType',
+    'x:cros.mojom.CameraMetadataSectionStart',
+    'x:cros.mojom.CameraMetadataTag',
+    'x:cros.mojom.HalPixelFormat',
+    'x:crosapi.mojom.AllowedPaths',
+    'x:crosapi.mojom.BrowserAppInstanceType',
+    'x:crosapi.mojom.CreationResult',
+    'x:crosapi.mojom.DeviceAccessResultCode',
+    'x:crosapi.mojom.DeviceMode',
+    'x:crosapi.mojom.DlpRestrictionLevel',
+    'x:crosapi.mojom.ExoImeSupport',
+    'x:crosapi.mojom.FullscreenVisibility',
+    'x:crosapi.mojom.GoogleServiceAuthError.State',
+    'x:crosapi.mojom.IsInstallableResult',
+    'x:crosapi.mojom.KeyTag',
+    'x:crosapi.mojom.KeystoreSigningAlgorithmName',
+    'x:crosapi.mojom.KeystoreType',
+    'x:crosapi.mojom.LacrosFeedbackSource',
+    'x:crosapi.mojom.MemoryPressureLevel',
+    'x:crosapi.mojom.MetricsReportingManaged',
+    'x:crosapi.mojom.NotificationType',
+    'x:crosapi.mojom.OndeviceHandwritingSupport',
+    'x:crosapi.mojom.OpenResult',
+    'x:crosapi.mojom.PolicyDomain',
+    'x:crosapi.mojom.RegistrationCodeType',
+    'x:crosapi.mojom.ScaleFactor',
+    'x:crosapi.mojom.SearchResult.OptionalBool',
+    'x:crosapi.mojom.SelectFileDialogType',
+    'x:crosapi.mojom.SelectFileResult',
+    'x:crosapi.mojom.SharesheetResult',
+    'x:crosapi.mojom.TouchEventType',
+    'x:crosapi.mojom.VideoRotation',
+    'x:crosapi.mojom.WallpaperLayout',
+    'x:crosapi.mojom.WebAppInstallResultCode',
+    'x:crosapi.mojom.WebAppUninstallResultCode',
+    'x:device.mojom.HidBusType',
+    'x:device.mojom.WakeLockReason',
+    'x:device.mojom.WakeLockType',
+    'x:drivefs.mojom.DialogReason.Type',
+    'x:drivefs.mojom.DriveError.Type',
+    'x:drivefs.mojom.DriveFsDelegate.ExtensionConnectionStatus',
+    'x:drivefs.mojom.FileMetadata.CanPinStatus',
+    'x:drivefs.mojom.FileMetadata.Type',
+    'x:drivefs.mojom.ItemEventReason',
+    'x:drivefs.mojom.MirrorPathStatus',
+    'x:drivefs.mojom.MirrorSyncStatus',
+    'x:drivefs.mojom.QueryParameters.SortField',
+    'x:fuzz.mojom.FuzzEnum',
+    'x:media.mojom.FillLightMode',
+    'x:media.mojom.MeteringMode',
+    'x:media.mojom.PowerLineFrequency',
+    'x:media.mojom.RedEyeReduction',
+    'x:media.mojom.ResolutionChangePolicy',
+    'x:media.mojom.VideoCaptureApi',
+    'x:media.mojom.VideoCaptureBufferType',
+    'x:media.mojom.VideoCaptureError',
+    'x:media.mojom.VideoCaptureFrameDropReason',
+    'x:media.mojom.VideoCapturePixelFormat',
+    'x:media.mojom.VideoCaptureTransportType',
+    'x:media.mojom.VideoFacingMode',
+    'x:media_session.mojom.AudioFocusType',
+    'x:media_session.mojom.CameraState',
+    'x:media_session.mojom.EnforcementMode',
+    'x:media_session.mojom.MediaAudioVideoState',
+    'x:media_session.mojom.MediaImageBitmapColorType',
+    'x:media_session.mojom.MediaPictureInPictureState',
+    'x:media_session.mojom.MediaPlaybackState',
+    'x:media_session.mojom.MediaSession.SuspendType',
+    'x:media_session.mojom.MediaSessionAction',
+    'x:media_session.mojom.MediaSessionImageType',
+    'x:media_session.mojom.MediaSessionInfo.SessionState',
+    'x:media_session.mojom.MicrophoneState',
+    'x:ml.model_loader.mojom.ComputeResult',
+    'x:ml.model_loader.mojom.CreateModelLoaderResult',
+    'x:ml.model_loader.mojom.LoadModelResult',
+    'x:mojo.test.AnExtensibleEnum',
+    'x:mojo.test.EnumB',
+    'x:mojo.test.ExtensibleEmptyEnum',
+    'x:mojo.test.enum_default_unittest.mojom.ExtensibleEnumWithoutDefault',
+    'x:network.mojom.WebSandboxFlags',
+    'x:payments.mojom.BillingResponseCode',
+    'x:payments.mojom.CreateDigitalGoodsResponseCode',
+    'x:payments.mojom.ItemType',
+    'x:printing.mojom.PrinterType',
+    'x:ui.mojom.KeyboardCode',
+)
+### DO NOT ADD ENTRIES TO THIS LIST. ###
 
 
 def _DuplicateName(values):
@@ -98,12 +375,6 @@
   }
   if kind.endswith('?'):
     base_kind = _MapKind(kind[0:-1])
-    # NOTE: This doesn't rule out enum types. Those will be detected later, when
-    # cross-reference is established.
-    reference_kinds = ('m', 's', 'h', 'a', 'r', 'x', 'asso', 'rmt', 'rcv',
-                       'rma', 'rca')
-    if re.split('[^a-z]', base_kind, 1)[0] not in reference_kinds:
-      raise Exception('A type (spec "%s") cannot be made nullable' % base_kind)
     return '?' + base_kind
   if kind.endswith('}'):
     lbracket = kind.rfind('{')
@@ -113,8 +384,6 @@
     lbracket = kind.rfind('[')
     typename = kind[0:lbracket]
     return 'a' + kind[lbracket + 1:-1] + ':' + _MapKind(typename)
-  if kind.endswith('&'):
-    return 'r:' + _MapKind(kind[0:-1])
   if kind.startswith('asso<'):
     assert kind.endswith('>')
     return 'asso:' + _MapKind(kind[5:-1])
@@ -135,13 +404,45 @@
   return 'x:' + kind
 
 
-def _AttributeListToDict(attribute_list):
+def _MapAttributeValue(module, kind, value):
+  # True/False/None
+  if value is None:
+    return value
+  if not isinstance(value, str):
+    return value
+  # Is the attribute value the name of a feature?
+  try:
+    # Features cannot be nested in other types, so lookup in the global scope.
+    trial = _LookupKind(module.kinds, 'x:' + value,
+                        _GetScopeForKind(module, kind))
+    if isinstance(trial, mojom.Feature):
+      return trial
+  except ValueError:
+    pass
+  # Is the attribute value a constant or enum value?
+  try:
+    trial = _LookupValue(module, None, None, ('IDENTIFIER', value))
+    if isinstance(trial, mojom.ConstantValue):
+      return trial.constant
+    if isinstance(trial, mojom.EnumValue):
+      return trial
+  except ValueError:
+    pass
+  # If not a referenceable mojo type - return as a string.
+  return value
+
+
+def _AttributeListToDict(module, kind, attribute_list):
   if attribute_list is None:
     return None
   assert isinstance(attribute_list, ast.AttributeList)
-  # TODO(vtl): Check for duplicate keys here.
-  return dict(
-      [(attribute.key, attribute.value) for attribute in attribute_list])
+  attributes = dict()
+  for attribute in attribute_list:
+    if attribute.key in attributes:
+      raise Exception("Duplicate key (%s) in attribute list" % attribute.key)
+    attributes[attribute.key] = _MapAttributeValue(module, kind,
+                                                   attribute.value)
+  return attributes
 
 
 builtin_values = frozenset([
@@ -257,7 +558,8 @@
     return kind
 
   if spec.startswith('?'):
-    kind = _Kind(kinds, spec[1:], scope).MakeNullableKind()
+    kind = _Kind(kinds, spec[1:], scope)
+    kind = kind.MakeNullableKind()
   elif spec.startswith('a:'):
     kind = mojom.Array(_Kind(kinds, spec[2:], scope))
   elif spec.startswith('asso:'):
@@ -303,7 +605,8 @@
 
 def _Import(module, import_module):
   # Copy the struct kinds from our imports into the current module.
-  importable_kinds = (mojom.Struct, mojom.Union, mojom.Enum, mojom.Interface)
+  importable_kinds = (mojom.Struct, mojom.Union, mojom.Enum, mojom.Interface,
+                      mojom.Feature)
   for kind in import_module.kinds.values():
     if (isinstance(kind, importable_kinds)
         and kind.module.path == import_module.path):
@@ -316,6 +619,32 @@
   return import_module
 
 
+def _Feature(module, parsed_feature):
+  """
+  Args:
+    module: {mojom.Module} Module currently being constructed.
+    parsed_feature: {ast.Feature} Parsed feature.
+
+  Returns:
+    {mojom.Feature} AST feature.
+  """
+  feature = mojom.Feature(module=module)
+  feature.mojom_name = parsed_feature.mojom_name
+  feature.spec = 'x:' + module.GetNamespacePrefix() + feature.mojom_name
+  module.kinds[feature.spec] = feature
+  feature.constants = []
+  _ProcessElements(
+      parsed_feature.mojom_name, parsed_feature.body, {
+          ast.Const:
+          lambda const: feature.constants.append(
+              _Constant(module, const, feature)),
+      })
+
+  feature.attributes = _AttributeListToDict(module, feature,
+                                            parsed_feature.attribute_list)
+  return feature
+
+
 def _Struct(module, parsed_struct):
   """
   Args:
@@ -345,7 +674,8 @@
             struct.fields_data.append,
         })
 
-  struct.attributes = _AttributeListToDict(parsed_struct.attribute_list)
+  struct.attributes = _AttributeListToDict(module, struct,
+                                           parsed_struct.attribute_list)
 
   # Enforce that a [Native] attribute is set to make native-only struct
   # declarations more explicit.
@@ -377,7 +707,8 @@
   union.fields_data = []
   _ProcessElements(parsed_union.mojom_name, parsed_union.body,
                    {ast.UnionField: union.fields_data.append})
-  union.attributes = _AttributeListToDict(parsed_union.attribute_list)
+  union.attributes = _AttributeListToDict(module, union,
+                                          parsed_union.attribute_list)
   return union
 
 
@@ -398,7 +729,8 @@
   field.ordinal = parsed_field.ordinal.value if parsed_field.ordinal else None
   field.default = _LookupValue(module, struct, field.kind,
                                parsed_field.default_value)
-  field.attributes = _AttributeListToDict(parsed_field.attribute_list)
+  field.attributes = _AttributeListToDict(module, field,
+                                          parsed_field.attribute_list)
   return field
 
 
@@ -414,11 +746,22 @@
   """
   field = mojom.UnionField()
   field.mojom_name = parsed_field.mojom_name
+  # Disallow unions from being self-recursive.
+  parsed_typename = parsed_field.typename
+  if parsed_typename.endswith('?'):
+    parsed_typename = parsed_typename[:-1]
+  assert parsed_typename != union.mojom_name
   field.kind = _Kind(module.kinds, _MapKind(parsed_field.typename),
                      (module.mojom_namespace, union.mojom_name))
   field.ordinal = parsed_field.ordinal.value if parsed_field.ordinal else None
   field.default = None
-  field.attributes = _AttributeListToDict(parsed_field.attribute_list)
+  field.attributes = _AttributeListToDict(module, field,
+                                          parsed_field.attribute_list)
+  if field.is_default and not mojom.IsNullableKind(field.kind) and \
+     not mojom.IsIntegralKind(field.kind):
+    raise Exception(
+        '[Default] field for union %s must be nullable or integral type.' %
+        union.mojom_name)
   return field
 
 
@@ -439,7 +782,8 @@
   parameter.ordinal = (parsed_param.ordinal.value
                        if parsed_param.ordinal else None)
   parameter.default = None  # TODO(tibell): We never have these. Remove field?
-  parameter.attributes = _AttributeListToDict(parsed_param.attribute_list)
+  parameter.attributes = _AttributeListToDict(module, parameter,
+                                              parsed_param.attribute_list)
   return parameter
 
 
@@ -464,7 +808,8 @@
     method.response_parameters = list(
         map(lambda parameter: _Parameter(module, parameter, interface),
             parsed_method.response_parameter_list))
-  method.attributes = _AttributeListToDict(parsed_method.attribute_list)
+  method.attributes = _AttributeListToDict(module, method,
+                                           parsed_method.attribute_list)
 
   # Enforce that only methods with response can have a [Sync] attribute.
   if method.sync and method.response_parameters is None:
@@ -492,7 +837,8 @@
   interface.mojom_name = parsed_iface.mojom_name
   interface.spec = 'x:' + module.GetNamespacePrefix() + interface.mojom_name
   module.kinds[interface.spec] = interface
-  interface.attributes = _AttributeListToDict(parsed_iface.attribute_list)
+  interface.attributes = _AttributeListToDict(module, interface,
+                                              parsed_iface.attribute_list)
   interface.enums = []
   interface.constants = []
   interface.methods_data = []
@@ -522,7 +868,8 @@
   field = mojom.EnumField()
   field.mojom_name = parsed_field.mojom_name
   field.value = _LookupValue(module, enum, None, parsed_field.value)
-  field.attributes = _AttributeListToDict(parsed_field.attribute_list)
+  field.attributes = _AttributeListToDict(module, field,
+                                          parsed_field.attribute_list)
   value = mojom.EnumValue(module, enum, field)
   module.values[value.GetSpec()] = value
   return field
@@ -544,7 +891,7 @@
       prev_value += 1
 
     # Integral value (e.g: BEGIN = -0x1).
-    elif _IsStrOrUnicode(field.value):
+    elif isinstance(field.value, str):
       prev_value = int(field.value, 0)
 
     # Reference to a previous enum value (e.g: INIT = BEGIN).
@@ -560,7 +907,10 @@
     else:
       raise Exception('Unresolved enum value for %s' % field.value.GetSpec())
 
-    #resolved_enum_values[field.mojom_name] = prev_value
+    if prev_value in (-128, -127):
+      raise Exception(f'{field.mojom_name} in {enum.spec} has the value '
+                      f'{prev_value}, which is reserved for WTF::HashTrait\'s '
+                      'default enum specialization and may not be used.')
     field.numeric_value = prev_value
     if min_value is None or prev_value < min_value:
       min_value = prev_value
@@ -588,7 +938,8 @@
     mojom_name = parent_kind.mojom_name + '.' + mojom_name
   enum.spec = 'x:%s.%s' % (module.mojom_namespace, mojom_name)
   enum.parent_kind = parent_kind
-  enum.attributes = _AttributeListToDict(parsed_enum.attribute_list)
+  enum.attributes = _AttributeListToDict(module, enum,
+                                         parsed_enum.attribute_list)
 
   if not enum.native_only:
     enum.fields = list(
@@ -600,11 +951,18 @@
     for field in enum.fields:
       if field.default:
         if not enum.extensible:
-          raise Exception('Non-extensible enums may not specify a default')
-        if enum.default_field is not None:
           raise Exception(
-              'Only one enumerator value may be specified as the default')
+              f'Non-extensible enum {enum.spec} may not specify a default')
+        if enum.default_field is not None:
+          raise Exception(f'Multiple [Default] enumerators in enum {enum.spec}')
         enum.default_field = field
+    # While running the backwards compatibility check, ignore errors because the
+    # old version of the enum might not specify [Default].
+    if (enum.extensible and enum.default_field is None
+        and enum.spec not in _EXTENSIBLE_ENUMS_MISSING_DEFAULT
+        and not is_running_backwards_compatibility_check_hack):
+      raise Exception(
+          f'Extensible enum {enum.spec} must specify a [Default] enumerator')
 
   module.kinds[enum.spec] = enum
 
@@ -696,6 +1054,11 @@
         for referenced_kind in extract_referenced_user_kinds(param.kind):
           sanitized_kind = sanitize_kind(referenced_kind)
           referenced_user_kinds[sanitized_kind.spec] = sanitized_kind
+  # Consts can reference imported enums.
+  for const in module.constants:
+    if not const.kind in mojom.PRIMITIVES:
+      sanitized_kind = sanitize_kind(const.kind)
+      referenced_user_kinds[sanitized_kind.spec] = sanitized_kind
 
   return referenced_user_kinds
 
@@ -741,6 +1104,16 @@
           assertDependencyIsStable(response_param.kind)
 
 
+def _AssertStructIsValid(kind):
+  expected_ordinals = set(range(0, len(kind.fields)))
+  ordinals = set(map(lambda field: field.ordinal, kind.fields))
+  if ordinals != expected_ordinals:
+    raise Exception(
+        'Structs must use contiguous ordinals starting from 0. ' +
+        '{} is missing the following ordinals: {}.'.format(
+            kind.mojom_name, ', '.join(map(str, expected_ordinals - ordinals))))
+
+
 def _Module(tree, path, imports):
   """
   Args:
@@ -778,6 +1151,8 @@
   module.structs = []
   module.unions = []
   module.interfaces = []
+  module.features = []
+
   _ProcessElements(
       filename, tree.definition_list, {
           ast.Const:
@@ -791,6 +1166,8 @@
           ast.Interface:
           lambda interface: module.interfaces.append(
               _Interface(module, interface)),
+          ast.Feature:
+          lambda feature: module.features.append(_Feature(module, feature)),
       })
 
   # Second pass expands fields and methods. This allows fields and parameters
@@ -806,12 +1183,24 @@
     for enum in struct.enums:
       all_defined_kinds[enum.spec] = enum
 
+  for feature in module.features:
+    all_defined_kinds[feature.spec] = feature
+
   for union in module.unions:
     union.fields = list(
         map(lambda field: _UnionField(module, field, union), union.fields_data))
     _AssignDefaultOrdinals(union.fields)
+    for field in union.fields:
+      if field.is_default:
+        if union.default_field is not None:
+          raise Exception('Multiple [Default] fields in union %s.' %
+                          union.mojom_name)
+        union.default_field = field
     del union.fields_data
     all_defined_kinds[union.spec] = union
+    if union.extensible and union.default_field is None:
+      raise Exception('Extensible union %s must specify a [Default] field' %
+                      union.mojom_name)
 
   for interface in module.interfaces:
     interface.methods = list(
@@ -829,8 +1218,8 @@
                                                  all_defined_kinds.values())
   imported_kind_specs = set(all_referenced_kinds.keys()).difference(
       set(all_defined_kinds.keys()))
-  module.imported_kinds = dict(
-      (spec, all_referenced_kinds[spec]) for spec in imported_kind_specs)
+  module.imported_kinds = OrderedDict((spec, all_referenced_kinds[spec])
+                                      for spec in sorted(imported_kind_specs))
 
   generator.AddComputedData(module)
   for iface in module.interfaces:
@@ -847,6 +1236,9 @@
       if kind.stable:
         _AssertTypeIsStable(kind)
 
+  for kind in module.structs:
+    _AssertStructIsValid(kind)
+
   return module
 
 
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/generate/translate_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom/generate/translate_unittest.py
index 19905c8..b4fea92 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/generate/translate_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/generate/translate_unittest.py
@@ -1,17 +1,13 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import imp
-import os.path
-import sys
 import unittest
 
 from mojom.generate import module as mojom
 from mojom.generate import translate
 from mojom.parse import ast
 
-
 class TranslateTest(unittest.TestCase):
   """Tests |parser.Parse()|."""
 
@@ -69,5 +65,77 @@
     # pylint: disable=W0212
     self.assertEquals(
         translate._MapKind("asso<SomeInterface>?"), "?asso:x:SomeInterface")
-    self.assertEquals(
-        translate._MapKind("asso<SomeInterface&>?"), "?asso:r:x:SomeInterface")
+    self.assertEquals(translate._MapKind("rca<SomeInterface>?"),
+                      "?rca:x:SomeInterface")
+
+  def testSelfRecursiveUnions(self):
+    """Verifies _UnionField() raises when a union is self-recursive."""
+    tree = ast.Mojom(None, ast.ImportList(), [
+        ast.Union("SomeUnion", None,
+                  ast.UnionBody([ast.UnionField("a", None, None, "SomeUnion")]))
+    ])
+    with self.assertRaises(Exception):
+      translate.OrderedModule(tree, "mojom_tree", [])
+
+    tree = ast.Mojom(None, ast.ImportList(), [
+        ast.Union(
+            "SomeUnion", None,
+            ast.UnionBody([ast.UnionField("a", None, None, "SomeUnion?")]))
+    ])
+    with self.assertRaises(Exception):
+      translate.OrderedModule(tree, "mojom_tree", [])
+
+  def testDuplicateAttributesException(self):
+    tree = ast.Mojom(None, ast.ImportList(), [
+        ast.Union(
+            "FakeUnion",
+            ast.AttributeList([
+                ast.Attribute("key1", "value"),
+                ast.Attribute("key1", "value")
+            ]),
+            ast.UnionBody([
+                ast.UnionField("a", None, None, "int32"),
+                ast.UnionField("b", None, None, "string")
+            ]))
+    ])
+    with self.assertRaises(Exception):
+      translate.OrderedModule(tree, "mojom_tree", [])
+
+  def testEnumWithReservedValues(self):
+    """Verifies that assigning reserved values to enumerators fails."""
+    # -128 is reserved for the empty representation in WTF::HashTraits.
+    tree = ast.Mojom(None, ast.ImportList(), [
+        ast.Enum(
+            "MyEnum", None,
+            ast.EnumValueList([
+                ast.EnumValue('kReserved', None, '-128'),
+            ]))
+    ])
+    with self.assertRaises(Exception) as context:
+      translate.OrderedModule(tree, "mojom_tree", [])
+    self.assertIn("reserved for WTF::HashTrait", str(context.exception))
+
+    # -127 is reserved for the deleted representation in WTF::HashTraits.
+    tree = ast.Mojom(None, ast.ImportList(), [
+        ast.Enum(
+            "MyEnum", None,
+            ast.EnumValueList([
+                ast.EnumValue('kReserved', None, '-127'),
+            ]))
+    ])
+    with self.assertRaises(Exception) as context:
+      translate.OrderedModule(tree, "mojom_tree", [])
+    self.assertIn("reserved for WTF::HashTrait", str(context.exception))
+
+    # Implicitly assigning a reserved value should also fail.
+    tree = ast.Mojom(None, ast.ImportList(), [
+        ast.Enum(
+            "MyEnum", None,
+            ast.EnumValueList([
+                ast.EnumValue('kNotReserved', None, '-129'),
+                ast.EnumValue('kImplicitlyReserved', None, None),
+            ]))
+    ])
+    with self.assertRaises(Exception) as context:
+      translate.OrderedModule(tree, "mojom_tree", [])
+    self.assertIn("reserved for WTF::HashTrait", str(context.exception))
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/parse/ast.py b/utils/ipc/mojo/public/tools/mojom/mojom/parse/ast.py
index 1f0db20..aae9cdb 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/parse/ast.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/parse/ast.py
@@ -1,4 +1,4 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """Node classes for the AST for a Mojo IDL file."""
@@ -8,17 +8,14 @@
 # and lineno). You may also define __repr__() to help with analyzing test
 # failures, especially for more complex types.
 
-
-import sys
+import os.path
 
 
-def _IsStrOrUnicode(x):
-  if sys.version_info[0] < 3:
-    return isinstance(x, (unicode, str))
-  return isinstance(x, str)
+# Instance of 'NodeListBase' has no '_list_item_type' member (no-member)
+# pylint: disable=no-member
 
 
-class NodeBase(object):
+class NodeBase:
   """Base class for nodes in the AST."""
 
   def __init__(self, filename=None, lineno=None):
@@ -43,7 +40,7 @@
   classes, in a tuple) of the members of the list.)"""
 
   def __init__(self, item_or_items=None, **kwargs):
-    super(NodeListBase, self).__init__(**kwargs)
+    super().__init__(**kwargs)
     self.items = []
     if item_or_items is None:
       pass
@@ -62,7 +59,7 @@
     return self.items.__iter__()
 
   def __eq__(self, other):
-    return super(NodeListBase, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.items == other.items
 
   # Implement this so that on failure, we get slightly more sensible output.
@@ -96,7 +93,7 @@
   include parameter definitions.) This class is meant to be subclassed."""
 
   def __init__(self, mojom_name, **kwargs):
-    assert _IsStrOrUnicode(mojom_name)
+    assert isinstance(mojom_name, str)
     NodeBase.__init__(self, **kwargs)
     self.mojom_name = mojom_name
 
@@ -108,13 +105,13 @@
   """Represents an attribute."""
 
   def __init__(self, key, value, **kwargs):
-    assert _IsStrOrUnicode(key)
-    super(Attribute, self).__init__(**kwargs)
+    assert isinstance(key, str)
+    super().__init__(**kwargs)
     self.key = key
     self.value = value
 
   def __eq__(self, other):
-    return super(Attribute, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.key == other.key and \
            self.value == other.value
 
@@ -131,17 +128,17 @@
   def __init__(self, mojom_name, attribute_list, typename, value, **kwargs):
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
     # The typename is currently passed through as a string.
-    assert _IsStrOrUnicode(typename)
+    assert isinstance(typename, str)
     # The value is either a literal (currently passed through as a string) or a
     # "wrapped identifier".
-    assert _IsStrOrUnicode or isinstance(value, tuple)
-    super(Const, self).__init__(mojom_name, **kwargs)
+    assert isinstance(value, (tuple, str))
+    super().__init__(mojom_name, **kwargs)
     self.attribute_list = attribute_list
     self.typename = typename
     self.value = value
 
   def __eq__(self, other):
-    return super(Const, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.typename == other.typename and \
            self.value == other.value
@@ -153,12 +150,12 @@
   def __init__(self, mojom_name, attribute_list, enum_value_list, **kwargs):
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
     assert enum_value_list is None or isinstance(enum_value_list, EnumValueList)
-    super(Enum, self).__init__(mojom_name, **kwargs)
+    super().__init__(mojom_name, **kwargs)
     self.attribute_list = attribute_list
     self.enum_value_list = enum_value_list
 
   def __eq__(self, other):
-    return super(Enum, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.enum_value_list == other.enum_value_list
 
@@ -170,13 +167,13 @@
     # The optional value is either an int (which is current a string) or a
     # "wrapped identifier".
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
-    assert value is None or _IsStrOrUnicode(value) or isinstance(value, tuple)
-    super(EnumValue, self).__init__(mojom_name, **kwargs)
+    assert value is None or isinstance(value, (tuple, str))
+    super().__init__(mojom_name, **kwargs)
     self.attribute_list = attribute_list
     self.value = value
 
   def __eq__(self, other):
-    return super(EnumValue, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.value == other.value
 
@@ -188,18 +185,47 @@
   _list_item_type = EnumValue
 
 
+class Feature(Definition):
+  """Represents a runtime feature definition."""
+  def __init__(self, mojom_name, attribute_list, body, **kwargs):
+    assert attribute_list is None or isinstance(attribute_list, AttributeList)
+    assert isinstance(body, FeatureBody) or body is None
+    super().__init__(mojom_name, **kwargs)
+    self.attribute_list = attribute_list
+    self.body = body
+
+  def __eq__(self, other):
+    return super().__eq__(other) and \
+           self.attribute_list == other.attribute_list and \
+           self.body == other.body
+
+  def __repr__(self):
+    return "Feature(mojom_name = %s, attribute_list = %s, body = %s)" % (
+        self.mojom_name, self.attribute_list, self.body)
+
+
+# This needs to be declared after `FeatureConst` and `FeatureField`.
+class FeatureBody(NodeListBase):
+  """Represents the body of (i.e., list of definitions inside) a feature."""
+
+  # Features are compile time helpers so all fields are initializers/consts
+  # for the underlying platform feature type.
+  _list_item_type = (Const)
+
+
 class Import(NodeBase):
   """Represents an import statement."""
 
   def __init__(self, attribute_list, import_filename, **kwargs):
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
-    assert _IsStrOrUnicode(import_filename)
-    super(Import, self).__init__(**kwargs)
+    assert isinstance(import_filename, str)
+    super().__init__(**kwargs)
     self.attribute_list = attribute_list
-    self.import_filename = import_filename
+    # TODO(crbug.com/953884): Use pathlib once we're migrated fully to Python 3.
+    self.import_filename = os.path.normpath(import_filename).replace('\\', '/')
 
   def __eq__(self, other):
-    return super(Import, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.import_filename == other.import_filename
 
@@ -216,12 +242,12 @@
   def __init__(self, mojom_name, attribute_list, body, **kwargs):
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
     assert isinstance(body, InterfaceBody)
-    super(Interface, self).__init__(mojom_name, **kwargs)
+    super().__init__(mojom_name, **kwargs)
     self.attribute_list = attribute_list
     self.body = body
 
   def __eq__(self, other):
-    return super(Interface, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.body == other.body
 
@@ -236,14 +262,14 @@
     assert isinstance(parameter_list, ParameterList)
     assert response_parameter_list is None or \
            isinstance(response_parameter_list, ParameterList)
-    super(Method, self).__init__(mojom_name, **kwargs)
+    super().__init__(mojom_name, **kwargs)
     self.attribute_list = attribute_list
     self.ordinal = ordinal
     self.parameter_list = parameter_list
     self.response_parameter_list = response_parameter_list
 
   def __eq__(self, other):
-    return super(Method, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.ordinal == other.ordinal and \
            self.parameter_list == other.parameter_list and \
@@ -264,12 +290,12 @@
     # |mojom_namespace| is either none or a "wrapped identifier".
     assert mojom_namespace is None or isinstance(mojom_namespace, tuple)
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
-    super(Module, self).__init__(**kwargs)
+    super().__init__(**kwargs)
     self.mojom_namespace = mojom_namespace
     self.attribute_list = attribute_list
 
   def __eq__(self, other):
-    return super(Module, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.mojom_namespace == other.mojom_namespace and \
            self.attribute_list == other.attribute_list
 
@@ -281,13 +307,13 @@
     assert module is None or isinstance(module, Module)
     assert isinstance(import_list, ImportList)
     assert isinstance(definition_list, list)
-    super(Mojom, self).__init__(**kwargs)
+    super().__init__(**kwargs)
     self.module = module
     self.import_list = import_list
     self.definition_list = definition_list
 
   def __eq__(self, other):
-    return super(Mojom, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.module == other.module and \
            self.import_list == other.import_list and \
            self.definition_list == other.definition_list
@@ -302,11 +328,11 @@
 
   def __init__(self, value, **kwargs):
     assert isinstance(value, int)
-    super(Ordinal, self).__init__(**kwargs)
+    super().__init__(**kwargs)
     self.value = value
 
   def __eq__(self, other):
-    return super(Ordinal, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.value == other.value
 
 
@@ -314,18 +340,18 @@
   """Represents a method request or response parameter."""
 
   def __init__(self, mojom_name, attribute_list, ordinal, typename, **kwargs):
-    assert _IsStrOrUnicode(mojom_name)
+    assert isinstance(mojom_name, str)
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
     assert ordinal is None or isinstance(ordinal, Ordinal)
-    assert _IsStrOrUnicode(typename)
-    super(Parameter, self).__init__(**kwargs)
+    assert isinstance(typename, str)
+    super().__init__(**kwargs)
     self.mojom_name = mojom_name
     self.attribute_list = attribute_list
     self.ordinal = ordinal
     self.typename = typename
 
   def __eq__(self, other):
-    return super(Parameter, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.mojom_name == other.mojom_name and \
            self.attribute_list == other.attribute_list and \
            self.ordinal == other.ordinal and \
@@ -344,42 +370,51 @@
   def __init__(self, mojom_name, attribute_list, body, **kwargs):
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
     assert isinstance(body, StructBody) or body is None
-    super(Struct, self).__init__(mojom_name, **kwargs)
+    super().__init__(mojom_name, **kwargs)
     self.attribute_list = attribute_list
     self.body = body
 
   def __eq__(self, other):
-    return super(Struct, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.body == other.body
 
+  def __repr__(self):
+    return "Struct(mojom_name = %s, attribute_list = %s, body = %s)" % (
+        self.mojom_name, self.attribute_list, self.body)
+
 
 class StructField(Definition):
   """Represents a struct field definition."""
 
   def __init__(self, mojom_name, attribute_list, ordinal, typename,
                default_value, **kwargs):
-    assert _IsStrOrUnicode(mojom_name)
+    assert isinstance(mojom_name, str)
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
     assert ordinal is None or isinstance(ordinal, Ordinal)
-    assert _IsStrOrUnicode(typename)
+    assert isinstance(typename, str)
     # The optional default value is currently either a value as a string or a
     # "wrapped identifier".
-    assert default_value is None or _IsStrOrUnicode(default_value) or \
-        isinstance(default_value, tuple)
-    super(StructField, self).__init__(mojom_name, **kwargs)
+    assert default_value is None or isinstance(default_value, (str, tuple))
+    super().__init__(mojom_name, **kwargs)
     self.attribute_list = attribute_list
     self.ordinal = ordinal
     self.typename = typename
     self.default_value = default_value
 
   def __eq__(self, other):
-    return super(StructField, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.ordinal == other.ordinal and \
            self.typename == other.typename and \
            self.default_value == other.default_value
 
+  def __repr__(self):
+    return ("StructField(mojom_name = %s, attribute_list = %s, ordinal = %s, "
+            "typename = %s, default_value = %s") % (
+                self.mojom_name, self.attribute_list, self.ordinal,
+                self.typename, self.default_value)
+
 
 # This needs to be declared after |StructField|.
 class StructBody(NodeListBase):
@@ -394,29 +429,29 @@
   def __init__(self, mojom_name, attribute_list, body, **kwargs):
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
     assert isinstance(body, UnionBody)
-    super(Union, self).__init__(mojom_name, **kwargs)
+    super().__init__(mojom_name, **kwargs)
     self.attribute_list = attribute_list
     self.body = body
 
   def __eq__(self, other):
-    return super(Union, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.body == other.body
 
 
 class UnionField(Definition):
   def __init__(self, mojom_name, attribute_list, ordinal, typename, **kwargs):
-    assert _IsStrOrUnicode(mojom_name)
+    assert isinstance(mojom_name, str)
     assert attribute_list is None or isinstance(attribute_list, AttributeList)
     assert ordinal is None or isinstance(ordinal, Ordinal)
-    assert _IsStrOrUnicode(typename)
-    super(UnionField, self).__init__(mojom_name, **kwargs)
+    assert isinstance(typename, str)
+    super().__init__(mojom_name, **kwargs)
     self.attribute_list = attribute_list
     self.ordinal = ordinal
     self.typename = typename
 
   def __eq__(self, other):
-    return super(UnionField, self).__eq__(other) and \
+    return super().__eq__(other) and \
            self.attribute_list == other.attribute_list and \
            self.ordinal == other.ordinal and \
            self.typename == other.typename
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/parse/ast_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom/parse/ast_unittest.py
index 6279863..b289f7b 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/parse/ast_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/parse/ast_unittest.py
@@ -1,32 +1,26 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import imp
-import os.path
-import sys
 import unittest
 
 from mojom.parse import ast
 
-
 class _TestNode(ast.NodeBase):
   """Node type for tests."""
 
   def __init__(self, value, **kwargs):
-    super(_TestNode, self).__init__(**kwargs)
+    super().__init__(**kwargs)
     self.value = value
 
   def __eq__(self, other):
-    return super(_TestNode, self).__eq__(other) and self.value == other.value
-
+    return super().__eq__(other) and self.value == other.value
 
 class _TestNodeList(ast.NodeListBase):
   """Node list type for tests."""
 
   _list_item_type = _TestNode
 
-
 class ASTTest(unittest.TestCase):
   """Tests various AST classes."""
 
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/parse/conditional_features.py b/utils/ipc/mojo/public/tools/mojom/mojom/parse/conditional_features.py
index 3cb73c5..9687edb 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/parse/conditional_features.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/parse/conditional_features.py
@@ -1,4 +1,4 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
+# Copyright 2018 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """Helpers for processing conditionally enabled features in a mojom."""
@@ -17,8 +17,10 @@
 def _IsEnabled(definition, enabled_features):
   """Returns true if a definition is enabled.
 
-  A definition is enabled if it has no EnableIf attribute, or if the value of
-  the EnableIf attribute is in enabled_features.
+  A definition is enabled if it has no EnableIf/EnableIfNot attribute.
+  It is retained if it has an EnableIf attribute and the attribute is in
+  enabled_features. It is retained if it has an EnableIfNot attribute and the
+  attribute is not in enabled features.
   """
   if not hasattr(definition, "attribute_list"):
     return True
@@ -27,17 +29,19 @@
 
   already_defined = False
   for a in definition.attribute_list:
-    if a.key == 'EnableIf':
+    if a.key == 'EnableIf' or a.key == 'EnableIfNot':
       if already_defined:
         raise EnableIfError(
             definition.filename,
-            "EnableIf attribute may only be defined once per field.",
+            "EnableIf/EnableIfNot attribute may only be set once per field.",
             definition.lineno)
       already_defined = True
 
   for attribute in definition.attribute_list:
     if attribute.key == 'EnableIf' and attribute.value not in enabled_features:
       return False
+    if attribute.key == 'EnableIfNot' and attribute.value in enabled_features:
+      return False
   return True
 
 
@@ -56,15 +60,12 @@
   """Filters definitions with a body."""
   if isinstance(definition, ast.Enum):
     _FilterDisabledFromNodeList(definition.enum_value_list, enabled_features)
-  elif isinstance(definition, ast.Interface):
-    _FilterDisabledFromNodeList(definition.body, enabled_features)
   elif isinstance(definition, ast.Method):
     _FilterDisabledFromNodeList(definition.parameter_list, enabled_features)
     _FilterDisabledFromNodeList(definition.response_parameter_list,
                                 enabled_features)
-  elif isinstance(definition, ast.Struct):
-    _FilterDisabledFromNodeList(definition.body, enabled_features)
-  elif isinstance(definition, ast.Union):
+  elif isinstance(definition,
+                  (ast.Interface, ast.Struct, ast.Union, ast.Feature)):
     _FilterDisabledFromNodeList(definition.body, enabled_features)
 
 
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/parse/conditional_features_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom/parse/conditional_features_unittest.py
index aa609be..cca1764 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/parse/conditional_features_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/parse/conditional_features_unittest.py
@@ -1,13 +1,12 @@
-# Copyright 2018 The Chromium Authors. All rights reserved.
+# Copyright 2018 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import imp
+import importlib.util
 import os
 import sys
 import unittest
 
-
 def _GetDirAbove(dirname):
   """Returns the directory "above" this file containing |dirname| (which must
   also be "above" this file)."""
@@ -18,9 +17,8 @@
     if tail == dirname:
       return path
 
-
 try:
-  imp.find_module('mojom')
+  importlib.util.find_spec("mojom")
 except ImportError:
   sys.path.append(os.path.join(_GetDirAbove('pylib'), 'pylib'))
 import mojom.parse.ast as ast
@@ -29,7 +27,6 @@
 
 ENABLED_FEATURES = frozenset({'red', 'green', 'blue'})
 
-
 class ConditionalFeaturesTest(unittest.TestCase):
   """Tests |mojom.parse.conditional_features|."""
 
@@ -55,6 +52,48 @@
     """
     self.parseAndAssertEqual(const_source, expected_source)
 
+  def testFilterIfNotConst(self):
+    """Test that Consts are correctly filtered."""
+    const_source = """
+      [EnableIfNot=blue]
+      const int kMyConst1 = 1;
+      [EnableIfNot=orange]
+      const double kMyConst2 = 2;
+      [EnableIf=blue]
+      const int kMyConst3 = 3;
+      [EnableIfNot=blue]
+      const int kMyConst4 = 4;
+      [EnableIfNot=purple]
+      const int kMyConst5 = 5;
+    """
+    expected_source = """
+      [EnableIfNot=orange]
+      const double kMyConst2 = 2;
+      [EnableIf=blue]
+      const int kMyConst3 = 3;
+      [EnableIfNot=purple]
+      const int kMyConst5 = 5;
+    """
+    self.parseAndAssertEqual(const_source, expected_source)
+
+  def testFilterIfNotMultipleConst(self):
+    """Test that Consts are correctly filtered."""
+    const_source = """
+      [EnableIfNot=blue]
+      const int kMyConst1 = 1;
+      [EnableIfNot=orange]
+      const double kMyConst2 = 2;
+      [EnableIfNot=orange]
+      const int kMyConst3 = 3;
+    """
+    expected_source = """
+      [EnableIfNot=orange]
+      const double kMyConst2 = 2;
+      [EnableIfNot=orange]
+      const int kMyConst3 = 3;
+    """
+    self.parseAndAssertEqual(const_source, expected_source)
+
   def testFilterEnum(self):
     """Test that EnumValues are correctly filtered from an Enum."""
     enum_source = """
@@ -91,6 +130,24 @@
     """
     self.parseAndAssertEqual(import_source, expected_source)
 
+  def testFilterIfNotImport(self):
+    """Test that imports are correctly filtered from a Mojom."""
+    import_source = """
+      [EnableIf=blue]
+      import "foo.mojom";
+      [EnableIfNot=purple]
+      import "bar.mojom";
+      [EnableIfNot=green]
+      import "baz.mojom";
+    """
+    expected_source = """
+      [EnableIf=blue]
+      import "foo.mojom";
+      [EnableIfNot=purple]
+      import "bar.mojom";
+    """
+    self.parseAndAssertEqual(import_source, expected_source)
+
   def testFilterInterface(self):
     """Test that definitions are correctly filtered from an Interface."""
     interface_source = """
@@ -175,6 +232,50 @@
     """
     self.parseAndAssertEqual(struct_source, expected_source)
 
+  def testFilterIfNotStruct(self):
+    """Test that definitions are correctly filtered from a Struct."""
+    struct_source = """
+      struct MyStruct {
+        [EnableIf=blue]
+        enum MyEnum {
+          VALUE1,
+          [EnableIfNot=red]
+          VALUE2,
+        };
+        [EnableIfNot=yellow]
+        const double kMyConst = 1.23;
+        [EnableIf=green]
+        int32 a;
+        double b;
+        [EnableIfNot=purple]
+        int32 c;
+        [EnableIf=blue]
+        double d;
+        int32 e;
+        [EnableIfNot=red]
+        double f;
+      };
+    """
+    expected_source = """
+      struct MyStruct {
+        [EnableIf=blue]
+        enum MyEnum {
+          VALUE1,
+        };
+        [EnableIfNot=yellow]
+        const double kMyConst = 1.23;
+        [EnableIf=green]
+        int32 a;
+        double b;
+        [EnableIfNot=purple]
+        int32 c;
+        [EnableIf=blue]
+        double d;
+        int32 e;
+      };
+    """
+    self.parseAndAssertEqual(struct_source, expected_source)
+
   def testFilterUnion(self):
     """Test that UnionFields are correctly filtered from a Union."""
     union_source = """
@@ -216,6 +317,25 @@
     """
     self.parseAndAssertEqual(mojom_source, expected_source)
 
+  def testFeaturesWithEnableIf(self):
+    mojom_source = """
+      feature Foo {
+        const string name = "FooFeature";
+        [EnableIf=red]
+        const bool default_state = false;
+        [EnableIf=yellow]
+        const bool default_state = true;
+      };
+    """
+    expected_source = """
+      feature Foo {
+        const string name = "FooFeature";
+        [EnableIf=red]
+        const bool default_state = false;
+      };
+    """
+    self.parseAndAssertEqual(mojom_source, expected_source)
+
   def testMultipleEnableIfs(self):
     source = """
       enum Foo {
@@ -228,6 +348,29 @@
                       conditional_features.RemoveDisabledDefinitions,
                       definition, ENABLED_FEATURES)
 
+  def testMultipleEnableIfs(self):
+    source = """
+      enum Foo {
+        [EnableIf=red,EnableIfNot=yellow]
+        kBarValue = 5,
+      };
+    """
+    definition = parser.Parse(source, "my_file.mojom")
+    self.assertRaises(conditional_features.EnableIfError,
+                      conditional_features.RemoveDisabledDefinitions,
+                      definition, ENABLED_FEATURES)
+
+  def testMultipleEnableIfs(self):
+    source = """
+      enum Foo {
+        [EnableIfNot=red,EnableIfNot=yellow]
+        kBarValue = 5,
+      };
+    """
+    definition = parser.Parse(source, "my_file.mojom")
+    self.assertRaises(conditional_features.EnableIfError,
+                      conditional_features.RemoveDisabledDefinitions,
+                      definition, ENABLED_FEATURES)
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/parse/lexer.py b/utils/ipc/mojo/public/tools/mojom/mojom/parse/lexer.py
index 3e084bb..00136a8 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/parse/lexer.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/parse/lexer.py
@@ -1,8 +1,7 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import imp
 import os.path
 import sys
 
@@ -22,7 +21,7 @@
 
 # We have methods which look like they could be functions:
 # pylint: disable=R0201
-class Lexer(object):
+class Lexer:
   def __init__(self, filename):
     self.filename = filename
 
@@ -56,6 +55,7 @@
       'PENDING_RECEIVER',
       'PENDING_ASSOCIATED_REMOTE',
       'PENDING_ASSOCIATED_RECEIVER',
+      'FEATURE',
   )
 
   keyword_map = {}
@@ -81,7 +81,6 @@
       # Operators
       'MINUS',
       'PLUS',
-      'AMP',
       'QSTN',
 
       # Assignment
@@ -168,7 +167,6 @@
   # Operators
   t_MINUS = r'-'
   t_PLUS = r'\+'
-  t_AMP = r'&'
   t_QSTN = r'\?'
 
   # =
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/parse/lexer_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom/parse/lexer_unittest.py
index eadc658..bc9f835 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/parse/lexer_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/parse/lexer_unittest.py
@@ -1,13 +1,12 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import imp
+import importlib.util
 import os.path
 import sys
 import unittest
 
-
 def _GetDirAbove(dirname):
   """Returns the directory "above" this file containing |dirname| (which must
   also be "above" this file)."""
@@ -18,17 +17,15 @@
     if tail == dirname:
       return path
 
-
 sys.path.insert(1, os.path.join(_GetDirAbove("mojo"), "third_party"))
 from ply import lex
 
 try:
-  imp.find_module("mojom")
+  importlib.util.find_spec("mojom")
 except ImportError:
   sys.path.append(os.path.join(_GetDirAbove("pylib"), "pylib"))
 import mojom.parse.lexer
 
-
 # This (monkey-patching LexToken to make comparison value-based) is evil, but
 # we'll do it anyway. (I'm pretty sure ply's lexer never cares about comparing
 # for object identity.)
@@ -146,7 +143,6 @@
         self._SingleTokenForInput("+"), _MakeLexToken("PLUS", "+"))
     self.assertEquals(
         self._SingleTokenForInput("-"), _MakeLexToken("MINUS", "-"))
-    self.assertEquals(self._SingleTokenForInput("&"), _MakeLexToken("AMP", "&"))
     self.assertEquals(
         self._SingleTokenForInput("?"), _MakeLexToken("QSTN", "?"))
     self.assertEquals(
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/parse/parser.py b/utils/ipc/mojo/public/tools/mojom/mojom/parse/parser.py
index b3b803d..1dffd98 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/parse/parser.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/parse/parser.py
@@ -1,8 +1,11 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """Generates a syntax tree from a Mojo IDL file."""
 
+# Breaking parser stanzas is unhelpful so allow longer lines.
+# pylint: disable=line-too-long
+
 import os.path
 import sys
 
@@ -33,7 +36,7 @@
 
 # We have methods which look like they could be functions:
 # pylint: disable=R0201
-class Parser(object):
+class Parser:
   def __init__(self, lexer, source, filename):
     self.tokens = lexer.tokens
     self.source = source
@@ -111,7 +114,8 @@
                   | union
                   | interface
                   | enum
-                  | const"""
+                  | const
+                  | feature"""
     p[0] = p[1]
 
   def p_attribute_section_1(self, p):
@@ -140,12 +144,19 @@
     p[0].Append(p[3])
 
   def p_attribute_1(self, p):
-    """attribute : NAME EQUALS evaled_literal
-                 | NAME EQUALS NAME"""
-    p[0] = ast.Attribute(p[1], p[3], filename=self.filename, lineno=p.lineno(1))
+    """attribute : name_wrapped EQUALS identifier_wrapped"""
+    p[0] = ast.Attribute(p[1],
+                         p[3][1],
+                         filename=self.filename,
+                         lineno=p.lineno(1))
 
   def p_attribute_2(self, p):
-    """attribute : NAME"""
+    """attribute : name_wrapped EQUALS evaled_literal
+                 | name_wrapped EQUALS name_wrapped"""
+    p[0] = ast.Attribute(p[1], p[3], filename=self.filename, lineno=p.lineno(1))
+
+  def p_attribute_3(self, p):
+    """attribute : name_wrapped"""
     p[0] = ast.Attribute(p[1], True, filename=self.filename, lineno=p.lineno(1))
 
   def p_evaled_literal(self, p):
@@ -161,11 +172,11 @@
       p[0] = eval(p[1])
 
   def p_struct_1(self, p):
-    """struct : attribute_section STRUCT NAME LBRACE struct_body RBRACE SEMI"""
+    """struct : attribute_section STRUCT name_wrapped LBRACE struct_body RBRACE SEMI"""
     p[0] = ast.Struct(p[3], p[1], p[5])
 
   def p_struct_2(self, p):
-    """struct : attribute_section STRUCT NAME SEMI"""
+    """struct : attribute_section STRUCT name_wrapped SEMI"""
     p[0] = ast.Struct(p[3], p[1], None)
 
   def p_struct_body_1(self, p):
@@ -180,11 +191,24 @@
     p[0].Append(p[2])
 
   def p_struct_field(self, p):
-    """struct_field : attribute_section typename NAME ordinal default SEMI"""
+    """struct_field : attribute_section typename name_wrapped ordinal default SEMI"""
     p[0] = ast.StructField(p[3], p[1], p[4], p[2], p[5])
 
+  def p_feature(self, p):
+    """feature : attribute_section FEATURE NAME LBRACE feature_body RBRACE SEMI"""
+    p[0] = ast.Feature(p[3], p[1], p[5])
+
+  def p_feature_body_1(self, p):
+    """feature_body : """
+    p[0] = ast.FeatureBody()
+
+  def p_feature_body_2(self, p):
+    """feature_body : feature_body const"""
+    p[0] = p[1]
+    p[0].Append(p[2])
+
   def p_union(self, p):
-    """union : attribute_section UNION NAME LBRACE union_body RBRACE SEMI"""
+    """union : attribute_section UNION name_wrapped LBRACE union_body RBRACE SEMI"""
     p[0] = ast.Union(p[3], p[1], p[5])
 
   def p_union_body_1(self, p):
@@ -197,7 +221,7 @@
     p[1].Append(p[2])
 
   def p_union_field(self, p):
-    """union_field : attribute_section typename NAME ordinal SEMI"""
+    """union_field : attribute_section typename name_wrapped ordinal SEMI"""
     p[0] = ast.UnionField(p[3], p[1], p[4], p[2])
 
   def p_default_1(self, p):
@@ -209,8 +233,7 @@
     p[0] = p[2]
 
   def p_interface(self, p):
-    """interface : attribute_section INTERFACE NAME LBRACE interface_body \
-                       RBRACE SEMI"""
+    """interface : attribute_section INTERFACE name_wrapped LBRACE interface_body RBRACE SEMI"""
     p[0] = ast.Interface(p[3], p[1], p[5])
 
   def p_interface_body_1(self, p):
@@ -233,8 +256,7 @@
     p[0] = p[3]
 
   def p_method(self, p):
-    """method : attribute_section NAME ordinal LPAREN parameter_list RPAREN \
-                    response SEMI"""
+    """method : attribute_section name_wrapped ordinal LPAREN parameter_list RPAREN response SEMI"""
     p[0] = ast.Method(p[2], p[1], p[3], p[5], p[7])
 
   def p_parameter_list_1(self, p):
@@ -255,7 +277,7 @@
     p[0].Append(p[3])
 
   def p_parameter(self, p):
-    """parameter : attribute_section typename NAME ordinal"""
+    """parameter : attribute_section typename name_wrapped ordinal"""
     p[0] = ast.Parameter(
         p[3], p[1], p[4], p[2], filename=self.filename, lineno=p.lineno(3))
 
@@ -271,8 +293,7 @@
     """nonnullable_typename : basictypename
                             | array
                             | fixed_array
-                            | associative_array
-                            | interfacerequest"""
+                            | associative_array"""
     p[0] = p[1]
 
   def p_basictypename(self, p):
@@ -297,18 +318,16 @@
     p[0] = "rcv<%s>" % p[3]
 
   def p_associatedremotetype(self, p):
-    """associatedremotetype : PENDING_ASSOCIATED_REMOTE LANGLE identifier \
-                                  RANGLE"""
+    """associatedremotetype : PENDING_ASSOCIATED_REMOTE LANGLE identifier RANGLE"""
     p[0] = "rma<%s>" % p[3]
 
   def p_associatedreceivertype(self, p):
-    """associatedreceivertype : PENDING_ASSOCIATED_RECEIVER LANGLE identifier \
-                                    RANGLE"""
+    """associatedreceivertype : PENDING_ASSOCIATED_RECEIVER LANGLE identifier RANGLE"""
     p[0] = "rca<%s>" % p[3]
 
   def p_handletype(self, p):
     """handletype : HANDLE
-                  | HANDLE LANGLE NAME RANGLE"""
+                  | HANDLE LANGLE name_wrapped RANGLE"""
     if len(p) == 2:
       p[0] = p[1]
     else:
@@ -342,14 +361,6 @@
     """associative_array : MAP LANGLE identifier COMMA typename RANGLE"""
     p[0] = p[5] + "{" + p[3] + "}"
 
-  def p_interfacerequest(self, p):
-    """interfacerequest : identifier AMP
-                        | ASSOCIATED identifier AMP"""
-    if len(p) == 3:
-      p[0] = p[1] + "&"
-    else:
-      p[0] = "asso<" + p[2] + "&>"
-
   def p_ordinal_1(self, p):
     """ordinal : """
     p[0] = None
@@ -366,15 +377,14 @@
     p[0] = ast.Ordinal(value, filename=self.filename, lineno=p.lineno(1))
 
   def p_enum_1(self, p):
-    """enum : attribute_section ENUM NAME LBRACE enum_value_list \
-                  RBRACE SEMI
-            | attribute_section ENUM NAME LBRACE nonempty_enum_value_list \
-                  COMMA RBRACE SEMI"""
+    """enum : attribute_section ENUM name_wrapped LBRACE enum_value_list RBRACE SEMI
+            | attribute_section ENUM name_wrapped LBRACE \
+                    nonempty_enum_value_list COMMA RBRACE SEMI"""
     p[0] = ast.Enum(
         p[3], p[1], p[5], filename=self.filename, lineno=p.lineno(2))
 
   def p_enum_2(self, p):
-    """enum : attribute_section ENUM NAME SEMI"""
+    """enum : attribute_section ENUM name_wrapped SEMI"""
     p[0] = ast.Enum(
         p[3], p[1], None, filename=self.filename, lineno=p.lineno(2))
 
@@ -396,9 +406,9 @@
     p[0].Append(p[3])
 
   def p_enum_value(self, p):
-    """enum_value : attribute_section NAME
-                  | attribute_section NAME EQUALS int
-                  | attribute_section NAME EQUALS identifier_wrapped"""
+    """enum_value : attribute_section name_wrapped
+                  | attribute_section name_wrapped EQUALS int
+                  | attribute_section name_wrapped EQUALS identifier_wrapped"""
     p[0] = ast.EnumValue(
         p[2],
         p[1],
@@ -407,7 +417,7 @@
         lineno=p.lineno(2))
 
   def p_const(self, p):
-    """const : attribute_section CONST typename NAME EQUALS constant SEMI"""
+    """const : attribute_section CONST typename name_wrapped EQUALS constant SEMI"""
     p[0] = ast.Const(p[4], p[1], p[3], p[6])
 
   def p_constant(self, p):
@@ -422,10 +432,16 @@
   # TODO(vtl): Make this produce a "wrapped" identifier (probably as an
   # |ast.Identifier|, to be added) and get rid of identifier_wrapped.
   def p_identifier(self, p):
-    """identifier : NAME
-                  | NAME DOT identifier"""
+    """identifier : name_wrapped
+                  | name_wrapped DOT identifier"""
     p[0] = ''.join(p[1:])
 
+  # Allow 'feature' to be a name literal not just a keyword.
+  def p_name_wrapped(self, p):
+    """name_wrapped : NAME
+                    | FEATURE"""
+    p[0] = p[1]
+
   def p_literal(self, p):
     """literal : int
                | float
@@ -458,6 +474,12 @@
       # TODO(vtl): Can we figure out what's missing?
       raise ParseError(self.filename, "Unexpected end of file")
 
+    if e.value == 'feature':
+      raise ParseError(self.filename,
+                       "`feature` is reserved for a future mojom keyword",
+                       lineno=e.lineno,
+                       snippet=self._GetSnippet(e.lineno))
+
     raise ParseError(
         self.filename,
         "Unexpected %r:" % e.value,
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom/parse/parser_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom/parse/parser_unittest.py
index 6d6b715..0a26307 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom/parse/parser_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom/parse/parser_unittest.py
@@ -1,17 +1,13 @@
-# Copyright 2014 The Chromium Authors. All rights reserved.
+# Copyright 2014 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
-import imp
-import os.path
-import sys
 import unittest
 
 from mojom.parse import ast
 from mojom.parse import lexer
 from mojom.parse import parser
 
-
 class ParserTest(unittest.TestCase):
   """Tests |parser.Parse()|."""
 
@@ -1086,7 +1082,7 @@
           handle<data_pipe_producer>? k;
           handle<message_pipe>? l;
           handle<shared_buffer>? m;
-          some_interface&? n;
+          pending_receiver<some_interface>? n;
           handle<platform>? o;
         };
         """
@@ -1110,7 +1106,7 @@
                 ast.StructField('l', None, None, 'handle<message_pipe>?', None),
                 ast.StructField('m', None, None, 'handle<shared_buffer>?',
                                 None),
-                ast.StructField('n', None, None, 'some_interface&?', None),
+                ast.StructField('n', None, None, 'rcv<some_interface>?', None),
                 ast.StructField('o', None, None, 'handle<platform>?', None)
             ]))
     ])
@@ -1138,16 +1134,6 @@
         r" *handle\?<data_pipe_consumer> a;$"):
       parser.Parse(source2, "my_file.mojom")
 
-    source3 = """\
-        struct MyStruct {
-          some_interface?& a;
-        };
-        """
-    with self.assertRaisesRegexp(
-        parser.ParseError, r"^my_file\.mojom:2: Error: Unexpected '&':\n"
-        r" *some_interface\?& a;$"):
-      parser.Parse(source3, "my_file.mojom")
-
   def testSimpleUnion(self):
     """Tests a simple .mojom source that just defines a union."""
     source = """\
@@ -1317,9 +1303,9 @@
     source1 = """\
         struct MyStruct {
           associated MyInterface a;
-          associated MyInterface& b;
+          pending_associated_receiver<MyInterface> b;
           associated MyInterface? c;
-          associated MyInterface&? d;
+          pending_associated_receiver<MyInterface>? d;
         };
         """
     expected1 = ast.Mojom(None, ast.ImportList(), [
@@ -1327,16 +1313,16 @@
             'MyStruct', None,
             ast.StructBody([
                 ast.StructField('a', None, None, 'asso<MyInterface>', None),
-                ast.StructField('b', None, None, 'asso<MyInterface&>', None),
+                ast.StructField('b', None, None, 'rca<MyInterface>', None),
                 ast.StructField('c', None, None, 'asso<MyInterface>?', None),
-                ast.StructField('d', None, None, 'asso<MyInterface&>?', None)
+                ast.StructField('d', None, None, 'rca<MyInterface>?', None)
             ]))
     ])
     self.assertEquals(parser.Parse(source1, "my_file.mojom"), expected1)
 
     source2 = """\
         interface MyInterface {
-          MyMethod(associated A a) =>(associated B& b);
+          MyMethod(associated A a) =>(pending_associated_receiver<B> b);
         };"""
     expected2 = ast.Mojom(None, ast.ImportList(), [
         ast.Interface(
@@ -1344,10 +1330,10 @@
             ast.InterfaceBody(
                 ast.Method(
                     'MyMethod', None, None,
-                    ast.ParameterList(
-                        ast.Parameter('a', None, None, 'asso<A>')),
-                    ast.ParameterList(
-                        ast.Parameter('b', None, None, 'asso<B&>')))))
+                    ast.ParameterList(ast.Parameter('a', None, None,
+                                                    'asso<A>')),
+                    ast.ParameterList(ast.Parameter('b', None, None,
+                                                    'rca<B>')))))
     ])
     self.assertEquals(parser.Parse(source2, "my_file.mojom"), expected2)
 
@@ -1385,6 +1371,5 @@
         r" *associated\? MyInterface& a;$"):
       parser.Parse(source3, "my_file.mojom")
 
-
 if __name__ == "__main__":
   unittest.main()
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom_parser.py b/utils/ipc/mojo/public/tools/mojom/mojom_parser.py
index eb90c82..9693090 100755
--- a/utils/ipc/mojo/public/tools/mojom/mojom_parser.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom_parser.py
@@ -1,5 +1,5 @@
-#!/usr/bin/env python
-# Copyright 2020 The Chromium Authors. All rights reserved.
+#!/usr/bin/env python3
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 """Parses mojom IDL files.
@@ -11,6 +11,7 @@
 """
 
 import argparse
+import builtins
 import codecs
 import errno
 import json
@@ -19,6 +20,7 @@
 import os
 import os.path
 import sys
+import traceback
 from collections import defaultdict
 
 from mojom.generate import module
@@ -28,16 +30,12 @@
 
 
 # Disable this for easier debugging.
-# In Python 2, subprocesses just hang when exceptions are thrown :(.
-_ENABLE_MULTIPROCESSING = sys.version_info[0] > 2
+_ENABLE_MULTIPROCESSING = True
 
-if sys.version_info < (3, 4):
-  _MULTIPROCESSING_USES_FORK = sys.platform.startswith('linux')
-else:
-  # https://docs.python.org/3/library/multiprocessing.html#:~:text=bpo-33725
-  if __name__ == '__main__' and sys.platform == 'darwin':
-    multiprocessing.set_start_method('fork')
-  _MULTIPROCESSING_USES_FORK = multiprocessing.get_start_method() == 'fork'
+# https://docs.python.org/3/library/multiprocessing.html#:~:text=bpo-33725
+if __name__ == '__main__' and sys.platform == 'darwin':
+  multiprocessing.set_start_method('fork')
+_MULTIPROCESSING_USES_FORK = multiprocessing.get_start_method() == 'fork'
 
 
 def _ResolveRelativeImportPath(path, roots):
@@ -63,7 +61,7 @@
   raise ValueError('"%s" does not exist in any of %s' % (path, roots))
 
 
-def _RebaseAbsolutePath(path, roots):
+def RebaseAbsolutePath(path, roots):
   """Rewrites an absolute file path as relative to an absolute directory path in
   roots.
 
@@ -139,7 +137,7 @@
     # Already done.
     return
 
-  for dep_abspath, dep_path in dependencies[mojom_abspath]:
+  for dep_abspath, dep_path in sorted(dependencies[mojom_abspath]):
     if dep_abspath not in loaded_modules:
       _EnsureInputLoaded(dep_abspath, dep_path, abs_paths, asts, dependencies,
                          loaded_modules, module_metadata)
@@ -159,11 +157,19 @@
 
   def collect(metadata_filename):
     processed_deps.add(metadata_filename)
+
+    # Paths in the metadata file are relative to the metadata file's dir.
+    metadata_dir = os.path.abspath(os.path.dirname(metadata_filename))
+
+    def to_abs(s):
+      return os.path.normpath(os.path.join(metadata_dir, s))
+
     with open(metadata_filename) as f:
       metadata = json.load(f)
       allowed_imports.update(
-          map(os.path.normcase, map(os.path.normpath, metadata['sources'])))
+          [os.path.normcase(to_abs(s)) for s in metadata['sources']])
       for dep_metadata in metadata['deps']:
+        dep_metadata = to_abs(dep_metadata)
         if dep_metadata not in processed_deps:
           collect(dep_metadata)
 
@@ -172,8 +178,7 @@
 
 
 # multiprocessing helper.
-def _ParseAstHelper(args):
-  mojom_abspath, enabled_features = args
+def _ParseAstHelper(mojom_abspath, enabled_features):
   with codecs.open(mojom_abspath, encoding='utf-8') as f:
     ast = parser.Parse(f.read(), mojom_abspath)
     conditional_features.RemoveDisabledDefinitions(ast, enabled_features)
@@ -181,8 +186,7 @@
 
 
 # multiprocessing helper.
-def _SerializeHelper(args):
-  mojom_abspath, mojom_path = args
+def _SerializeHelper(mojom_abspath, mojom_path):
   module_path = os.path.join(_SerializeHelper.output_root_path,
                              _GetModuleFilename(mojom_path))
   module_dir = os.path.dirname(module_path)
@@ -199,12 +203,33 @@
     _SerializeHelper.loaded_modules[mojom_abspath].Dump(f)
 
 
-def _Shard(target_func, args, processes=None):
-  args = list(args)
+class _ExceptionWrapper:
+  def __init__(self):
+    # Do not capture exception object to ensure pickling works.
+    self.formatted_trace = traceback.format_exc()
+
+
+class _FuncWrapper:
+  """Marshals exceptions and spreads args."""
+
+  def __init__(self, func):
+    self._func = func
+
+  def __call__(self, args):
+    # multiprocessing does not gracefully handle excptions.
+    # https://crbug.com/1219044
+    try:
+      return self._func(*args)
+    except:  # pylint: disable=bare-except
+      return _ExceptionWrapper()
+
+
+def _Shard(target_func, arg_list, processes=None):
+  arg_list = list(arg_list)
   if processes is None:
     processes = multiprocessing.cpu_count()
   # Seems optimal to have each process perform at least 2 tasks.
-  processes = min(processes, len(args) // 2)
+  processes = min(processes, len(arg_list) // 2)
 
   if sys.platform == 'win32':
     # TODO(crbug.com/1190269) - we can't use more than 56
@@ -213,13 +238,17 @@
 
   # Don't spin up processes unless there is enough work to merit doing so.
   if not _ENABLE_MULTIPROCESSING or processes < 2:
-    for result in map(target_func, args):
-      yield result
+    for arg_tuple in arg_list:
+      yield target_func(*arg_tuple)
     return
 
   pool = multiprocessing.Pool(processes=processes)
   try:
-    for result in pool.imap_unordered(target_func, args):
+    wrapped_func = _FuncWrapper(target_func)
+    for result in pool.imap_unordered(wrapped_func, arg_list):
+      if isinstance(result, _ExceptionWrapper):
+        sys.stderr.write(result.formatted_trace)
+        sys.exit(1)
       yield result
   finally:
     pool.close()
@@ -230,6 +259,7 @@
 def _ParseMojoms(mojom_files,
                  input_root_paths,
                  output_root_path,
+                 module_root_paths,
                  enabled_features,
                  module_metadata,
                  allowed_imports=None):
@@ -245,8 +275,10 @@
         are based on the mojom's relative path, rebased onto this path.
         Additionally, the script expects this root to contain already-generated
         modules for any transitive dependencies not listed in mojom_files.
+    module_root_paths: A list of absolute filesystem paths which contain
+        already-generated modules for any non-transitive dependencies.
     enabled_features: A list of enabled feature names, controlling which AST
-        nodes are filtered by [EnableIf] attributes.
+        nodes are filtered by [EnableIf] or [EnableIfNot] attributes.
     module_metadata: A list of 2-tuples representing metadata key-value pairs to
         attach to each compiled module output.
 
@@ -262,7 +294,7 @@
   loaded_modules = {}
   input_dependencies = defaultdict(set)
   mojom_files_to_parse = dict((os.path.normcase(abs_path),
-                               _RebaseAbsolutePath(abs_path, input_root_paths))
+                               RebaseAbsolutePath(abs_path, input_root_paths))
                               for abs_path in mojom_files)
   abs_paths = dict(
       (path, abs_path) for abs_path, path in mojom_files_to_parse.items())
@@ -274,7 +306,7 @@
     loaded_mojom_asts[mojom_abspath] = ast
 
   logging.info('Processing dependencies')
-  for mojom_abspath, ast in loaded_mojom_asts.items():
+  for mojom_abspath, ast in sorted(loaded_mojom_asts.items()):
     invalid_imports = []
     for imp in ast.import_list:
       import_abspath = _ResolveRelativeImportPath(imp.import_filename,
@@ -295,8 +327,8 @@
         # be parsed and have a module file sitting in a corresponding output
         # location.
         module_path = _GetModuleFilename(imp.import_filename)
-        module_abspath = _ResolveRelativeImportPath(module_path,
-                                                    [output_root_path])
+        module_abspath = _ResolveRelativeImportPath(
+            module_path, module_root_paths + [output_root_path])
         with open(module_abspath, 'rb') as module_file:
           loaded_modules[import_abspath] = module.Module.Load(module_file)
 
@@ -371,6 +403,15 @@
       'ROOT is also searched for existing modules of any transitive imports '
       'which were not included in the set of inputs.')
   arg_parser.add_argument(
+      '--module-root',
+      default=[],
+      action='append',
+      metavar='ROOT',
+      dest='module_root_paths',
+      help='Adds ROOT to the set of root paths to search for existing modules '
+      'of non-transitive imports. Provided root paths are always searched in '
+      'order from longest absolute path to shortest.')
+  arg_parser.add_argument(
       '--mojoms',
       nargs='+',
       dest='mojom_files',
@@ -396,9 +437,9 @@
       help='Enables a named feature when parsing the given mojoms. Features '
       'are identified by arbitrary string values. Specifying this flag with a '
       'given FEATURE name will cause the parser to process any syntax elements '
-      'tagged with an [EnableIf=FEATURE] attribute. If this flag is not '
-      'provided for a given FEATURE, such tagged elements are discarded by the '
-      'parser and will not be present in the compiled output.')
+      'tagged with an [EnableIf=FEATURE] or [EnableIfNot] attribute. If this '
+      'flag is not provided for a given FEATURE, such tagged elements are '
+      'discarded by the parser and will not be present in the compiled output.')
   arg_parser.add_argument(
       '--check-imports',
       dest='build_metadata_filename',
@@ -436,6 +477,7 @@
   mojom_files = list(map(os.path.abspath, args.mojom_files))
   input_roots = list(map(os.path.abspath, args.input_root_paths))
   output_root = os.path.abspath(args.output_root_path)
+  module_roots = list(map(os.path.abspath, args.module_root_paths))
 
   if args.build_metadata_filename:
     allowed_imports = _CollectAllowedImportsFromBuildMetadata(
@@ -445,13 +487,16 @@
 
   module_metadata = list(
       map(lambda kvp: tuple(kvp.split('=')), args.module_metadata))
-  _ParseMojoms(mojom_files, input_roots, output_root, args.enabled_features,
-               module_metadata, allowed_imports)
+  _ParseMojoms(mojom_files, input_roots, output_root, module_roots,
+               args.enabled_features, module_metadata, allowed_imports)
   logging.info('Finished')
-  # Exit without running GC, which can save multiple seconds due the large
-  # number of object created.
-  os._exit(0)
 
 
 if __name__ == '__main__':
   Run(sys.argv[1:])
+  # Exit without running GC, which can save multiple seconds due to the large
+  # number of object created. But flush is necessary as os._exit doesn't do
+  # that.
+  sys.stdout.flush()
+  sys.stderr.flush()
+  os._exit(0)
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom_parser_test_case.py b/utils/ipc/mojo/public/tools/mojom/mojom_parser_test_case.py
index e213fbf..f0ee696 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom_parser_test_case.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom_parser_test_case.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -20,7 +20,7 @@
   resolution, and module serialization and deserialization."""
 
   def __init__(self, method_name):
-    super(MojomParserTestCase, self).__init__(method_name)
+    super().__init__(method_name)
     self._temp_dir = None
 
   def setUp(self):
@@ -67,7 +67,7 @@
     self.ParseMojoms([filename])
     m = self.LoadModule(filename)
     definitions = {}
-    for kinds in (m.enums, m.structs, m.unions, m.interfaces):
+    for kinds in (m.enums, m.structs, m.unions, m.interfaces, m.features):
       for kind in kinds:
         definitions[kind.mojom_name] = kind
     return definitions
diff --git a/utils/ipc/mojo/public/tools/mojom/mojom_parser_unittest.py b/utils/ipc/mojo/public/tools/mojom/mojom_parser_unittest.py
index a93f34b..353a2b6 100644
--- a/utils/ipc/mojo/public/tools/mojom/mojom_parser_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/mojom_parser_unittest.py
@@ -1,7 +1,9 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
+import json
+
 from mojom_parser_test_case import MojomParserTestCase
 
 
@@ -119,15 +121,22 @@
     c = 'c.mojom'
     c_metadata = 'out/c.build_metadata'
     self.WriteFile(a_metadata,
-                   '{"sources": ["%s"], "deps": []}\n' % self.GetPath(a))
+                   json.dumps({
+                       "sources": [self.GetPath(a)],
+                       "deps": []
+                   }))
     self.WriteFile(
         b_metadata,
-        '{"sources": ["%s"], "deps": ["%s"]}\n' % (self.GetPath(b),
-                                                   self.GetPath(a_metadata)))
+        json.dumps({
+            "sources": [self.GetPath(b)],
+            "deps": [self.GetPath(a_metadata)]
+        }))
     self.WriteFile(
         c_metadata,
-        '{"sources": ["%s"], "deps": ["%s"]}\n' % (self.GetPath(c),
-                                                   self.GetPath(b_metadata)))
+        json.dumps({
+            "sources": [self.GetPath(c)],
+            "deps": [self.GetPath(b_metadata)]
+        }))
     self.WriteFile(a, """\
         module a;
         struct Bar {};""")
@@ -154,9 +163,15 @@
     b = 'b.mojom'
     b_metadata = 'out/b.build_metadata'
     self.WriteFile(a_metadata,
-                   '{"sources": ["%s"], "deps": []}\n' % self.GetPath(a))
+                   json.dumps({
+                       "sources": [self.GetPath(a)],
+                       "deps": []
+                   }))
     self.WriteFile(b_metadata,
-                   '{"sources": ["%s"], "deps": []}\n' % self.GetPath(b))
+                   json.dumps({
+                       "sources": [self.GetPath(b)],
+                       "deps": []
+                   }))
     self.WriteFile(a, """\
         module a;
         struct Bar {};""")
diff --git a/utils/ipc/mojo/public/tools/mojom/stable_attribute_unittest.py b/utils/ipc/mojo/public/tools/mojom/stable_attribute_unittest.py
index d45ec58..d10d69c 100644
--- a/utils/ipc/mojo/public/tools/mojom/stable_attribute_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/stable_attribute_unittest.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
diff --git a/utils/ipc/mojo/public/tools/mojom/union_unittest.py b/utils/ipc/mojo/public/tools/mojom/union_unittest.py
new file mode 100644
index 0000000..6b2525e
--- /dev/null
+++ b/utils/ipc/mojo/public/tools/mojom/union_unittest.py
@@ -0,0 +1,44 @@
+# Copyright 2022 The Chromium Authors
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+from mojom_parser_test_case import MojomParserTestCase
+
+
+class UnionTest(MojomParserTestCase):
+  """Tests union parsing behavior."""
+
+  def testExtensibleMustHaveDefault(self):
+    """Verifies that extensible unions must have a default field."""
+    mojom = 'foo.mojom'
+    self.WriteFile(mojom, 'module foo; [Extensible] union U { bool x; };')
+    with self.assertRaisesRegexp(Exception, 'must specify a \[Default\]'):
+      self.ParseMojoms([mojom])
+
+  def testExtensibleSingleDefault(self):
+    """Verifies that extensible unions must not have multiple default fields."""
+    mojom = 'foo.mojom'
+    self.WriteFile(
+        mojom, """\
+               module foo;
+               [Extensible] union U {
+                 [Default] bool x;
+                 [Default] bool y;
+               };
+               """)
+    with self.assertRaisesRegexp(Exception, 'Multiple \[Default\] fields'):
+      self.ParseMojoms([mojom])
+
+  def testExtensibleDefaultTypeValid(self):
+    """Verifies that an extensible union's default field must be nullable or
+    integral type."""
+    mojom = 'foo.mojom'
+    self.WriteFile(
+        mojom, """\
+               module foo;
+               [Extensible] union U {
+                 [Default] handle<message_pipe> p;
+               };
+               """)
+    with self.assertRaisesRegexp(Exception, 'must be nullable or integral'):
+      self.ParseMojoms([mojom])
diff --git a/utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py b/utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py
index 65db4dc..45e45ec 100644
--- a/utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py
+++ b/utils/ipc/mojo/public/tools/mojom/version_compatibility_unittest.py
@@ -1,4 +1,4 @@
-# Copyright 2020 The Chromium Authors. All rights reserved.
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -23,9 +23,12 @@
 
     checker = module.BackwardCompatibilityChecker()
     compatibility_map = {}
-    for name in old.keys():
-      compatibility_map[name] = checker.IsBackwardCompatible(
-          new[name], old[name])
+    for name in old:
+      try:
+        compatibility_map[name] = checker.IsBackwardCompatible(
+            new[name], old[name])
+      except Exception:
+        compatibility_map[name] = False
     return compatibility_map
 
   def assertBackwardCompatible(self, old_mojom, new_mojom):
@@ -60,40 +63,48 @@
     """Adding a value to an existing version is not allowed, even if the old
     enum was marked [Extensible]. Note that it is irrelevant whether or not the
     new enum is marked [Extensible]."""
-    self.assertNotBackwardCompatible('[Extensible] enum E { kFoo, kBar };',
-                                     'enum E { kFoo, kBar, kBaz };')
     self.assertNotBackwardCompatible(
-        '[Extensible] enum E { kFoo, kBar };',
-        '[Extensible] enum E { kFoo, kBar, kBaz };')
+        '[Extensible] enum E { [Default] kFoo, kBar };',
+        'enum E { kFoo, kBar, kBaz };')
     self.assertNotBackwardCompatible(
-        '[Extensible] enum E { kFoo, [MinVersion=1] kBar };',
+        '[Extensible] enum E { [Default] kFoo, kBar };',
+        '[Extensible] enum E { [Default] kFoo, kBar, kBaz };')
+    self.assertNotBackwardCompatible(
+        '[Extensible] enum E { [Default] kFoo, [MinVersion=1] kBar };',
         'enum E { kFoo, [MinVersion=1] kBar, [MinVersion=1] kBaz };')
 
   def testEnumValueRemoval(self):
     """Removal of an enum value is never valid even for [Extensible] enums."""
     self.assertNotBackwardCompatible('enum E { kFoo, kBar };',
                                      'enum E { kFoo };')
-    self.assertNotBackwardCompatible('[Extensible] enum E { kFoo, kBar };',
-                                     '[Extensible] enum E { kFoo };')
     self.assertNotBackwardCompatible(
-        '[Extensible] enum E { kA, [MinVersion=1] kB };',
-        '[Extensible] enum E { kA, };')
+        '[Extensible] enum E { [Default] kFoo, kBar };',
+        '[Extensible] enum E { [Default] kFoo };')
     self.assertNotBackwardCompatible(
-        '[Extensible] enum E { kA, [MinVersion=1] kB, [MinVersion=1] kZ };',
-        '[Extensible] enum E { kA, [MinVersion=1] kB };')
+        '[Extensible] enum E { [Default] kA, [MinVersion=1] kB };',
+        '[Extensible] enum E { [Default] kA, };')
+    self.assertNotBackwardCompatible(
+        """[Extensible] enum E {
+          [Default] kA,
+          [MinVersion=1] kB,
+          [MinVersion=1] kZ };""",
+        '[Extensible] enum E { [Default] kA, [MinVersion=1] kB };')
 
   def testNewExtensibleEnumValueWithMinVersion(self):
     """Adding a new and properly [MinVersion]'d value to an [Extensible] enum
     is a backward-compatible change. Note that it is irrelevant whether or not
     the new enum is marked [Extensible]."""
-    self.assertBackwardCompatible('[Extensible] enum E { kA, kB };',
+    self.assertBackwardCompatible('[Extensible] enum E { [Default] kA, kB };',
                                   'enum E { kA, kB, [MinVersion=1] kC };')
     self.assertBackwardCompatible(
-        '[Extensible] enum E { kA, kB };',
-        '[Extensible] enum E { kA, kB, [MinVersion=1] kC };')
+        '[Extensible] enum E { [Default] kA, kB };',
+        '[Extensible] enum E { [Default] kA, kB, [MinVersion=1] kC };')
     self.assertBackwardCompatible(
-        '[Extensible] enum E { kA, [MinVersion=1] kB };',
-        '[Extensible] enum E { kA, [MinVersion=1] kB, [MinVersion=2] kC };')
+        '[Extensible] enum E { [Default] kA, [MinVersion=1] kB };',
+        """[Extensible] enum E {
+          [Default] kA,
+          [MinVersion=1] kB,
+          [MinVersion=2] kC };""")
 
   def testRenameEnumValue(self):
     """Renaming an enum value does not affect backward-compatibility. Only
@@ -161,14 +172,17 @@
         'struct S {}; struct T { S s; };',
         'struct S { [MinVersion=1] int32 x; }; struct T { S s; };')
     self.assertBackwardCompatible(
-        '[Extensible] enum E { kA }; struct S { E e; };',
-        '[Extensible] enum E { kA, [MinVersion=1] kB }; struct S { E e; };')
+        '[Extensible] enum E { [Default] kA }; struct S { E e; };',
+        """[Extensible] enum E {
+          [Default] kA,
+          [MinVersion=1] kB };
+          struct S { E e; };""")
     self.assertNotBackwardCompatible(
         'struct S {}; struct T { S s; };',
         'struct S { int32 x; }; struct T { S s; };')
     self.assertNotBackwardCompatible(
-        '[Extensible] enum E { kA }; struct S { E e; };',
-        '[Extensible] enum E { kA, kB }; struct S { E e; };')
+        '[Extensible] enum E { [Default] kA }; struct S { E e; };',
+        '[Extensible] enum E { [Default] kA, kB }; struct S { E e; };')
 
   def testNewStructFieldWithInvalidMinVersion(self):
     """Adding a new field using an existing MinVersion breaks backward-
@@ -305,14 +319,17 @@
         'struct S {}; union U { S s; };',
         'struct S { [MinVersion=1] int32 x; }; union U { S s; };')
     self.assertBackwardCompatible(
-        '[Extensible] enum E { kA }; union U { E e; };',
-        '[Extensible] enum E { kA, [MinVersion=1] kB }; union U { E e; };')
+        '[Extensible] enum E { [Default] kA }; union U { E e; };',
+        """[Extensible] enum E {
+          [Default] kA,
+          [MinVersion=1] kB };
+          union U { E e; };""")
     self.assertNotBackwardCompatible(
         'struct S {}; union U { S s; };',
         'struct S { int32 x; }; union U { S s; };')
     self.assertNotBackwardCompatible(
-        '[Extensible] enum E { kA }; union U { E e; };',
-        '[Extensible] enum E { kA, kB }; union U { E e; };')
+        '[Extensible] enum E { [Default] kA }; union U { E e; };',
+        '[Extensible] enum E { [Default] kA, kB }; union U { E e; };')
 
   def testNewUnionFieldWithInvalidMinVersion(self):
     """Adding a new field using an existing MinVersion breaks backward-
diff --git a/utils/ipc/mojo/public/tools/run_all_python_unittests.py b/utils/ipc/mojo/public/tools/run_all_python_unittests.py
index b201095..98bce18 100755
--- a/utils/ipc/mojo/public/tools/run_all_python_unittests.py
+++ b/utils/ipc/mojo/public/tools/run_all_python_unittests.py
@@ -1,5 +1,5 @@
-#!/usr/bin/env python
-# Copyright 2020 The Chromium Authors. All rights reserved.
+#!/usr/bin/env python3
+# Copyright 2020 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
@@ -8,11 +8,13 @@
 
 _TOOLS_DIR = os.path.dirname(__file__)
 _MOJOM_DIR = os.path.join(_TOOLS_DIR, 'mojom')
+_BINDINGS_DIR = os.path.join(_TOOLS_DIR, 'bindings')
 _SRC_DIR = os.path.join(_TOOLS_DIR, os.path.pardir, os.path.pardir,
                         os.path.pardir)
 
 # Ensure that the mojom library is discoverable.
 sys.path.append(_MOJOM_DIR)
+sys.path.append(_BINDINGS_DIR)
 
 # Help Python find typ in //third_party/catapult/third_party/typ/
 sys.path.append(
@@ -21,7 +23,7 @@
 
 
 def Main():
-  return typ.main(top_level_dir=_MOJOM_DIR)
+  return typ.main(top_level_dirs=[_MOJOM_DIR, _BINDINGS_DIR])
 
 
 if __name__ == '__main__':
diff --git a/utils/ipc/parser.py b/utils/ipc/parser.py
index 231a326..cb5608b 100755
--- a/utils/ipc/parser.py
+++ b/utils/ipc/parser.py
@@ -4,7 +4,7 @@
 #
 # Author: Paul Elder <paul.elder@ideasonboard.com>
 #
-# parser.py - Run mojo parser with python3
+# Run mojo parser with python3
 
 import os
 import sys
diff --git a/utils/ipc/tools/README b/utils/ipc/tools/README
index d5c24fc..961cabd 100644
--- a/utils/ipc/tools/README
+++ b/utils/ipc/tools/README
@@ -1,4 +1,4 @@
 # SPDX-License-Identifier: CC0-1.0
 
-Files in this directory are imported from 9c138d992bfc of Chromium. Do not
+Files in this directory are imported from 9be4263648d7 of Chromium. Do not
 modify them manually.
diff --git a/utils/ipc/tools/diagnosis/crbug_1001171.py b/utils/ipc/tools/diagnosis/crbug_1001171.py
index 478fb8c..40900d1 100644
--- a/utils/ipc/tools/diagnosis/crbug_1001171.py
+++ b/utils/ipc/tools/diagnosis/crbug_1001171.py
@@ -1,4 +1,4 @@
-# Copyright 2019 The Chromium Authors. All rights reserved.
+# Copyright 2019 The Chromium Authors
 # Use of this source code is governed by a BSD-style license that can be
 # found in the LICENSE file.
 
diff --git a/utils/ipu3/ipu3-capture.sh b/utils/ipu3/ipu3-capture.sh
index 9294d02..004a92b 100755
--- a/utils/ipu3/ipu3-capture.sh
+++ b/utils/ipu3/ipu3-capture.sh
@@ -4,7 +4,7 @@
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# ipu3-capture.sh - Capture raw frames from cameras based on the Intel IPU3
+# Capture raw frames from cameras based on the Intel IPU3
 #
 # The scripts makes use of the following tools, which are expected to be
 # executable from the system-wide path or from the local directory:
diff --git a/utils/ipu3/ipu3-pack.c b/utils/ipu3/ipu3-pack.c
index decbfc6..23d2db8 100644
--- a/utils/ipu3/ipu3-pack.c
+++ b/utils/ipu3/ipu3-pack.c
@@ -8,6 +8,7 @@
 
 #include <errno.h>
 #include <fcntl.h>
+#include <libgen.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <string.h>
@@ -15,9 +16,8 @@
 #include <sys/types.h>
 #include <unistd.h>
 
-static void usage(const char *argv0)
+static void usage(char *argv0)
 {
-
 	printf("Usage: %s input-file output-file\n", basename(argv0));
 	printf("Convert unpacked RAW10 Bayer data to the IPU3 packed Bayer formats\n");
 	printf("If the output-file '-', output data will be written to standard output\n");
diff --git a/utils/ipu3/ipu3-process.sh b/utils/ipu3/ipu3-process.sh
index bb4abbe..25bc849 100755
--- a/utils/ipu3/ipu3-process.sh
+++ b/utils/ipu3/ipu3-process.sh
@@ -4,7 +4,7 @@
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# ipu3-process.sh - Process raw frames with the Intel IPU3
+# Process raw frames with the Intel IPU3
 #
 # The scripts makes use of the following tools, which are expected to be
 # found in $PATH:
diff --git a/utils/ipu3/ipu3-unpack.c b/utils/ipu3/ipu3-unpack.c
index 9d2c120..6ee8c45 100644
--- a/utils/ipu3/ipu3-unpack.c
+++ b/utils/ipu3/ipu3-unpack.c
@@ -8,6 +8,7 @@
 
 #include <errno.h>
 #include <fcntl.h>
+#include <libgen.h>
 #include <stdint.h>
 #include <stdio.h>
 #include <string.h>
@@ -15,7 +16,7 @@
 #include <sys/types.h>
 #include <unistd.h>
 
-static void usage(const char *argv0)
+static void usage(char *argv0)
 {
 	printf("Usage: %s input-file output-file\n", basename(argv0));
 	printf("Unpack the IPU3 raw Bayer format to 16-bit Bayer\n");
@@ -78,7 +79,7 @@
 		}
 
 		ret = write(out_fd, out_data, 50);
-		if (ret < -1) {
+		if (ret == -1) {
 			fprintf(stderr, "Failed to write output data: %s\n",
 				strerror(errno));
 			goto done;
diff --git a/utils/raspberrypi/ctt/alsc_only.py b/utils/raspberrypi/ctt/alsc_only.py
index 7cd0ac0..a521c4a 100755
--- a/utils/raspberrypi/ctt/alsc_only.py
+++ b/utils/raspberrypi/ctt/alsc_only.py
@@ -2,12 +2,14 @@
 #
 # SPDX-License-Identifier: BSD-2-Clause
 #
-# Copyright (C) 2022, Raspberry Pi (Trading) Limited
+# Copyright (C) 2022, Raspberry Pi Ltd
 #
-# alsc_only.py - alsc tuning tool
+# alsc tuning tool
+
+import sys
 
 from ctt import *
-
+from ctt_tools import parse_input
 
 if __name__ == '__main__':
     """
@@ -15,13 +17,14 @@
     """
     if len(sys.argv) == 1:
         print("""
-    Pisp Camera Tuning Tool version 1.0
+    PiSP Lens Shading Camera Tuning Tool version 1.0
 
     Required Arguments:
     '-i' : Calibration image directory.
     '-o' : Name of output json file.
 
     Optional Arguments:
+    '-t' : Target platform - 'pisp' or 'vc4'. Default 'vc4'
     '-c' : Config file for the CTT. If not passed, default parameters used.
     '-l' : Name of output log file. If not passed, 'ctt_log.txt' used.
               """)
@@ -30,5 +33,10 @@
         """
         parse input arguments
         """
-        json_output, directory, config, log_output = parse_input()
-        run_ctt(json_output, directory, config, log_output, alsc_only=True)
+        json_output, directory, config, log_output, target = parse_input()
+        if target == 'pisp':
+            from ctt_pisp import json_template, grid_size
+        elif target == 'vc4':
+            from ctt_vc4 import json_template, grid_size
+
+        run_ctt(json_output, directory, config, log_output, json_template, grid_size, target, alsc_only=True)
diff --git a/utils/raspberrypi/ctt/cac_only.py b/utils/raspberrypi/ctt/cac_only.py
new file mode 100644
index 0000000..1c0a819
--- /dev/null
+++ b/utils/raspberrypi/ctt/cac_only.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2023, Raspberry Pi (Trading) Ltd.
+#
+# cac_only.py - cac tuning tool
+
+
+# This file allows you to tune only the chromatic aberration correction
+# Specify any number of files in the command line args, and it shall iterate through
+# and generate an averaged cac table from all the input images, which you can then
+# input into your tuning file.
+
+# Takes .dng files produced by the camera modules of the dots grid and calculates the chromatic abberation of each dot.
+# Then takes each dot, and works out where it was in the image, and uses that to output a tables of the shifts
+# across the whole image.
+
+from PIL import Image
+import numpy as np
+import rawpy
+import sys
+import getopt
+
+from ctt_cac import *
+
+
+def cac(filelist, output_filepath, plot_results=False):
+    np.set_printoptions(precision=3)
+    np.set_printoptions(suppress=True)
+
+    # Create arrays to hold all the dots data and their colour offsets
+    red_shift = []  # Format is: [[Dot Center X, Dot Center Y, x shift, y shift]]
+    blue_shift = []
+    # Iterate through the files
+    # Multiple files is reccomended to average out the lens aberration through rotations
+    for file in filelist:
+        print("\n Processing file " + str(file))
+        # Read the raw RGB values from the .dng file
+        with rawpy.imread(file) as raw:
+            rgb = raw.postprocess()
+            sizes = (raw.sizes)
+
+        image_size = [sizes[2], sizes[3]]  # Image size, X, Y
+        # Create a colour copy of the RGB values to use later in the calibration
+        imout = Image.new(mode="RGB", size=image_size)
+        rgb_image = np.array(imout)
+        # The rgb values need reshaping from a 1d array to a 3d array to be worked with easily
+        rgb.reshape((image_size[0], image_size[1], 3))
+        rgb_image = rgb
+
+        # Pass the RGB image through to the dots locating program
+        # Returns an array of the dots (colour rectangles around the dots), and an array of their locations
+        print("Finding dots")
+        dots, dots_locations = find_dots_locations(rgb_image)
+
+        # Now, analyse each dot. Work out the centroid of each colour channel, and use that to work out
+        # by how far the chromatic aberration has shifted each channel
+        print('Dots found: ' + str(len(dots)))
+
+        for dot, dot_location in zip(dots, dots_locations):
+            if len(dot) > 0:
+                if (dot_location[0] > 0) and (dot_location[1] > 0):
+                    ret = analyse_dot(dot, dot_location)
+                    red_shift.append(ret[0])
+                    blue_shift.append(ret[1])
+
+    # Take our arrays of red shifts and locations, push them through to be interpolated into a 9x9 matrix
+    # for the CAC block to handle and then store these as a .json file to be added to the camera
+    # tuning file
+    print("\nCreating output grid")
+    rx, ry, bx, by = shifts_to_yaml(red_shift, blue_shift, image_size)
+
+    print("CAC correction complete!")
+
+    # The json format that we then paste into the tuning file (manually)
+    sample = '''
+    {
+        "rpi.cac" :
+        {
+            "strength": 1.0,
+            "lut_rx" : [
+                   rx_vals
+            ],
+            "lut_ry" : [
+                   ry_vals
+            ],
+            "lut_bx" : [
+                   bx_vals
+            ],
+            "lut_by" : [
+                   by_vals
+            ]
+        }
+    }
+    '''
+
+    # Below, may look incorrect, however, the PiSP (standard) dimensions are flipped in comparison to
+    # PIL image coordinate directions, hence why xr -> yr. Also, the shifts calculated are colour shifts,
+    # and the PiSP block asks for the values it should shift (hence the * -1, to convert from colour shift to a pixel shift)
+    sample = sample.replace("rx_vals", pprint_array(ry * -1))
+    sample = sample.replace("ry_vals", pprint_array(rx * -1))
+    sample = sample.replace("bx_vals", pprint_array(by * -1))
+    sample = sample.replace("by_vals", pprint_array(bx * -1))
+    print("Successfully converted to JSON")
+    f = open(str(output_filepath), "w+")
+    f.write(sample)
+    f.close()
+    print("Successfully written to json file")
+    '''
+    If you wish to see a plot of the colour channel shifts, add the -p or --plots option
+    Can be a quick way of validating if the data/dots you've got are good, or if you need to
+    change some parameters/take some better images
+    '''
+    if plot_results:
+        plot_shifts(red_shift, blue_shift)
+
+
+if __name__ == "__main__":
+    argv = sys.argv
+    # Detect the input and output file paths
+    arg_output = "output.json"
+    arg_help = "{0} -i <input> -o <output> -p <plot results>".format(argv[0])
+    opts, args = getopt.getopt(argv[1:], "hi:o:p", ["help", "input=", "output=", "plot"])
+
+    output_location = 0
+    input_location = 0
+    filelist = []
+    plot_results = False
+    for i in range(len(argv)):
+        if ("-h") in argv[i]:
+            print(arg_help)  # print the help message
+            sys.exit(2)
+        if "-o" in argv[i]:
+            output_location = i
+        if ".dng" in argv[i]:
+            filelist.append(argv[i])
+        if "-p" in argv[i]:
+            plot_results = True
+
+    arg_output = argv[output_location + 1]
+    cac(filelist, arg_output, plot_results)
diff --git a/utils/raspberrypi/ctt/colors.py b/utils/raspberrypi/ctt/colors.py
index 1ab986d..cb4d236 100644
--- a/utils/raspberrypi/ctt/colors.py
+++ b/utils/raspberrypi/ctt/colors.py
@@ -1,4 +1,4 @@
-# colors.py - Program to convert from RGB to LAB color space
+# Program to convert from RGB to LAB color space
 def RGB_to_LAB(RGB):  # where RGB is a 1x3 array.   e.g RGB = [100, 255, 230]
     num = 0
     XYZ = [0, 0, 0]
diff --git a/utils/raspberrypi/ctt/convert_tuning.py b/utils/raspberrypi/ctt/convert_tuning.py
index f4504d4..83cf69d 100755
--- a/utils/raspberrypi/ctt/convert_tuning.py
+++ b/utils/raspberrypi/ctt/convert_tuning.py
@@ -8,30 +8,104 @@
 
 import argparse
 import json
+import numpy as np
 import sys
 
 from ctt_pretty_print_json import pretty_print
+from ctt_pisp import grid_size as grid_size_pisp
+from ctt_pisp import json_template as json_template_pisp
+from ctt_vc4 import grid_size as grid_size_vc4
+from ctt_vc4 import json_template as json_template_vc4
 
 
-def convert_v2(in_json: dict) -> str:
+def interp_2d(in_ls, src_w, src_h, dst_w, dst_h):
 
-    if 'version' in in_json.keys() and in_json['version'] != 1.0:
-        print(f'The JSON config reports version {in_json["version"]} that is incompatible with this tool.')
-        sys.exit(-1)
+    out_ls = np.zeros((dst_h, dst_w))
+    for i in range(src_h):
+        out_ls[i] = np.interp(np.linspace(0, dst_w - 1, dst_w),
+                              np.linspace(0, dst_w - 1, src_w),
+                              in_ls[i])
+    for i in range(dst_w):
+        out_ls[:,i] = np.interp(np.linspace(0, dst_h - 1, dst_h),
+                                np.linspace(0, dst_h - 1, src_h),
+                                out_ls[:src_h, i])
+    return out_ls
 
-    converted = {
-        'version': 2.0,
-        'target': 'bcm2835',
-        'algorithms': [{algo: config} for algo, config in in_json.items()]
-    }
 
-    return pretty_print(converted)
+def convert_target(in_json: dict, target: str):
+
+    src_w, src_h = grid_size_pisp if target == 'vc4' else grid_size_vc4
+    dst_w, dst_h = grid_size_vc4 if target == 'vc4' else grid_size_pisp
+    json_template = json_template_vc4 if target == 'vc4' else json_template_pisp
+
+    # ALSC grid sizes
+    alsc = next(algo for algo in in_json['algorithms'] if 'rpi.alsc' in algo)['rpi.alsc']
+    for colour in ['calibrations_Cr', 'calibrations_Cb']:
+        if colour not in alsc:
+            continue
+        for temperature in alsc[colour]:
+            in_ls = np.reshape(temperature['table'], (src_h, src_w))
+            out_ls = interp_2d(in_ls, src_w, src_h, dst_w, dst_h)
+            temperature['table'] = np.round(out_ls.flatten(), 3).tolist()
+
+    if 'luminance_lut' in alsc:
+        in_ls = np.reshape(alsc['luminance_lut'], (src_h, src_w))
+        out_ls = interp_2d(in_ls, src_w, src_h, dst_w, dst_h)
+        alsc['luminance_lut'] = np.round(out_ls.flatten(), 3).tolist()
+
+    # Denoise blocks
+    for i, algo in enumerate(in_json['algorithms']):
+        if list(algo.keys())[0] == 'rpi.sdn':
+            in_json['algorithms'][i] = {'rpi.denoise': json_template['rpi.sdn'] if target == 'vc4' else json_template['rpi.denoise']}
+            break
+
+    # AGC mode weights
+    agc = next(algo for algo in in_json['algorithms'] if 'rpi.agc' in algo)['rpi.agc']
+    if 'channels' in agc:
+        for i, channel in enumerate(agc['channels']):
+            target_agc_metering = json_template['rpi.agc']['channels'][i]['metering_modes']
+            for mode, v in channel['metering_modes'].items():
+                v['weights'] = target_agc_metering[mode]['weights']
+    else:
+        for mode, v in agc["metering_modes"].items():
+            target_agc_metering = json_template['rpi.agc']['channels'][0]['metering_modes']
+            v['weights'] = target_agc_metering[mode]['weights']
+
+    # HDR
+    if target == 'pisp':
+        for i, algo in enumerate(in_json['algorithms']):
+            if list(algo.keys())[0] == 'rpi.hdr':
+                in_json['algorithms'][i] = {'rpi.hdr': json_template['rpi.hdr']}
+
+    return in_json
+
+
+def convert_v2(in_json: dict, target: str) -> str:
+
+    if 'version' in in_json.keys() and in_json['version'] == 1.0:
+        converted = {
+            'version': 2.0,
+            'target': target,
+            'algorithms': [{algo: config} for algo, config in in_json.items()]
+        }
+    else:
+        converted = in_json
+
+    # Convert between vc4 <-> pisp targets. This is a best effort thing.
+    if converted['target'] != target:
+        converted = convert_target(converted, target)
+        converted['target'] = target
+
+    grid_size = grid_size_vc4[0] if target == 'vc4' else grid_size_pisp[0]
+    return pretty_print(converted, custom_elems={'table': grid_size, 'luminance_lut': grid_size})
 
 
 if __name__ == "__main__":
     parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=
-                    'Convert the format of the Raspberry Pi camera tuning file from v1.0 to v2.0.\n')
+        'Convert the format of the Raspberry Pi camera tuning file from v1.0 to v2.0 and/or the vc4 <-> pisp targets.\n')
     parser.add_argument('input', type=str, help='Input tuning file.')
+    parser.add_argument('-t', '--target', type=str, help='Target platform.',
+                        choices=['pisp', 'vc4'], default='vc4')
     parser.add_argument('output', type=str, nargs='?',
                         help='Output converted tuning file. If not provided, the input file will be updated in-place.',
                         default=None)
@@ -40,7 +114,7 @@
     with open(args.input, 'r') as f:
         in_json = json.load(f)
 
-    out_json = convert_v2(in_json)
+    out_json = convert_v2(in_json, args.target)
 
     with open(args.output if args.output is not None else args.input, 'w') as f:
         f.write(out_json)
diff --git a/utils/raspberrypi/ctt/ctt.py b/utils/raspberrypi/ctt/ctt.py
index cd89f17..96f1b5e 100755
--- a/utils/raspberrypi/ctt/ctt.py
+++ b/utils/raspberrypi/ctt/ctt.py
@@ -4,11 +4,12 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt.py - camera tuning tool
+# camera tuning tool
 
 import os
 import sys
 from ctt_image_load import *
+from ctt_cac import *
 from ctt_ccm import *
 from ctt_awb import *
 from ctt_alsc import *
@@ -22,9 +23,10 @@
 
 """
 This file houses the camera object, which is used to perform the calibrations.
-The camera object houses all the calibration images as attributes in two lists:
+The camera object houses all the calibration images as attributes in three lists:
     - imgs (macbeth charts)
     - imgs_alsc (alsc correction images)
+    - imgs_cac (cac correction images)
 Various calibrations are methods of the camera object, and the output is stored
 in a dictionary called self.json.
 Once all the caibration has been completed, the Camera.json is written into a
@@ -67,139 +69,26 @@
 Input is the desired path of the output json.
 """
 class Camera:
-    def __init__(self, jfile):
+    def __init__(self, jfile, json):
         self.path = os.path.dirname(os.path.expanduser(__file__)) + '/'
         if self.path == '/':
             self.path = ''
         self.imgs = []
         self.imgs_alsc = []
+        self.imgs_cac = []
         self.log = 'Log created : ' + time.asctime(time.localtime(time.time()))
         self.log_separator = '\n'+'-'*70+'\n'
         self.jf = jfile
         """
         initial json dict populated by uncalibrated values
         """
-        self.json = {
-            "rpi.black_level": {
-                "black_level": 4096
-            },
-            "rpi.dpc": {
-            },
-            "rpi.lux": {
-                "reference_shutter_speed": 10000,
-                "reference_gain": 1,
-                "reference_aperture": 1.0
-            },
-            "rpi.noise": {
-            },
-            "rpi.geq": {
-            },
-            "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
-            },
-            "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': 100,
-                'luminance_strength': 0.7,
-            },
-            "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": {
-            },
-            "rpi.sharpen": {
-            }
-        }
+        self.json = json
 
     """
     Perform colour correction calibrations by comparing macbeth patch colours
     to standard macbeth chart colours.
     """
-    def ccm_cal(self, do_alsc_colour):
+    def ccm_cal(self, do_alsc_colour, grid_size):
         if 'rpi.ccm' in self.disable:
             return 1
         print('\nStarting CCM calibration')
@@ -245,7 +134,7 @@
         Do CCM calibration
         """
         try:
-            ccms = ccm(self, cal_cr_list, cal_cb_list)
+            ccms = ccm(self, cal_cr_list, cal_cb_list, grid_size)
         except ArithmeticError:
             print('ERROR: Matrix is singular!\nTake new pictures and try again...')
             self.log += '\nERROR: Singular matrix encountered during fit!'
@@ -259,11 +148,67 @@
         print('Finished CCM calibration')
 
     """
+    Perform chromatic abberation correction using multiple dots images.
+    """
+    def cac_cal(self, do_alsc_colour):
+        if 'rpi.cac' in self.disable:
+            return 1
+        print('\nStarting CAC calibration')
+        self.log_new_sec('CAC')
+        """
+        check if cac images have been taken
+        """
+        if len(self.imgs_cac) == 0:
+            print('\nError:\nNo cac calibration images found')
+            self.log += '\nERROR: No CAC calibration images found!'
+            self.log += '\nCAC calibration aborted!'
+            return 1
+        """
+        if image is greyscale then CAC makes no sense
+        """
+        if self.grey:
+            print('\nERROR: Can\'t do CAC on greyscale image!')
+            self.log += '\nERROR: Cannot perform CAC calibration '
+            self.log += 'on greyscale image!\nCAC aborted!'
+            del self.json['rpi.cac']
+            return 0
+        a = time.time()
+        """
+        Check if camera is greyscale or color. If not greyscale, then perform cac
+        """
+        if do_alsc_colour:
+            """
+            Here we have a color sensor. Perform cac
+            """
+            try:
+                cacs = cac(self)
+            except ArithmeticError:
+                print('ERROR: Matrix is singular!\nTake new pictures and try again...')
+                self.log += '\nERROR: Singular matrix encountered during fit!'
+                self.log += '\nCAC aborted!'
+                return 1
+        else:
+            """
+            case where config options suggest greyscale camera. No point in doing CAC
+            """
+            cal_cr_list, cal_cb_list = None, None
+            self.log += '\nWARNING: No ALSC tables found.\nCAC calibration '
+            self.log += 'performed without ALSC correction...'
+
+        """
+        Write output to json
+        """
+        self.json['rpi.cac']['cac'] = cacs
+        self.log += '\nCAC calibration written to json file'
+        print('Finished CAC calibration')
+
+
+    """
     Auto white balance calibration produces a colour curve for
     various colour temperatures, as well as providing a maximum 'wiggle room'
     distance from this curve (transverse_neg/pos).
     """
-    def awb_cal(self, greyworld, do_alsc_colour):
+    def awb_cal(self, greyworld, do_alsc_colour, grid_size):
         if 'rpi.awb' in self.disable:
             return 1
         print('\nStarting AWB calibration')
@@ -306,7 +251,7 @@
         call calibration function
         """
         plot = "rpi.awb" in self.plot
-        awb_out = awb(self, cal_cr_list, cal_cb_list, plot)
+        awb_out = awb(self, cal_cr_list, cal_cb_list, plot, grid_size)
         ct_curve, transverse_neg, transverse_pos = awb_out
         """
         write output to json
@@ -324,7 +269,7 @@
     colour channel seperately, and then partially corrects for vignetting.
     The extent of the correction depends on the 'luminance_strength' parameter.
     """
-    def alsc_cal(self, luminance_strength, do_alsc_colour):
+    def alsc_cal(self, luminance_strength, do_alsc_colour, grid_size, max_gain=8.0):
         if 'rpi.alsc' in self.disable:
             return 1
         print('\nStarting ALSC calibration')
@@ -347,10 +292,10 @@
         call calibration function
         """
         plot = "rpi.alsc" in self.plot
-        alsc_out = alsc_all(self, do_alsc_colour, plot)
+        alsc_out = alsc_all(self, do_alsc_colour, plot, grid_size, max_gain=max_gain)
         cal_cr_list, cal_cb_list, luminance_lut, av_corn = alsc_out
         """
-        write ouput to json and finish if not do_alsc_colour
+        write output to json and finish if not do_alsc_colour
         """
         if not do_alsc_colour:
             self.json['rpi.alsc']['luminance_lut'] = luminance_lut
@@ -393,7 +338,7 @@
         """
         obtain worst-case scenario residual sigmas
         """
-        sigma_r, sigma_b = get_sigma(self, cal_cr_list, cal_cb_list)
+        sigma_r, sigma_b = get_sigma(self, cal_cr_list, cal_cb_list, grid_size)
         """
         write output to json
         """
@@ -509,19 +454,20 @@
     """
     writes the json dictionary to the raw json file then make pretty
     """
-    def write_json(self):
+    def write_json(self, version=2.0, target='bcm2835', grid_size=(16, 12)):
         """
         Write json dictionary to file using our version 2 format
         """
 
         out_json = {
-            "version": 2.0,
-            'target': 'bcm2835',
+            "version": version,
+            'target': target if target != 'vc4' else 'bcm2835',
             "algorithms": [{name: data} for name, data in self.json.items()],
         }
 
         with open(self.jf, 'w') as f:
-            f.write(pretty_print(out_json))
+            f.write(pretty_print(out_json,
+                                 custom_elems={'table': grid_size[0], 'luminance_lut': grid_size[0]}))
 
     """
     add a new section to the log file
@@ -627,6 +573,16 @@
                     self.log += '\nWARNING: Error reading colour temperature'
                     self.log += '\nImage discarded!'
                     print('DISCARDED')
+            elif 'cac' in filename:
+                Img = load_image(self, address, mac=False)
+                self.log += '\nIdentified as an CAC image'
+                Img.name = filename
+                self.log += '\nColour temperature: {} K'.format(col)
+                self.imgs_cac.append(Img)
+                if blacklevel != -1:
+                    Img.blacklevel_16 = blacklevel
+                print(img_suc_msg)
+                continue
             else:
                 self.log += '\nIdentified as macbeth chart image'
                 """
@@ -672,6 +628,7 @@
         self.log += '\n\nImages found:'
         self.log += '\nMacbeth : {}'.format(len(self.imgs))
         self.log += '\nALSC : {} '.format(len(self.imgs_alsc))
+        self.log += '\nCAC: {} '.format(len(self.imgs_cac))
         self.log += '\n\nCamera metadata'
         """
         check usable images found
@@ -680,22 +637,21 @@
             print('\nERROR: No usable macbeth chart images found')
             self.log += '\nERROR: No usable macbeth chart images found'
             return 0
-        elif len(self.imgs) == 0 and len(self.imgs_alsc) == 0:
+        elif len(self.imgs) == 0 and len(self.imgs_alsc) == 0 and len(self.imgs_cac) == 0:
             print('\nERROR: No usable images found')
             self.log += '\nERROR: No usable images found'
             return 0
         """
         Double check that every image has come from the same camera...
         """
-        all_imgs = self.imgs + self.imgs_alsc
+        all_imgs = self.imgs + self.imgs_alsc + self.imgs_cac
         camNames = list(set([Img.camName for Img in all_imgs]))
         patterns = list(set([Img.pattern for Img in all_imgs]))
         sigbitss = list(set([Img.sigbits for Img in all_imgs]))
         blacklevels = list(set([Img.blacklevel_16 for Img in all_imgs]))
         sizes = list(set([(Img.w, Img.h) for Img in all_imgs]))
 
-        if len(camNames) == 1 and len(patterns) == 1 and len(sigbitss) == 1 and \
-           len(blacklevels) == 1 and len(sizes) == 1:
+        if 1:
             self.grey = (patterns[0] == 128)
             self.blacklevel_16 = blacklevels[0]
             self.log += '\nName: {}'.format(camNames[0])
@@ -712,7 +668,7 @@
             return 0
 
 
-def run_ctt(json_output, directory, config, log_output, alsc_only=False):
+def run_ctt(json_output, directory, config, log_output, json_template, grid_size, target, alsc_only=False):
     """
     check input files are jsons
     """
@@ -748,12 +704,14 @@
     greyworld = get_config(awb_d, "greyworld", 0, 'bool')
     alsc_d = get_config(configs, "alsc", {}, 'dict')
     do_alsc_colour = get_config(alsc_d, "do_alsc_colour", 1, 'bool')
-    luminance_strength = get_config(alsc_d, "luminance_strength", 0.5, 'num')
+    luminance_strength = get_config(alsc_d, "luminance_strength", 0.8, 'num')
+    lsc_max_gain = get_config(alsc_d, "max_gain", 8.0, 'num')
     blacklevel = get_config(configs, "blacklevel", -1, 'num')
     macbeth_d = get_config(configs, "macbeth", {}, 'dict')
     mac_small = get_config(macbeth_d, "small", 0, 'bool')
     mac_show = get_config(macbeth_d, "show", 0, 'bool')
     mac_config = (mac_small, mac_show)
+    print("Read lsc_max_gain", lsc_max_gain)
 
     if blacklevel < -1 or blacklevel >= 2**16:
         print('\nInvalid blacklevel, defaulted to 64')
@@ -772,7 +730,7 @@
     initialise tuning tool and load images
     """
     try:
-        Cam = Camera(json_output)
+        Cam = Camera(json_output, json=json_template)
         Cam.log_user_input(json_output, directory, config, log_output)
         if alsc_only:
             disable = set(Cam.json.keys()).symmetric_difference({"rpi.alsc"})
@@ -794,14 +752,17 @@
             Cam.json['rpi.black_level']['black_level'] = Cam.blacklevel_16
         Cam.json_remove(disable)
         print('\nSTARTING CALIBRATIONS')
-        Cam.alsc_cal(luminance_strength, do_alsc_colour)
+        Cam.alsc_cal(luminance_strength, do_alsc_colour, grid_size, max_gain=lsc_max_gain)
         Cam.geq_cal()
         Cam.lux_cal()
         Cam.noise_cal()
-        Cam.awb_cal(greyworld, do_alsc_colour)
-        Cam.ccm_cal(do_alsc_colour)
+        if "rpi.cac" in json_template:
+            Cam.cac_cal(do_alsc_colour)
+        Cam.awb_cal(greyworld, do_alsc_colour, grid_size)
+        Cam.ccm_cal(do_alsc_colour, grid_size)
+
         print('\nFINISHED CALIBRATIONS')
-        Cam.write_json()
+        Cam.write_json(target=target, grid_size=grid_size)
         Cam.write_log(log_output)
         print('\nCalibrations written to: '+json_output)
         if log_output is None:
@@ -811,20 +772,19 @@
     else:
         Cam.write_log(log_output)
 
-
 if __name__ == '__main__':
     """
     initialise calibration
     """
     if len(sys.argv) == 1:
         print("""
-    Pisp Camera Tuning Tool version 1.0
-
+    PiSP Tuning Tool version 1.0
     Required Arguments:
     '-i' : Calibration image directory.
     '-o' : Name of output json file.
 
     Optional Arguments:
+    '-t' : Target platform - 'pisp' or 'vc4'. Default 'vc4'
     '-c' : Config file for the CTT. If not passed, default parameters used.
     '-l' : Name of output log file. If not passed, 'ctt_log.txt' used.
               """)
@@ -833,5 +793,10 @@
         """
         parse input arguments
         """
-        json_output, directory, config, log_output = parse_input()
-        run_ctt(json_output, directory, config, log_output)
+        json_output, directory, config, log_output, target = parse_input()
+        if target == 'pisp':
+            from ctt_pisp import json_template, grid_size
+        elif target == 'vc4':
+            from ctt_vc4 import json_template, grid_size
+
+        run_ctt(json_output, directory, config, log_output, json_template, grid_size, target)
diff --git a/utils/raspberrypi/ctt/ctt_alsc.py b/utils/raspberrypi/ctt/ctt_alsc.py
index e51d693..1d94dfa 100644
--- a/utils/raspberrypi/ctt/ctt_alsc.py
+++ b/utils/raspberrypi/ctt/ctt_alsc.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_alsc.py - camera tuning tool for ALSC (auto lens shading correction)
+# camera tuning tool for ALSC (auto lens shading correction)
 
 from ctt_image_load import *
 import matplotlib.pyplot as plt
@@ -13,8 +13,9 @@
 """
 preform alsc calibration on a set of images
 """
-def alsc_all(Cam, do_alsc_colour, plot):
+def alsc_all(Cam, do_alsc_colour, plot, grid_size=(16, 12), max_gain=8.0):
     imgs_alsc = Cam.imgs_alsc
+    grid_w, grid_h = grid_size
     """
     create list of colour temperatures and associated calibration tables
     """
@@ -23,7 +24,7 @@
     list_cb = []
     list_cg = []
     for Img in imgs_alsc:
-        col, cr, cb, cg, size = alsc(Cam, Img, do_alsc_colour, plot)
+        col, cr, cb, cg, size = alsc(Cam, Img, do_alsc_colour, plot, grid_size=grid_size, max_gain=max_gain)
         list_col.append(col)
         list_cr.append(cr)
         list_cb.append(cb)
@@ -68,11 +69,12 @@
             t_b = np.where((100*t_b) % 1 >= 0.95, t_b-0.001, t_b)
             t_r = np.round(t_r, 3)
             t_b = np.round(t_b, 3)
-            r_corners = (t_r[0], t_r[15], t_r[-1], t_r[-16])
-            b_corners = (t_b[0], t_b[15], t_b[-1], t_b[-16])
-            r_cen = t_r[5*16+7]+t_r[5*16+8]+t_r[6*16+7]+t_r[6*16+8]
+            r_corners = (t_r[0], t_r[grid_w - 1], t_r[-1], t_r[-grid_w])
+            b_corners = (t_b[0], t_b[grid_w - 1], t_b[-1], t_b[-grid_w])
+            middle_pos = (grid_h // 2 - 1) * grid_w + grid_w - 1
+            r_cen = t_r[middle_pos]+t_r[middle_pos + 1]+t_r[middle_pos + grid_w]+t_r[middle_pos + grid_w + 1]
             r_cen = round(r_cen/4, 3)
-            b_cen = t_b[5*16+7]+t_b[5*16+8]+t_b[6*16+7]+t_b[6*16+8]
+            b_cen = t_b[middle_pos]+t_b[middle_pos + 1]+t_b[middle_pos + grid_w]+t_b[middle_pos + grid_w + 1]
             b_cen = round(b_cen/4, 3)
             Cam.log += '\nRed table corners: {}'.format(r_corners)
             Cam.log += '\nRed table centre: {}'.format(r_cen)
@@ -116,8 +118,9 @@
 """
 calculate g/r and g/b for 32x32 points arranged in a grid for a single image
 """
-def alsc(Cam, Img, do_alsc_colour, plot=False):
+def alsc(Cam, Img, do_alsc_colour, plot=False, grid_size=(16, 12), max_gain=8.0):
     Cam.log += '\nProcessing image: ' + Img.name
+    grid_w, grid_h = grid_size
     """
     get channel in correct order
     """
@@ -128,31 +131,34 @@
     where w is a multiple of 32.
     """
     w, h = Img.w/2, Img.h/2
-    dx, dy = int(-(-(w-1)//16)), int(-(-(h-1)//12))
+    dx, dy = int(-(-(w-1)//grid_w)), int(-(-(h-1)//grid_h))
     """
     average the green channels into one
     """
     av_ch_g = np.mean((channels[1:3]), axis=0)
     if do_alsc_colour:
         """
-        obtain 16x12 grid of intensities for each channel and subtract black level
+        obtain grid_w x grid_h grid of intensities for each channel and subtract black level
         """
-        g = get_16x12_grid(av_ch_g, dx, dy) - Img.blacklevel_16
-        r = get_16x12_grid(channels[0], dx, dy) - Img.blacklevel_16
-        b = get_16x12_grid(channels[3], dx, dy) - Img.blacklevel_16
+        g = get_grid(av_ch_g, dx, dy, grid_size) - Img.blacklevel_16
+        r = get_grid(channels[0], dx, dy, grid_size) - Img.blacklevel_16
+        b = get_grid(channels[3], dx, dy, grid_size) - Img.blacklevel_16
         """
         calculate ratios as 32 bit in order to be supported by medianBlur function
         """
-        cr = np.reshape(g/r, (12, 16)).astype('float32')
-        cb = np.reshape(g/b, (12, 16)).astype('float32')
-        cg = np.reshape(1/g, (12, 16)).astype('float32')
+        cr = np.reshape(g/r, (grid_h, grid_w)).astype('float32')
+        cb = np.reshape(g/b, (grid_h, grid_w)).astype('float32')
+        cg = np.reshape(1/g, (grid_h, grid_w)).astype('float32')
         """
         median blur to remove peaks and save as float 64
         """
         cr = cv2.medianBlur(cr, 3).astype('float64')
+        cr = cr/np.min(cr)  # gain tables are easier for humans to read if the minimum is 1.0
         cb = cv2.medianBlur(cb, 3).astype('float64')
+        cb = cb/np.min(cb)
         cg = cv2.medianBlur(cg, 3).astype('float64')
         cg = cg/np.min(cg)
+        cg = [min(v, max_gain) for v in cg.flatten()]  # never exceed the max luminance gain
 
         """
         debugging code showing 2D surface plot of vignetting. Quite useful for
@@ -164,7 +170,7 @@
             """
             note Y is plotted as -Y so plot has same axes as image
             """
-            X, Y = np.meshgrid(range(16), range(12))
+            X, Y = np.meshgrid(range(grid_w), range(grid_h))
             ha.plot_surface(X, -Y, cr, cmap=cm.coolwarm, linewidth=0)
             ha.set_title('ALSC Plot\nImg: {}\n\ncr'.format(Img.str))
             hb = hf.add_subplot(312, projection='3d')
@@ -176,21 +182,22 @@
             # print(Img.str)
             plt.show()
 
-        return Img.col, cr.flatten(), cb.flatten(), cg.flatten(), (w, h, dx, dy)
+        return Img.col, cr.flatten(), cb.flatten(), cg, (w, h, dx, dy)
 
     else:
         """
         only perform calculations for luminance shading
         """
-        g = get_16x12_grid(av_ch_g, dx, dy) - Img.blacklevel_16
-        cg = np.reshape(1/g, (12, 16)).astype('float32')
+        g = get_grid(av_ch_g, dx, dy, grid_size) - Img.blacklevel_16
+        cg = np.reshape(1/g, (grid_h, grid_w)).astype('float32')
         cg = cv2.medianBlur(cg, 3).astype('float64')
         cg = cg/np.min(cg)
+        cg = [min(v, max_gain) for v in cg.flatten()]  # never exceed the max luminance gain
 
         if plot:
             hf = plt.figure(figssize=(8, 8))
             ha = hf.add_subplot(1, 1, 1, projection='3d')
-            X, Y = np.meashgrid(range(16), range(12))
+            X, Y = np.meashgrid(range(grid_w), range(grid_h))
             ha.plot_surface(X, -Y, cg, cmap=cm.coolwarm, linewidth=0)
             ha.set_title('ALSC Plot (Luminance only!)\nImg: {}\n\ncg').format(Img.str)
             plt.show()
@@ -199,21 +206,22 @@
 
 
 """
-Compresses channel down to a 16x12 grid
+Compresses channel down to a grid of the requested size
 """
-def get_16x12_grid(chan, dx, dy):
+def get_grid(chan, dx, dy, grid_size):
+    grid_w, grid_h = grid_size
     grid = []
     """
     since left and bottom border will not necessarily have rectangles of
     dimension dx x dy, the 32nd iteration has to be handled separately.
     """
-    for i in range(11):
-        for j in range(15):
+    for i in range(grid_h - 1):
+        for j in range(grid_w - 1):
             grid.append(np.mean(chan[dy*i:dy*(1+i), dx*j:dx*(1+j)]))
-        grid.append(np.mean(chan[dy*i:dy*(1+i), 15*dx:]))
-    for j in range(15):
-        grid.append(np.mean(chan[11*dy:, dx*j:dx*(1+j)]))
-    grid.append(np.mean(chan[11*dy:, 15*dx:]))
+        grid.append(np.mean(chan[dy*i:dy*(1+i), (grid_w - 1)*dx:]))
+    for j in range(grid_w - 1):
+        grid.append(np.mean(chan[(grid_h - 1)*dy:, dx*j:dx*(1+j)]))
+    grid.append(np.mean(chan[(grid_h - 1)*dy:, (grid_w - 1)*dx:]))
     """
     return as np.array, ready for further manipulation
     """
@@ -223,7 +231,7 @@
 """
 obtains sigmas for red and blue, effectively a measure of the 'error'
 """
-def get_sigma(Cam, cal_cr_list, cal_cb_list):
+def get_sigma(Cam, cal_cr_list, cal_cb_list, grid_size):
     Cam.log += '\nCalculating sigmas'
     """
     provided colour alsc tables were generated for two different colour
@@ -241,8 +249,8 @@
     sigma_rs = []
     sigma_bs = []
     for i in range(len(cts)-1):
-        sigma_rs.append(calc_sigma(cal_cr_list[i]['table'], cal_cr_list[i+1]['table']))
-        sigma_bs.append(calc_sigma(cal_cb_list[i]['table'], cal_cb_list[i+1]['table']))
+        sigma_rs.append(calc_sigma(cal_cr_list[i]['table'], cal_cr_list[i+1]['table'], grid_size))
+        sigma_bs.append(calc_sigma(cal_cb_list[i]['table'], cal_cb_list[i+1]['table'], grid_size))
         Cam.log += '\nColour temperature interval {} - {} K'.format(cts[i], cts[i+1])
         Cam.log += '\nSigma red: {}'.format(sigma_rs[-1])
         Cam.log += '\nSigma blue: {}'.format(sigma_bs[-1])
@@ -263,12 +271,13 @@
 """
 calculate sigma from two adjacent gain tables
 """
-def calc_sigma(g1, g2):
+def calc_sigma(g1, g2, grid_size):
+    grid_w, grid_h = grid_size
     """
     reshape into 16x12 matrix
     """
-    g1 = np.reshape(g1, (12, 16))
-    g2 = np.reshape(g2, (12, 16))
+    g1 = np.reshape(g1, (grid_h, grid_w))
+    g2 = np.reshape(g2, (grid_h, grid_w))
     """
     apply gains to gain table
     """
@@ -280,8 +289,8 @@
     neighbours, then append to list
     """
     diffs = []
-    for i in range(10):
-        for j in range(14):
+    for i in range(grid_h - 2):
+        for j in range(grid_w - 2):
             """
             note indexing is incremented by 1 since all patches on borders are
             not counted
diff --git a/utils/raspberrypi/ctt/ctt_awb.py b/utils/raspberrypi/ctt/ctt_awb.py
index bf45e54..4af1fe4 100644
--- a/utils/raspberrypi/ctt/ctt_awb.py
+++ b/utils/raspberrypi/ctt/ctt_awb.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_awb.py - camera tuning tool for AWB
+# camera tuning tool for AWB
 
 from ctt_image_load import *
 import matplotlib.pyplot as plt
@@ -13,7 +13,7 @@
 """
 obtain piecewise linear approximation for colour curve
 """
-def awb(Cam, cal_cr_list, cal_cb_list, plot):
+def awb(Cam, cal_cr_list, cal_cb_list, plot, grid_size):
     imgs = Cam.imgs
     """
     condense alsc calibration tables into one dictionary
@@ -43,7 +43,7 @@
         Note: if alsc is disabled then colour_cals will be set to None and the
         function will just return the greyscale patches
         """
-        r_patchs, b_patchs, g_patchs = get_alsc_patches(Img, colour_cals)
+        r_patchs, b_patchs, g_patchs = get_alsc_patches(Img, colour_cals, grid_size=grid_size)
         """
         calculate ratio of r, b to g
         """
@@ -293,12 +293,13 @@
 """
 obtain greyscale patches and perform alsc colour correction
 """
-def get_alsc_patches(Img, colour_cals, grey=True):
+def get_alsc_patches(Img, colour_cals, grey=True, grid_size=(16, 12)):
     """
     get patch centre coordinates, image colour and the actual
     patches for each channel, remembering to subtract blacklevel
     If grey then only greyscale patches considered
     """
+    grid_w, grid_h = grid_size
     if grey:
         cen_coords = Img.cen_coords[3::4]
         col = Img.col
@@ -345,12 +346,12 @@
         bef_tabs = np.array(colour_cals[bef])
         aft_tabs = np.array(colour_cals[aft])
         col_tabs = (bef_tabs*db + aft_tabs*da)/(da+db)
-    col_tabs = np.reshape(col_tabs, (2, 12, 16))
+    col_tabs = np.reshape(col_tabs, (2, grid_h, grid_w))
     """
     calculate dx, dy used to calculate alsc table
     """
     w, h = Img.w/2, Img.h/2
-    dx, dy = int(-(-(w-1)//16)), int(-(-(h-1)//12))
+    dx, dy = int(-(-(w-1)//grid_w)), int(-(-(h-1)//grid_h))
     """
     make list of pairs of gains for each patch by selecting the correct value
     in alsc colour calibration table
diff --git a/utils/raspberrypi/ctt/ctt_cac.py b/utils/raspberrypi/ctt/ctt_cac.py
new file mode 100644
index 0000000..5a4c510
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt_cac.py
@@ -0,0 +1,228 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2023, Raspberry Pi Ltd
+#
+# ctt_cac.py - CAC (Chromatic Aberration Correction) tuning tool
+
+from PIL import Image
+import numpy as np
+import matplotlib.pyplot as plt
+from matplotlib import cm
+
+from ctt_dots_locator import find_dots_locations
+
+
+# This is the wrapper file that creates a JSON entry for you to append
+# to your camera tuning file.
+# It calculates the chromatic aberration at different points throughout
+# the image and uses that to produce a martix that can then be used
+# in the camera tuning files to correct this aberration.
+
+
+def pprint_array(array):
+    # Function to print the array in a tidier format
+    array = array
+    output = ""
+    for i in range(len(array)):
+        for j in range(len(array[0])):
+            output += str(round(array[i, j], 2)) + ", "
+        # Add the necessary indentation to the array
+        output += "\n                   "
+    # Cut off the end of the array (nicely formats it)
+    return output[:-22]
+
+
+def plot_shifts(red_shifts, blue_shifts):
+    # If users want, they can pass a command line option to show the shifts on a graph
+    # Can be useful to check that the functions are all working, and that the sample
+    # images are doing the right thing
+    Xs = np.array(red_shifts)[:, 0]
+    Ys = np.array(red_shifts)[:, 1]
+    Zs = np.array(red_shifts)[:, 2]
+    Zs2 = np.array(red_shifts)[:, 3]
+    Zs3 = np.array(blue_shifts)[:, 2]
+    Zs4 = np.array(blue_shifts)[:, 3]
+
+    fig, axs = plt.subplots(2, 2)
+    ax = fig.add_subplot(2, 2, 1, projection='3d')
+    ax.scatter(Xs, Ys, Zs, cmap=cm.jet, linewidth=0)
+    ax.set_title('Red X Shift')
+    ax = fig.add_subplot(2, 2, 2, projection='3d')
+    ax.scatter(Xs, Ys, Zs2, cmap=cm.jet, linewidth=0)
+    ax.set_title('Red Y Shift')
+    ax = fig.add_subplot(2, 2, 3, projection='3d')
+    ax.scatter(Xs, Ys, Zs3, cmap=cm.jet, linewidth=0)
+    ax.set_title('Blue X Shift')
+    ax = fig.add_subplot(2, 2, 4, projection='3d')
+    ax.scatter(Xs, Ys, Zs4, cmap=cm.jet, linewidth=0)
+    ax.set_title('Blue Y Shift')
+    fig.tight_layout()
+    plt.show()
+
+
+def shifts_to_yaml(red_shift, blue_shift, image_dimensions, output_grid_size=9):
+    # Convert the shifts to a numpy array for easier handling and initialise other variables
+    red_shifts = np.array(red_shift)
+    blue_shifts = np.array(blue_shift)
+    # create a grid that's smaller than the output grid, which we then interpolate from to get the output values
+    xrgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))
+    xbgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))
+    yrgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))
+    ybgrid = np.zeros((output_grid_size - 1, output_grid_size - 1))
+
+    xrsgrid = []
+    xbsgrid = []
+    yrsgrid = []
+    ybsgrid = []
+    xg = np.zeros((output_grid_size - 1, output_grid_size - 1))
+    yg = np.zeros((output_grid_size - 1, output_grid_size - 1))
+
+    # Format the grids - numpy doesn't work for this, it wants a
+    # nice uniformly spaced grid, which we don't know if we have yet, hence the rather mundane setup
+    for x in range(output_grid_size - 1):
+        xrsgrid.append([])
+        yrsgrid.append([])
+        xbsgrid.append([])
+        ybsgrid.append([])
+        for y in range(output_grid_size - 1):
+            xrsgrid[x].append([])
+            yrsgrid[x].append([])
+            xbsgrid[x].append([])
+            ybsgrid[x].append([])
+
+    image_size = (image_dimensions[0], image_dimensions[1])
+    gridxsize = image_size[0] / (output_grid_size - 1)
+    gridysize = image_size[1] / (output_grid_size - 1)
+
+    # Iterate through each dot, and it's shift values and put these into the correct grid location
+    for red_shift in red_shifts:
+        xgridloc = int(red_shift[0] / gridxsize)
+        ygridloc = int(red_shift[1] / gridysize)
+        xrsgrid[xgridloc][ygridloc].append(red_shift[2])
+        yrsgrid[xgridloc][ygridloc].append(red_shift[3])
+
+    for blue_shift in blue_shifts:
+        xgridloc = int(blue_shift[0] / gridxsize)
+        ygridloc = int(blue_shift[1] / gridysize)
+        xbsgrid[xgridloc][ygridloc].append(blue_shift[2])
+        ybsgrid[xgridloc][ygridloc].append(blue_shift[3])
+
+    # Now calculate the average pixel shift for each square in the grid
+    for x in range(output_grid_size - 1):
+        for y in range(output_grid_size - 1):
+            xrgrid[x, y] = np.mean(xrsgrid[x][y])
+            yrgrid[x, y] = np.mean(yrsgrid[x][y])
+            xbgrid[x, y] = np.mean(xbsgrid[x][y])
+            ybgrid[x, y] = np.mean(ybsgrid[x][y])
+
+    # Next, we start to interpolate the central points of the grid that gets passed to the tuning file
+    input_grids = np.array([xrgrid, yrgrid, xbgrid, ybgrid])
+    output_grids = np.zeros((4, output_grid_size, output_grid_size))
+
+    # Interpolate the centre of the grid
+    output_grids[:, 1:-1, 1:-1] = (input_grids[:, 1:, :-1] + input_grids[:, 1:, 1:] + input_grids[:, :-1, 1:] + input_grids[:, :-1, :-1]) / 4
+
+    # Edge cases:
+    output_grids[:, 1:-1, 0] = ((input_grids[:, :-1, 0] + input_grids[:, 1:, 0]) / 2 - output_grids[:, 1:-1, 1]) * 2 + output_grids[:, 1:-1, 1]
+    output_grids[:, 1:-1, -1] = ((input_grids[:, :-1, 7] + input_grids[:, 1:, 7]) / 2 - output_grids[:, 1:-1, -2]) * 2 + output_grids[:, 1:-1, -2]
+    output_grids[:, 0, 1:-1] = ((input_grids[:, 0, :-1] + input_grids[:, 0, 1:]) / 2 - output_grids[:, 1, 1:-1]) * 2 + output_grids[:, 1, 1:-1]
+    output_grids[:, -1, 1:-1] = ((input_grids[:, 7, :-1] + input_grids[:, 7, 1:]) / 2 - output_grids[:, -2, 1:-1]) * 2 + output_grids[:, -2, 1:-1]
+
+    # Corner Cases:
+    output_grids[:, 0, 0] = (output_grids[:, 0, 1] - output_grids[:, 1, 1]) + (output_grids[:, 1, 0] - output_grids[:, 1, 1]) + output_grids[:, 1, 1]
+    output_grids[:, 0, -1] = (output_grids[:, 0, -2] - output_grids[:, 1, -2]) + (output_grids[:, 1, -1] - output_grids[:, 1, -2]) + output_grids[:, 1, -2]
+    output_grids[:, -1, 0] = (output_grids[:, -1, 1] - output_grids[:, -2, 1]) + (output_grids[:, -2, 0] - output_grids[:, -2, 1]) + output_grids[:, -2, 1]
+    output_grids[:, -1, -1] = (output_grids[:, -2, -1] - output_grids[:, -2, -2]) + (output_grids[:, -1, -2] - output_grids[:, -2, -2]) + output_grids[:, -2, -2]
+
+    # Below, we swap the x and the y coordinates, and also multiply by a factor of -1
+    # This is due to the PiSP (standard) dimensions being flipped in comparison to
+    # PIL image coordinate directions, hence why xr -> yr. Also, the shifts calculated are colour shifts,
+    # and the PiSP block asks for the values it should shift by (hence the * -1, to convert from colour shift to a pixel shift)
+
+    output_grid_yr, output_grid_xr, output_grid_yb, output_grid_xb = output_grids * -1
+    return output_grid_xr, output_grid_yr, output_grid_xb, output_grid_yb
+
+
+def analyse_dot(dot, dot_location=[0, 0]):
+    # Scan through the dot, calculate the centroid of each colour channel by doing:
+    # pixel channel brightness * distance from top left corner
+    # Sum these, and divide by the sum of each channel's brightnesses to get a centroid for each channel
+    red_channel = np.array(dot)[:, :, 0]
+    y_num_pixels = len(red_channel[0])
+    x_num_pixels = len(red_channel)
+    yred_weight = np.sum(np.dot(red_channel, np.arange(y_num_pixels)))
+    xred_weight = np.sum(np.dot(np.arange(x_num_pixels), red_channel))
+    red_sum = np.sum(red_channel)
+
+    green_channel = np.array(dot)[:, :, 1]
+    ygreen_weight = np.sum(np.dot(green_channel, np.arange(y_num_pixels)))
+    xgreen_weight = np.sum(np.dot(np.arange(x_num_pixels), green_channel))
+    green_sum = np.sum(green_channel)
+
+    blue_channel = np.array(dot)[:, :, 2]
+    yblue_weight = np.sum(np.dot(blue_channel, np.arange(y_num_pixels)))
+    xblue_weight = np.sum(np.dot(np.arange(x_num_pixels), blue_channel))
+    blue_sum = np.sum(blue_channel)
+
+    # We return this structure. It contains 2 arrays that contain:
+    # the locations of the dot center, along with the channel shifts in the x and y direction:
+    # [ [red_center_x, red_center_y, red_x_shift, red_y_shift], [blue_center_x, blue_center_y, blue_x_shift, blue_y_shift] ]
+
+    return [[int(dot_location[0]) + int(len(dot) / 2), int(dot_location[1]) + int(len(dot[0]) / 2), xred_weight / red_sum - xgreen_weight / green_sum, yred_weight / red_sum - ygreen_weight / green_sum], [dot_location[0] + int(len(dot) / 2), dot_location[1] + int(len(dot[0]) / 2), xblue_weight / blue_sum - xgreen_weight / green_sum, yblue_weight / blue_sum - ygreen_weight / green_sum]]
+
+
+def cac(Cam):
+    filelist = Cam.imgs_cac
+
+    Cam.log += '\nCAC analysing files: {}'.format(str(filelist))
+    np.set_printoptions(precision=3)
+    np.set_printoptions(suppress=True)
+
+    # Create arrays to hold all the dots data and their colour offsets
+    red_shift = []  # Format is: [[Dot Center X, Dot Center Y, x shift, y shift]]
+    blue_shift = []
+    # Iterate through the files
+    # Multiple files is reccomended to average out the lens aberration through rotations
+    for file in filelist:
+        Cam.log += '\nCAC processing file'
+        print("\n Processing file")
+        # Read the raw RGB values
+        rgb = file.rgb
+        image_size = [file.h, file.w]  # Image size, X, Y
+        # Create a colour copy of the RGB values to use later in the calibration
+        imout = Image.new(mode="RGB", size=image_size)
+        rgb_image = np.array(imout)
+        # The rgb values need reshaping from a 1d array to a 3d array to be worked with easily
+        rgb.reshape((image_size[0], image_size[1], 3))
+        rgb_image = rgb
+
+        # Pass the RGB image through to the dots locating program
+        # Returns an array of the dots (colour rectangles around the dots), and an array of their locations
+        print("Finding dots")
+        Cam.log += '\nFinding dots'
+        dots, dots_locations = find_dots_locations(rgb_image)
+
+        # Now, analyse each dot. Work out the centroid of each colour channel, and use that to work out
+        # by how far the chromatic aberration has shifted each channel
+        Cam.log += '\nDots found: {}'.format(str(len(dots)))
+        print('Dots found: ' + str(len(dots)))
+
+        for dot, dot_location in zip(dots, dots_locations):
+            if len(dot) > 0:
+                if (dot_location[0] > 0) and (dot_location[1] > 0):
+                    ret = analyse_dot(dot, dot_location)
+                    red_shift.append(ret[0])
+                    blue_shift.append(ret[1])
+
+    # Take our arrays of red shifts and locations, push them through to be interpolated into a 9x9 matrix
+    # for the CAC block to handle and then store these as a .json file to be added to the camera
+    # tuning file
+    print("\nCreating output grid")
+    Cam.log += '\nCreating output grid'
+    rx, ry, bx, by = shifts_to_yaml(red_shift, blue_shift, image_size)
+
+    print("CAC correction complete!")
+    Cam.log += '\nCAC correction complete!'
+
+    # Give the JSON dict back to the main ctt program
+    return {"strength": 1.0, "lut_rx": list(rx.round(2).reshape(81)), "lut_ry": list(ry.round(2).reshape(81)), "lut_bx": list(bx.round(2).reshape(81)), "lut_by": list(by.round(2).reshape(81))}
diff --git a/utils/raspberrypi/ctt/ctt_ccm.py b/utils/raspberrypi/ctt/ctt_ccm.py
index a09bfd0..07c943a 100644
--- a/utils/raspberrypi/ctt/ctt_ccm.py
+++ b/utils/raspberrypi/ctt/ctt_ccm.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_ccm.py - camera tuning tool for CCM (colour correction matrix)
+# camera tuning tool for CCM (colour correction matrix)
 
 from ctt_image_load import *
 from ctt_awb import get_alsc_patches
@@ -56,7 +56,7 @@
 """
 
 
-def ccm(Cam, cal_cr_list, cal_cb_list):
+def ccm(Cam, cal_cr_list, cal_cb_list, grid_size):
     global matrix_selection_types, typenum
     imgs = Cam.imgs
     """
@@ -133,9 +133,7 @@
         Note: if alsc is disabled then colour_cals will be set to None and no
         the function will simply return the macbeth patches
         """
-        r, b, g = get_alsc_patches(Img, colour_cals, grey=False)
-        # 256 values for each patch of sRGB values
-
+        r, b, g = get_alsc_patches(Img, colour_cals, grey=False, grid_size=grid_size)
         """
         do awb
         Note: awb is done by measuring the macbeth chart in the image, rather
diff --git a/utils/raspberrypi/ctt/ctt_config_example.json b/utils/raspberrypi/ctt/ctt_config_example.json
index c7f9076..1105862 100644
--- a/utils/raspberrypi/ctt/ctt_config_example.json
+++ b/utils/raspberrypi/ctt/ctt_config_example.json
@@ -3,7 +3,8 @@
   "plot": [],
   "alsc": {
     "do_alsc_colour": 1,
-    "luminance_strength": 0.5
+    "luminance_strength": 0.8,
+    "max_gain": 8.0
   },
   "awb": {
     "greyworld": 0
@@ -13,4 +14,4 @@
     "small": 0,
     "show": 0
   }
-}
\ No newline at end of file
+}
diff --git a/utils/raspberrypi/ctt/ctt_dots_locator.py b/utils/raspberrypi/ctt/ctt_dots_locator.py
new file mode 100644
index 0000000..4945c04
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt_dots_locator.py
@@ -0,0 +1,118 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2023, Raspberry Pi Ltd
+#
+# find_dots.py - Used by CAC algorithm to convert image to set of dots
+
+'''
+This file takes the black and white version of the image, along with
+the color version. It then located the black dots on the image by
+thresholding dark pixels.
+In a rather fun way, the algorithm bounces around the thresholded area in a random path
+We then use the maximum and minimum of these paths to determine the dot shape and size
+This info is then used to return colored dots and locations back to the main file
+'''
+
+import numpy as np
+import random
+from PIL import Image, ImageEnhance, ImageFilter
+
+
+def find_dots_locations(rgb_image, color_threshold=100, dots_edge_avoid=75, image_edge_avoid=10, search_path_length=500, grid_scan_step_size=10, logfile=open("log.txt", "a+")):
+    # Initialise some starting variables
+    pixels = Image.fromarray(rgb_image)
+    pixels = pixels.convert("L")
+    enhancer = ImageEnhance.Contrast(pixels)
+    im_output = enhancer.enhance(1.4)
+    # We smooth it slightly to make it easier for the dot recognition program to locate the dots
+    im_output = im_output.filter(ImageFilter.GaussianBlur(radius=2))
+    bw_image = np.array(im_output)
+
+    location = [0, 0]
+    dots = []
+    dots_location = []
+    # the program takes away the edges - we don't want a dot that is half a circle, the
+    # centroids would all be wrong
+    for x in range(dots_edge_avoid, len(bw_image) - dots_edge_avoid, grid_scan_step_size):
+        for y in range(dots_edge_avoid, len(bw_image[0]) - dots_edge_avoid, grid_scan_step_size):
+            location = [x, y]
+            scrap_dot = False  # A variable used to make sure that this is a valid dot
+            if (bw_image[location[0], location[1]] < color_threshold) and not (scrap_dot):
+                heading = "south"  # Define a starting direction to move in
+                coords = []
+                for i in range(search_path_length):  # Creates a path of length `search_path_length`. This turns out to always be enough to work out the rough shape of the dot.
+                    # Now make sure that the thresholded area doesn't come within 10 pixels of the edge of the image, ensures we capture all the CA
+                    if ((image_edge_avoid < location[0] < len(bw_image) - image_edge_avoid) and (image_edge_avoid < location[1] < len(bw_image[0]) - image_edge_avoid)) and not (scrap_dot):
+                        if heading == "south":
+                            if bw_image[location[0] + 1, location[1]] < color_threshold:
+                                # Here, notice it does not go south, but actually goes southeast
+                                # This is crucial in ensuring that we make our way around the majority of the dot
+                                location[0] = location[0] + 1
+                                location[1] = location[1] + 1
+                                heading = "south"
+                            else:
+                                # This happens when we reach a thresholded edge. We now randomly change direction and keep searching
+                                dir = random.randint(1, 2)
+                                if dir == 1:
+                                    heading = "west"
+                                if dir == 2:
+                                    heading = "east"
+
+                        if heading == "east":
+                            if bw_image[location[0], location[1] + 1] < color_threshold:
+                                location[1] = location[1] + 1
+                                heading = "east"
+                            else:
+                                dir = random.randint(1, 2)
+                                if dir == 1:
+                                    heading = "north"
+                                if dir == 2:
+                                    heading = "south"
+
+                        if heading == "west":
+                            if bw_image[location[0], location[1] - 1] < color_threshold:
+                                location[1] = location[1] - 1
+                                heading = "west"
+                            else:
+                                dir = random.randint(1, 2)
+                                if dir == 1:
+                                    heading = "north"
+                                if dir == 2:
+                                    heading = "south"
+
+                        if heading == "north":
+                            if bw_image[location[0] - 1, location[1]] < color_threshold:
+                                location[0] = location[0] - 1
+                                heading = "north"
+                            else:
+                                dir = random.randint(1, 2)
+                                if dir == 1:
+                                    heading = "west"
+                                if dir == 2:
+                                    heading = "east"
+                        # Log where our particle travels across the dot
+                        coords.append([location[0], location[1]])
+                    else:
+                        scrap_dot = True  # We just don't have enough space around the dot, discard this one, and move on
+                if not scrap_dot:
+                    # get the size of the dot surrounding the dot
+                    x_coords = np.array(coords)[:, 0]
+                    y_coords = np.array(coords)[:, 1]
+                    hsquaresize = max(list(x_coords)) - min(list(x_coords))
+                    vsquaresize = max(list(y_coords)) - min(list(y_coords))
+                    # Create the bounding coordinates of the rectangle surrounding the dot
+                    # Program uses the dotsize + half of the dotsize to ensure we get all that color fringing
+                    extra_space_factor = 0.45
+                    top_left_x = (min(list(x_coords)) - int(hsquaresize * extra_space_factor))
+                    btm_right_x = max(list(x_coords)) + int(hsquaresize * extra_space_factor)
+                    top_left_y = (min(list(y_coords)) - int(vsquaresize * extra_space_factor))
+                    btm_right_y = max(list(y_coords)) + int(vsquaresize * extra_space_factor)
+                    # Overwrite the area of the dot to ensure we don't use it again
+                    bw_image[top_left_x:btm_right_x, top_left_y:btm_right_y] = 255
+                    # Add the color version of the dot to the list to send off, along with some coordinates.
+                    dots.append(rgb_image[top_left_x:btm_right_x, top_left_y:btm_right_y])
+                    dots_location.append([top_left_x, top_left_y])
+                else:
+                    # Dot was too close to the image border to be useable
+                    pass
+    return dots, dots_location
diff --git a/utils/raspberrypi/ctt/ctt_geq.py b/utils/raspberrypi/ctt/ctt_geq.py
index c45addc..5a91ebb 100644
--- a/utils/raspberrypi/ctt/ctt_geq.py
+++ b/utils/raspberrypi/ctt/ctt_geq.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_geq.py - camera tuning tool for GEQ (green equalisation)
+# camera tuning tool for GEQ (green equalisation)
 
 from ctt_tools import *
 import matplotlib.pyplot as plt
diff --git a/utils/raspberrypi/ctt/ctt_image_load.py b/utils/raspberrypi/ctt/ctt_image_load.py
index 310c5e8..531de32 100644
--- a/utils/raspberrypi/ctt/ctt_image_load.py
+++ b/utils/raspberrypi/ctt/ctt_image_load.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019-2020, Raspberry Pi Ltd
 #
-# ctt_image_load.py - camera tuning tool image loading
+# camera tuning tool image loading
 
 from ctt_tools import *
 from ctt_macbeth_locator import *
@@ -350,6 +350,7 @@
         c2 = np.left_shift(raw_data[1::2, 0::2].astype(np.int64), shift)
         c3 = np.left_shift(raw_data[1::2, 1::2].astype(np.int64), shift)
         Img.channels = [c0, c1, c2, c3]
+        Img.rgb = raw_im.postprocess()
 
     except Exception:
         print("\nERROR: failed to load DNG file", im_str)
diff --git a/utils/raspberrypi/ctt/ctt_lux.py b/utils/raspberrypi/ctt/ctt_lux.py
index 70855e1..46be151 100644
--- a/utils/raspberrypi/ctt/ctt_lux.py
+++ b/utils/raspberrypi/ctt/ctt_lux.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_lux.py - camera tuning tool for lux level
+# camera tuning tool for lux level
 
 from ctt_tools import *
 
diff --git a/utils/raspberrypi/ctt/ctt_macbeth_locator.py b/utils/raspberrypi/ctt/ctt_macbeth_locator.py
index 3e95df8..f22dbf3 100644
--- a/utils/raspberrypi/ctt/ctt_macbeth_locator.py
+++ b/utils/raspberrypi/ctt/ctt_macbeth_locator.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_macbeth_locator.py - camera tuning tool Macbeth chart locator
+# camera tuning tool Macbeth chart locator
 
 from ctt_ransac import *
 from ctt_tools import *
@@ -57,6 +57,10 @@
     """
     cor, mac, coords, msg = get_macbeth_chart(img, ref_data)
 
+    # Keep a list that will include this and any brightened up versions of
+    # the image for reuse.
+    all_images = [img]
+
     """
     following bits of code tries to fix common problems with simple
     techniques.
@@ -71,6 +75,7 @@
     if cor < 0.75:
         a = 2
         img_br = cv2.convertScaleAbs(img, alpha=a, beta=0)
+        all_images.append(img_br)
         cor_b, mac_b, coords_b, msg_b = get_macbeth_chart(img_br, ref_data)
         if cor_b > cor:
             cor, mac, coords, msg = cor_b, mac_b, coords_b, msg_b
@@ -81,6 +86,7 @@
     if cor < 0.75:
         a = 4
         img_br = cv2.convertScaleAbs(img, alpha=a, beta=0)
+        all_images.append(img_br)
         cor_b, mac_b, coords_b, msg_b = get_macbeth_chart(img_br, ref_data)
         if cor_b > cor:
             cor, mac, coords, msg = cor_b, mac_b, coords_b, msg_b
@@ -128,23 +134,26 @@
         h_inc = int(h/6)
         """
         for each subselection, look for a macbeth chart
+        loop over this and any brightened up images that we made to increase the
+        likelihood of success
         """
-        for i in range(3):
-            for j in range(3):
-                w_s, h_s = i*w_inc, j*h_inc
-                img_sel = img[w_s:w_s+w_sel, h_s:h_s+h_sel]
-                cor_ij, mac_ij, coords_ij, msg_ij = get_macbeth_chart(img_sel, ref_data)
-                """
-                if the correlation is better than the best then record the
-                scale and current subselection at which macbeth chart was
-                found. Also record the coordinates, macbeth chart and message.
-                """
-                if cor_ij > cor:
-                    cor = cor_ij
-                    mac, coords, msg = mac_ij, coords_ij, msg_ij
-                    ii, jj = i, j
-                    w_best, h_best = w_inc, h_inc
-                    d_best = 1
+        for img_br in all_images:
+            for i in range(3):
+                for j in range(3):
+                    w_s, h_s = i*w_inc, j*h_inc
+                    img_sel = img_br[w_s:w_s+w_sel, h_s:h_s+h_sel]
+                    cor_ij, mac_ij, coords_ij, msg_ij = get_macbeth_chart(img_sel, ref_data)
+                    """
+                    if the correlation is better than the best then record the
+                    scale and current subselection at which macbeth chart was
+                    found. Also record the coordinates, macbeth chart and message.
+                    """
+                    if cor_ij > cor:
+                        cor = cor_ij
+                        mac, coords, msg = mac_ij, coords_ij, msg_ij
+                        ii, jj = i, j
+                        w_best, h_best = w_inc, h_inc
+                        d_best = 1
 
     """
     scale 2
@@ -157,17 +166,19 @@
         h_sel = int(h/2)
         w_inc = int(w/8)
         h_inc = int(h/8)
-        for i in range(5):
-            for j in range(5):
-                w_s, h_s = i*w_inc, j*h_inc
-                img_sel = img[w_s:w_s+w_sel, h_s:h_s+h_sel]
-                cor_ij, mac_ij, coords_ij, msg_ij = get_macbeth_chart(img_sel, ref_data)
-                if cor_ij > cor:
-                    cor = cor_ij
-                    mac, coords, msg = mac_ij, coords_ij, msg_ij
-                    ii, jj = i, j
-                    w_best, h_best = w_inc, h_inc
-                    d_best = 2
+        # Again, loop over any brightened up images as well
+        for img_br in all_images:
+            for i in range(5):
+                for j in range(5):
+                    w_s, h_s = i*w_inc, j*h_inc
+                    img_sel = img_br[w_s:w_s+w_sel, h_s:h_s+h_sel]
+                    cor_ij, mac_ij, coords_ij, msg_ij = get_macbeth_chart(img_sel, ref_data)
+                    if cor_ij > cor:
+                        cor = cor_ij
+                        mac, coords, msg = mac_ij, coords_ij, msg_ij
+                        ii, jj = i, j
+                        w_best, h_best = w_inc, h_inc
+                        d_best = 2
 
     """
     The following code checks for macbeth charts at even smaller scales. This
@@ -238,7 +249,7 @@
     print error or success message
     """
     print(msg)
-    Cam.log += '\n' + msg
+    Cam.log += '\n' + str(msg)
     if msg == success_msg:
         coords_fit = coords
         Cam.log += '\nMacbeth chart vertices:\n'
@@ -606,7 +617,7 @@
                     '\nNot enough squares found'
                     '\nPossible problems:\n'
                     '- Macbeth chart is occluded\n'
-                    '- Macbeth chart is too dark of bright\n'
+                    '- Macbeth chart is too dark or bright\n'
                 )
 
             ref_cents = np.array(ref_cents)
diff --git a/utils/raspberrypi/ctt/ctt_noise.py b/utils/raspberrypi/ctt/ctt_noise.py
index 3270bf3..0b18d83 100644
--- a/utils/raspberrypi/ctt/ctt_noise.py
+++ b/utils/raspberrypi/ctt/ctt_noise.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_noise.py - camera tuning tool noise calibration
+# camera tuning tool noise calibration
 
 from ctt_image_load import *
 import matplotlib.pyplot as plt
diff --git a/utils/raspberrypi/ctt/ctt_pisp.py b/utils/raspberrypi/ctt/ctt_pisp.py
new file mode 100755
index 0000000..a59b053
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt_pisp.py
@@ -0,0 +1,805 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+#
+# ctt_pisp.py - camera tuning tool data for PiSP platforms
+
+
+json_template = {
+    "rpi.black_level": {
+        "black_level": 4096
+    },
+    "rpi.lux": {
+        "reference_shutter_speed": 10000,
+        "reference_gain": 1,
+        "reference_aperture": 1.0
+    },
+    "rpi.dpc": {
+	"strength": 1
+    },
+    "rpi.noise": {
+    },
+    "rpi.geq": {
+    },
+    "rpi.denoise":
+    {
+        "normal":
+        {
+            "sdn":
+            {
+                "deviation": 1.6,
+                "strength": 0.5,
+                "deviation2": 3.2,
+                "deviation_no_tdn": 3.2,
+                "strength_no_tdn": 0.75
+            },
+            "cdn":
+            {
+                "deviation": 200,
+                "strength": 0.3
+            },
+            "tdn":
+            {
+                "deviation": 0.8,
+                "threshold": 0.05
+            }
+        },
+        "hdr":
+        {
+            "sdn":
+            {
+                "deviation": 1.6,
+                "strength": 0.5,
+                "deviation2": 3.2,
+                "deviation_no_tdn": 3.2,
+                "strength_no_tdn": 0.75
+            },
+            "cdn":
+            {
+                "deviation": 200,
+                "strength": 0.3
+            },
+            "tdn":
+            {
+                "deviation": 1.3,
+                "threshold": 0.1
+            }
+        },
+        "night":
+        {
+            "sdn":
+            {
+                "deviation": 1.6,
+                "strength": 0.5,
+                "deviation2": 3.2,
+                "deviation_no_tdn": 3.2,
+                "strength_no_tdn": 0.75
+            },
+            "cdn":
+            {
+                "deviation": 200,
+                "strength": 0.3
+            },
+            "tdn":
+            {
+                "deviation": 1.3,
+                "threshold": 0.1
+            }
+        }
+    },
+    "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": 7700},
+            "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
+    },
+    "rpi.agc":
+    {
+        "channels":
+        [
+            {
+                "comment": "Channel 0 is normal AGC",
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights":
+                        [
+                            0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
+                            0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+                            1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+                            1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+                            1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+                            0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+                            0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0
+                        ]
+                    },
+                    "spot":
+                    {
+                        "weights":
+                        [
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 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,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            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, 1.5, 2.0, 4.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 5000, 10000, 20000, 60000 ],
+                        "gain": [ 1.0, 1.5, 2.0, 4.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 10000, 30000, 60000, 90000, 120000 ],
+                        "gain": [ 1.0, 1.5, 2.0, 4.0, 8.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
+                ]
+            },
+            {
+                "comment": "Channel 1 is the HDR short channel",
+                "desaturate": 0,
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights":
+                        [
+                            0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
+                            0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+                            1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+                            1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+                            1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+                            0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+                            0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0
+                        ]
+                    },
+                    "spot":
+                    {
+                        "weights":
+                        [
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 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,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 20000, 60000 ],
+                        "gain": [ 1.0, 1.0, 1.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 20000, 60000 ],
+                        "gain": [ 1.0, 1.0, 1.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 20000, 60000 ],
+                        "gain": [ 1.0, 1.0, 1.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.95,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.95,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.7,
+                                1000, 0.7
+                            ]
+                        },
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.2,
+                            "y_target":
+                            [
+                                0, 0.002,
+                                1000, 0.002
+                            ]
+                        }
+                    ],
+                    "highlight": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.95,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.95,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.7,
+                                1000, 0.7
+                            ]
+                        },
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.2,
+                            "y_target":
+                            [
+                                0, 0.002,
+                                1000, 0.002
+                            ]
+                        }
+                    ],
+                    "shadows": [
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.95,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        },
+                        {
+                            "bound": "UPPER",
+                            "q_lo": 0.95,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.7,
+                                1000, 0.7
+                            ]
+                        },
+                        {
+                            "bound": "LOWER",
+                            "q_lo": 0.0,
+                            "q_hi": 0.2,
+                            "y_target":
+                            [
+                                0, 0.002,
+                                1000, 0.002
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            },
+            {
+                "comment": "Channel 2 is the HDR long channel",
+                "desaturate": 0,
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights":
+                        [
+                            0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
+                            0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+                            1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+                            1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+                            1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+                            0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+                            0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0
+                        ]
+                    },
+                    "spot":
+                    {
+                        "weights":
+                        [
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 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,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 20000, 30000, 60000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 8.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 20000, 30000, 60000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 8.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 20000, 30000, 60000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 8.0 ]
+                    }
+                },
+                "constraint_modes":
+                {
+                    "normal": [
+                    ],
+                    "highlight": [
+                    ],
+                    "shadows": [
+                    ]
+                },
+                "channel_constraints":
+                [
+                    {
+                        "bound": "UPPER",
+                        "channel": 4,
+                        "factor": 8
+                    },
+                    {
+                        "bound": "LOWER",
+                        "channel": 4,
+                        "factor": 2
+                    }
+                ],
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.165,
+                    10000, 0.17
+                ]
+            },
+            {
+                "comment": "Channel 3 is the night mode channel",
+                "base_ev": 0.33,
+                "metering_modes":
+                {
+                    "centre-weighted":
+                    {
+                        "weights":
+                        [
+                            0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0,
+                            0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+                            1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+                            1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 4, 4, 4, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 3, 3, 3, 4, 3, 3, 3, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 3, 3, 3, 2, 2, 2, 2, 1, 1,
+                            1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1,
+                            1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1,
+                            0, 1, 1, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1, 0,
+                            0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0
+                        ]
+                    },
+                    "spot":
+                    {
+                        "weights":
+                        [
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 1, 2, 3, 2, 1, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 1, 2, 1, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+                            0, 0, 0, 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,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
+                            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1
+                        ]
+                    }
+                },
+                "exposure_modes":
+                {
+                    "normal":
+                    {
+                        "shutter": [ 100, 20000, 66666 ],
+                        "gain": [ 1.0, 2.0, 4.0 ]
+                    },
+                    "short":
+                    {
+                        "shutter": [ 100, 20000, 33333 ],
+                        "gain": [ 1.0, 2.0, 4.0 ]
+                    },
+                    "long":
+                    {
+                        "shutter": [ 100, 20000, 66666, 120000 ],
+                        "gain": [ 1.0, 2.0, 4.0, 4.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.98,
+                            "q_hi": 1.0,
+                            "y_target":
+                            [
+                                0, 0.5,
+                                1000, 0.5
+                            ]
+                        }
+                    ]
+                },
+                "y_target":
+                [
+                    0, 0.16,
+                    1000, 0.16,
+                    10000, 0.17
+                ]
+            }
+        ]
+    },
+    "rpi.alsc": {
+        'omega': 1.3,
+        'n_iter': 100,
+        'luminance_strength': 0.8,
+    },
+    "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": {
+    },
+    "rpi.cac": {
+    },
+    "rpi.sharpen": {
+	"threshold": 0.25,
+	"limit": 1.0,
+	"strength": 1.0
+    },
+    "rpi.hdr":
+    {
+        "Off":
+        {
+            "cadence": [ 0 ]
+        },
+        "MultiExposureUnmerged":
+        {
+            "cadence": [ 1, 2 ],
+            "channel_map": { "short": 1, "long": 2 }
+        },
+        "SingleExposure":
+        {
+            "cadence": [1],
+            "channel_map": { "short": 1 },
+            "spatial_gain": 2.0,
+            "tonemap_enable": 1
+        },
+        "MultiExposure":
+        {
+            "cadence": [1, 2],
+            "channel_map": { "short": 1, "long": 2 },
+            "stitch_enable": 1,
+            "spatial_gain": 2.0,
+            "tonemap_enable": 1
+        },
+        "Night":
+        {
+            "cadence": [ 3 ],
+            "channel_map": { "night": 3 },
+            "tonemap_enable": 1,
+            "tonemap":
+            [
+                0, 0,
+                5000, 20000,
+                10000, 30000,
+                20000, 47000,
+                30000, 55000,
+                65535, 65535
+            ]
+        }
+    }
+}
+
+grid_size = (32, 32)
diff --git a/utils/raspberrypi/ctt/ctt_pretty_print_json.py b/utils/raspberrypi/ctt/ctt_pretty_print_json.py
index 3e3b847..a4cae62 100755
--- a/utils/raspberrypi/ctt/ctt_pretty_print_json.py
+++ b/utils/raspberrypi/ctt/ctt_pretty_print_json.py
@@ -19,13 +19,19 @@
         self.indentation_level = 0
         self.hard_break = 120
         self.custom_elems = {
+            'weights': 15,
             'table': 16,
             'luminance_lut': 16,
             'ct_curve': 3,
             'ccm': 3,
+            'lut_rx': 9,
+            'lut_bx': 9,
+            'lut_by': 9,
+            'lut_ry': 9,
             'gamma_curve': 2,
             'y_target': 2,
-            'prior': 2
+            'prior': 2,
+            'tonemap': 2
         }
 
     def encode(self, o, node_key=None):
@@ -87,7 +93,7 @@
         return self.encode(o)
 
 
-def pretty_print(in_json: dict) -> str:
+def pretty_print(in_json: dict, custom_elems={}) -> str:
 
     if 'version' not in in_json or \
        'target' not in in_json or \
@@ -95,12 +101,15 @@
        in_json['version'] < 2.0:
         raise RuntimeError('Incompatible JSON dictionary has been provided')
 
-    return json.dumps(in_json, cls=Encoder, indent=4, sort_keys=False)
+    encoder = Encoder(indent=4, sort_keys=False)
+    encoder.custom_elems |= custom_elems
+    return encoder.encode(in_json) #json.dumps(in_json, cls=Encoder, indent=4, sort_keys=False)
 
 
 if __name__ == "__main__":
     parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter, description=
                     'Prettify a version 2.0 camera tuning config JSON file.')
+    parser.add_argument('-t', '--target', type=str, help='Target platform', choices=['pisp', 'vc4'], default='vc4')
     parser.add_argument('input', type=str, help='Input tuning file.')
     parser.add_argument('output', type=str, nargs='?',
                         help='Output converted tuning file. If not provided, the input file will be updated in-place.',
@@ -110,7 +119,12 @@
     with open(args.input, 'r') as f:
         in_json = json.load(f)
 
-    out_json = pretty_print(in_json)
+    if args.target == 'pisp':
+        from ctt_pisp import grid_size
+    elif args.target == 'vc4':
+        from ctt_vc4 import grid_size
+
+    out_json = pretty_print(in_json, custom_elems={'table': grid_size[0], 'luminance_lut': grid_size[0]})
 
     with open(args.output if args.output is not None else args.input, 'w') as f:
         f.write(out_json)
diff --git a/utils/raspberrypi/ctt/ctt_ransac.py b/utils/raspberrypi/ctt/ctt_ransac.py
index 9ed7d93..01bba30 100644
--- a/utils/raspberrypi/ctt/ctt_ransac.py
+++ b/utils/raspberrypi/ctt/ctt_ransac.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_ransac.py - camera tuning tool RANSAC selector for Macbeth chart locator
+# camera tuning tool RANSAC selector for Macbeth chart locator
 
 import numpy as np
 
diff --git a/utils/raspberrypi/ctt/ctt_tools.py b/utils/raspberrypi/ctt/ctt_tools.py
index 7919528..50b01ec 100644
--- a/utils/raspberrypi/ctt/ctt_tools.py
+++ b/utils/raspberrypi/ctt/ctt_tools.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# ctt_tools.py - camera tuning tool miscellaneous
+# camera tuning tool miscellaneous
 
 import time
 import re
@@ -65,11 +65,12 @@
     directory = get_config(args_dict, '-i', None, 'string')
     config = get_config(args_dict, '-c', None, 'string')
     log_path = get_config(args_dict, '-l', None, 'string')
+    target = get_config(args_dict, '-t', "vc4", 'string')
     if directory is None:
         raise ArgError('\n\nERROR! No input directory given.')
     if json_output is None:
         raise ArgError('\n\nERROR! No output json given.')
-    return json_output, directory, config, log_path
+    return json_output, directory, config, log_path, target
 
 
 """
diff --git a/utils/raspberrypi/ctt/ctt_vc4.py b/utils/raspberrypi/ctt/ctt_vc4.py
new file mode 100755
index 0000000..7154e11
--- /dev/null
+++ b/utils/raspberrypi/ctt/ctt_vc4.py
@@ -0,0 +1,126 @@
+#!/usr/bin/env python3
+#
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+#
+# ctt_vc4.py - camera tuning tool data for VC4 platforms
+
+
+json_template = {
+    "rpi.black_level": {
+        "black_level": 4096
+    },
+    "rpi.dpc": {
+    },
+    "rpi.lux": {
+        "reference_shutter_speed": 10000,
+        "reference_gain": 1,
+        "reference_aperture": 1.0
+    },
+    "rpi.noise": {
+    },
+    "rpi.geq": {
+    },
+    "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
+    },
+    "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': 100,
+        'luminance_strength': 0.7,
+    },
+    "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": {
+    },
+    "rpi.sharpen": {
+    }
+}
+
+grid_size = (16, 12)
diff --git a/utils/rkisp1/gen-csc-table.py b/utils/rkisp1/gen-csc-table.py
index c47f504..ffc0370 100755
--- a/utils/rkisp1/gen-csc-table.py
+++ b/utils/rkisp1/gen-csc-table.py
@@ -1,4 +1,4 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
 # SPDX-License-Identifier: GPL-2.0-or-later
 # Copyright (C) 2022, Ideas on Board Oy
 #
diff --git a/utils/rkisp1/rkisp1-capture.sh b/utils/rkisp1/rkisp1-capture.sh
index c5f859f..d767e31 100755
--- a/utils/rkisp1/rkisp1-capture.sh
+++ b/utils/rkisp1/rkisp1-capture.sh
@@ -4,8 +4,7 @@
 #
 # Author: Laurent Pinchart <laurent.pinchart@ideasonboard.com>
 #
-# rkisp-capture.sh - Capture processed frames from cameras based on the
-# Rockchip ISP1
+# Capture processed frames from cameras based on the Rockchip ISP1
 #
 # The scripts makes use of the following tools, which are expected to be
 # executable from the system-wide path or from the local directory:
diff --git a/utils/tracepoints/analyze-ipa-trace.py b/utils/tracepoints/analyze-ipa-trace.py
index 50fbbf4..92e8a23 100755
--- a/utils/tracepoints/analyze-ipa-trace.py
+++ b/utils/tracepoints/analyze-ipa-trace.py
@@ -4,7 +4,7 @@
 #
 # Author: Paul Elder <paul.elder@ideasonboard.com>
 #
-# analyze-ipa-trace.py - Example of how to extract information from libcamera lttng traces
+# Example of how to extract information from libcamera lttng traces
 
 import argparse
 import bt2
diff --git a/utils/tracepoints/gen-tp-header.py b/utils/tracepoints/gen-tp-header.py
index a454615..83606c3 100755
--- a/utils/tracepoints/gen-tp-header.py
+++ b/utils/tracepoints/gen-tp-header.py
@@ -4,7 +4,7 @@
 #
 # Author: Paul Elder <paul.elder@ideasonboard.com>
 #
-# gen-tp-header.py - Generate header file to contain lttng tracepoints
+# Generate header file to contain lttng tracepoints
 
 import datetime
 import jinja2
diff --git a/utils/tuning/README.rst b/utils/tuning/README.rst
index ce533b2..ef3e6ad 100644
--- a/utils/tuning/README.rst
+++ b/utils/tuning/README.rst
@@ -5,7 +5,7 @@
 Dependencies
 ------------
 
-- cv2
 - numpy
-- pyexiv2
+- opencv-python
+- py3exiv2
 - rawpy
diff --git a/utils/tuning/libtuning/average.py b/utils/tuning/libtuning/average.py
index e28770d..c41075a 100644
--- a/utils/tuning/libtuning/average.py
+++ b/utils/tuning/libtuning/average.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# average.py - Wrapper for numpy averaging functions to enable duck-typing
+# Wrapper for numpy averaging functions to enable duck-typing
 
 import numpy as np
 
diff --git a/utils/tuning/libtuning/generators/generator.py b/utils/tuning/libtuning/generators/generator.py
index 7c8c9b9..77a8ba4 100644
--- a/utils/tuning/libtuning/generators/generator.py
+++ b/utils/tuning/libtuning/generators/generator.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# generator.py - Base class for a generator to convert dict to tuning file
+# Base class for a generator to convert dict to tuning file
 
 from pathlib import Path
 
diff --git a/utils/tuning/libtuning/generators/raspberrypi_output.py b/utils/tuning/libtuning/generators/raspberrypi_output.py
index 813491c..47b4905 100644
--- a/utils/tuning/libtuning/generators/raspberrypi_output.py
+++ b/utils/tuning/libtuning/generators/raspberrypi_output.py
@@ -2,7 +2,7 @@
 #
 # Copyright 2022 Raspberry Pi Ltd
 #
-# raspberrypi_output.py - Generate tuning file in Raspberry Pi's json format
+# Generate tuning file in Raspberry Pi's json format
 #
 # (Copied from ctt_pretty_print_json.py)
 
diff --git a/utils/tuning/libtuning/generators/yaml_output.py b/utils/tuning/libtuning/generators/yaml_output.py
index effb4fb..8f22d38 100644
--- a/utils/tuning/libtuning/generators/yaml_output.py
+++ b/utils/tuning/libtuning/generators/yaml_output.py
@@ -2,7 +2,7 @@
 #
 # Copyright 2022 Paul Elder <paul.elder@ideasonboard.com>
 #
-# yaml_output.py - Generate tuning file in YAML format
+# Generate tuning file in YAML format
 
 from .generator import Generator
 
diff --git a/utils/tuning/libtuning/gradient.py b/utils/tuning/libtuning/gradient.py
index 5106f82..b643f50 100644
--- a/utils/tuning/libtuning/gradient.py
+++ b/utils/tuning/libtuning/gradient.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# gradient.py - Gradients that can be used to distribute or map numbers
+# Gradients that can be used to distribute or map numbers
 
 import libtuning as lt
 
diff --git a/utils/tuning/libtuning/image.py b/utils/tuning/libtuning/image.py
index aa9d20b..6ff60ec 100644
--- a/utils/tuning/libtuning/image.py
+++ b/utils/tuning/libtuning/image.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# image.py - Container for an image and associated metadata
+# Container for an image and associated metadata
 
 import binascii
 import numpy as np
@@ -79,7 +79,7 @@
         # is R, then G, then G, then B.
         bayer_case = {
             '0 1 1 2': (lt.Color.R, lt.Color.GR, lt.Color.GB, lt.Color.B),
-            '1 2 0 1': (lt.Color.GB, lt.Color.R, lt.Color.B, lt.Color.GR),
+            '1 2 0 1': (lt.Color.GB, lt.Color.B, lt.Color.R, lt.Color.GR),
             '2 1 1 0': (lt.Color.B, lt.Color.GB, lt.Color.GR, lt.Color.R),
             '1 0 2 1': (lt.Color.GR, lt.Color.R, lt.Color.B, lt.Color.GB)
         }
diff --git a/utils/tuning/libtuning/libtuning.py b/utils/tuning/libtuning/libtuning.py
index d84c148..5e22288 100644
--- a/utils/tuning/libtuning/libtuning.py
+++ b/utils/tuning/libtuning/libtuning.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# libtuning.py - An infrastructure for camera tuning tools
+# An infrastructure for camera tuning tools
 
 import argparse
 
diff --git a/utils/tuning/libtuning/macbeth.py b/utils/tuning/libtuning/macbeth.py
index 5faddf6..e118246 100644
--- a/utils/tuning/libtuning/macbeth.py
+++ b/utils/tuning/libtuning/macbeth.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2019, Raspberry Pi Ltd
 #
-# macbeth.py - Locate and extract Macbeth charts from images
+# Locate and extract Macbeth charts from images
 # (Copied from: ctt_macbeth_locator.py)
 
 # \todo Add debugging
diff --git a/utils/tuning/libtuning/modules/agc/__init__.py b/utils/tuning/libtuning/modules/agc/__init__.py
new file mode 100644
index 0000000..4db9ca3
--- /dev/null
+++ b/utils/tuning/libtuning/modules/agc/__init__.py
@@ -0,0 +1,6 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+
+from libtuning.modules.agc.agc import AGC
+from libtuning.modules.agc.rkisp1 import AGCRkISP1
diff --git a/utils/tuning/libtuning/modules/agc/agc.py b/utils/tuning/libtuning/modules/agc/agc.py
new file mode 100644
index 0000000..9c8899b
--- /dev/null
+++ b/utils/tuning/libtuning/modules/agc/agc.py
@@ -0,0 +1,21 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+# Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+
+from ..module import Module
+
+import libtuning as lt
+
+
+class AGC(Module):
+    type = 'agc'
+    hr_name = 'AGC (Base)'
+    out_name = 'GenericAGC'
+
+    # \todo Add sector shapes and stuff just like lsc
+    def __init__(self, *,
+                 debug: list):
+        super().__init__()
+
+        self.debug = debug
diff --git a/utils/tuning/libtuning/modules/agc/rkisp1.py b/utils/tuning/libtuning/modules/agc/rkisp1.py
new file mode 100644
index 0000000..19a5555
--- /dev/null
+++ b/utils/tuning/libtuning/modules/agc/rkisp1.py
@@ -0,0 +1,79 @@
+# SPDX-License-Identifier: BSD-2-Clause
+#
+# Copyright (C) 2019, Raspberry Pi Ltd
+# Copyright (C) 2024, Paul Elder <paul.elder@ideasonboard.com>
+#
+# rkisp1.py - AGC module for tuning rkisp1
+
+from .agc import AGC
+
+import libtuning as lt
+
+
+class AGCRkISP1(AGC):
+    hr_name = 'AGC (RkISP1)'
+    out_name = 'Agc'
+
+    def __init__(self, **kwargs):
+        super().__init__(**kwargs)
+
+    # We don't actually need anything from the config file
+    def validate_config(self, config: dict) -> bool:
+        return True
+
+    def _generate_metering_modes(self) -> dict:
+        centre_weighted = [
+            0, 0,  0, 0, 0,
+            0, 6,  8, 6, 0,
+            0, 8, 16, 8, 0,
+            0, 6,  8, 6, 0,
+            0, 0,  0, 0, 0
+        ]
+
+        spot = [
+            0, 0,  0, 0, 0,
+            0, 2,  4, 2, 0,
+            0, 4, 16, 4, 0,
+            0, 2,  4, 2, 0,
+            0, 0,  0, 0, 0
+        ]
+
+        matrix = [1 for i in range(0, 25)]
+
+        return {
+            'MeteringCentreWeighted': centre_weighted,
+            'MeteringSpot': spot,
+            'MeteringMatrix': matrix
+        }
+
+    def _generate_exposure_modes(self) -> dict:
+        normal = {'shutter': [100, 10000, 30000, 60000, 120000],
+                  'gain': [2.0, 4.0, 6.0, 6.0, 6.0]}
+        short = {'shutter': [100, 5000, 10000, 20000, 120000],
+                 'gain': [2.0, 4.0, 6.0, 6.0, 6.0]}
+
+        return {'ExposureNormal': normal, 'ExposureShort': short}
+
+    def _generate_constraint_modes(self) -> dict:
+        normal = {'lower': {'qLo': 0.98, 'qHi': 1.0, 'yTarget': 0.5}}
+        highlight = {
+            'lower': {'qLo': 0.98, 'qHi': 1.0, 'yTarget': 0.5},
+            'upper': {'qLo': 0.98, 'qHi': 1.0, 'yTarget': 0.8}
+        }
+
+        return {'ConstraintNormal': normal, 'ConstraintHighlight': highlight}
+
+    def _generate_y_target(self) -> list:
+        return 0.16
+
+    def process(self, config: dict, images: list, outputs: dict) -> dict:
+        output = {}
+
+        output['AeMeteringMode'] = self._generate_metering_modes()
+        output['AeExposureMode'] = self._generate_exposure_modes()
+        output['AeConstraintMode'] = self._generate_constraint_modes()
+        output['relativeLuminanceTarget'] = self._generate_y_target()
+
+        # \todo Debug functionality
+
+        return output
diff --git a/utils/tuning/libtuning/modules/lsc/raspberrypi.py b/utils/tuning/libtuning/modules/lsc/raspberrypi.py
index 58f5000..f19c716 100644
--- a/utils/tuning/libtuning/modules/lsc/raspberrypi.py
+++ b/utils/tuning/libtuning/modules/lsc/raspberrypi.py
@@ -3,7 +3,7 @@
 # Copyright (C) 2019, Raspberry Pi Ltd
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# raspberrypi.py - ALSC module for tuning Raspberry Pi
+# ALSC module for tuning Raspberry Pi
 
 from .lsc import LSC
 
diff --git a/utils/tuning/libtuning/modules/lsc/rkisp1.py b/utils/tuning/libtuning/modules/lsc/rkisp1.py
index 5701ae0..20406e4 100644
--- a/utils/tuning/libtuning/modules/lsc/rkisp1.py
+++ b/utils/tuning/libtuning/modules/lsc/rkisp1.py
@@ -3,7 +3,7 @@
 # Copyright (C) 2019, Raspberry Pi Ltd
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# rkisp1.py - LSC module for tuning rkisp1
+# LSC module for tuning rkisp1
 
 from .lsc import LSC
 
diff --git a/utils/tuning/libtuning/modules/module.py b/utils/tuning/libtuning/modules/module.py
index 12e2fc7..de62438 100644
--- a/utils/tuning/libtuning/modules/module.py
+++ b/utils/tuning/libtuning/modules/module.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# module.py - Base class for algorithm-specific tuning modules
+# Base class for algorithm-specific tuning modules
 
 
 # @var type Type of the module. Defined in the base module.
diff --git a/utils/tuning/libtuning/parsers/parser.py b/utils/tuning/libtuning/parsers/parser.py
index a17d8d7..0c3944c 100644
--- a/utils/tuning/libtuning/parsers/parser.py
+++ b/utils/tuning/libtuning/parsers/parser.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# parser.py - Base class for a parser for a specific format of config file
+# Base class for a parser for a specific format of config file
 
 class Parser(object):
     def __init__(self):
diff --git a/utils/tuning/libtuning/parsers/raspberrypi_parser.py b/utils/tuning/libtuning/parsers/raspberrypi_parser.py
index d26586b..f1da459 100644
--- a/utils/tuning/libtuning/parsers/raspberrypi_parser.py
+++ b/utils/tuning/libtuning/parsers/raspberrypi_parser.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# raspberrypi_parser.py - Parser for Raspberry Pi config file format
+# Parser for Raspberry Pi config file format
 
 from .parser import Parser
 
diff --git a/utils/tuning/libtuning/parsers/yaml_parser.py b/utils/tuning/libtuning/parsers/yaml_parser.py
index 5c1673a..244db24 100644
--- a/utils/tuning/libtuning/parsers/yaml_parser.py
+++ b/utils/tuning/libtuning/parsers/yaml_parser.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# yaml_parser.py - Parser for YAML format config file
+# Parser for YAML format config file
 
 from .parser import Parser
 
diff --git a/utils/tuning/libtuning/smoothing.py b/utils/tuning/libtuning/smoothing.py
index b8a5a24..de4d920 100644
--- a/utils/tuning/libtuning/smoothing.py
+++ b/utils/tuning/libtuning/smoothing.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# smoothing.py - Wrapper for cv2 smoothing functions to enable duck-typing
+# Wrapper for cv2 smoothing functions to enable duck-typing
 
 import cv2
 
diff --git a/utils/tuning/libtuning/utils.py b/utils/tuning/libtuning/utils.py
index b60f2c9..1e8128e 100644
--- a/utils/tuning/libtuning/utils.py
+++ b/utils/tuning/libtuning/utils.py
@@ -3,7 +3,7 @@
 # Copyright (C) 2019, Raspberry Pi Ltd
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# utils.py - Utilities for libtuning
+# Utilities for libtuning
 
 import decimal
 import math
diff --git a/utils/tuning/raspberrypi/alsc.py b/utils/tuning/raspberrypi/alsc.py
index 024eb5a..ba8fc9e 100644
--- a/utils/tuning/raspberrypi/alsc.py
+++ b/utils/tuning/raspberrypi/alsc.py
@@ -2,7 +2,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# alsc.py - ALSC module instance for Raspberry Pi tuning scripts
+# ALSC module instance for Raspberry Pi tuning scripts
 
 import libtuning as lt
 from libtuning.modules.lsc import ALSCRaspberryPi
diff --git a/utils/tuning/raspberrypi_alsc_only.py b/utils/tuning/raspberrypi_alsc_only.py
index af04e6a..777d800 100755
--- a/utils/tuning/raspberrypi_alsc_only.py
+++ b/utils/tuning/raspberrypi_alsc_only.py
@@ -3,7 +3,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# raspberrypi_alsc_only.py - Tuning script for raspberrypi, ALSC only
+# Tuning script for raspberrypi, ALSC only
 
 import sys
 
diff --git a/utils/tuning/rkisp1.py b/utils/tuning/rkisp1.py
index 1cea6dd..d0ce15d 100755
--- a/utils/tuning/rkisp1.py
+++ b/utils/tuning/rkisp1.py
@@ -3,7 +3,7 @@
 #
 # Copyright (C) 2022, Paul Elder <paul.elder@ideasonboard.com>
 #
-# rkisp1.py - Tuning script for rkisp1
+# Tuning script for rkisp1
 
 import sys
 
@@ -11,6 +11,7 @@
 from libtuning.parsers import YamlParser
 from libtuning.generators import YamlOutput
 from libtuning.modules.lsc import LSCRkISP1
+from libtuning.modules.agc import AGCRkISP1
 
 tuner = lt.Tuner('RkISP1')
 tuner.add(LSCRkISP1(
@@ -32,9 +33,10 @@
           # values.  This can also be a custom function.
           smoothing_function=lt.smoothing.MedianBlur(3),
           ))
+tuner.add(AGCRkISP1(debug=[lt.Debug.Plot]))
 tuner.set_input_parser(YamlParser())
 tuner.set_output_formatter(YamlOutput())
-tuner.set_output_order([LSCRkISP1])
+tuner.set_output_order([AGCRkISP1, LSCRkISP1])
 
 if __name__ == '__main__':
     sys.exit(tuner.run(sys.argv))
diff --git a/utils/update-kernel-headers.sh b/utils/update-kernel-headers.sh
index a006452..8405be0 100755
--- a/utils/update-kernel-headers.sh
+++ b/utils/update-kernel-headers.sh
@@ -18,7 +18,7 @@
 	exit 1
 fi
 
-if [ ! -d "${kernel_dir}/.git" ] ; then
+if [ ! -e "${kernel_dir}/.git" ] ; then
 	echo "Directory ${kernel_dir} doesn't contain a git tree"
 	exit 1
 fi
@@ -52,6 +52,7 @@
 	linux/media-bus-format.h
 	linux/media.h
 	linux/rkisp1-config.h
+	linux/udmabuf.h
 	linux/v4l2-common.h
 	linux/v4l2-controls.h
 	linux/v4l2-mediabus.h
diff --git a/utils/update-mojo.sh b/utils/update-mojo.sh
index fcbc81e..09c8ff5 100755
--- a/utils/update-mojo.sh
+++ b/utils/update-mojo.sh
@@ -3,13 +3,23 @@
 # SPDX-License-Identifier: GPL-2.0-or-later
 # Update mojo copy from a chromium source tree
 
+set -e
+
 if [ $# != 1 ] ; then
 	echo "Usage: $0 <chromium dir>"
 	exit 1
 fi
 
 ipc_dir="$(dirname "$(realpath "$0")")/ipc"
-chromium_dir="$1"
+chromium_dir="$(realpath "$1")"
+
+cd "${ipc_dir}/../../"
+
+# Reject dirty libcamera trees
+if [ -n "$(git status --porcelain -uno)" ] ; then
+	echo "libcamera tree is dirty"
+	exit 1
+fi
 
 if [ ! -d "${chromium_dir}/mojo" ] ; then
 	echo "Directory ${chromium_dir} doesn't contain mojo"
@@ -24,19 +34,23 @@
 # Get the chromium commit id
 version=$(git -C "${chromium_dir}" rev-parse --short HEAD)
 
-# Reject dirty trees
+# Reject dirty chromium trees
 if [ -n "$(git -C "${chromium_dir}" status --porcelain)" ] ; then
 	echo "Chromium tree in ${chromium_dir} is dirty"
 	exit 1
 fi
 
+# Remove the previously imported files.
+rm -rf utils/ipc/mojo/
+rm -rf utils/ipc/tools/
+
 # Copy the diagnosis file
-cp "${chromium_dir}/tools/diagnosis/crbug_1001171.py" "${ipc_dir}/tools/diagnosis"
+mkdir -p utils/ipc/tools/diagnosis/
+cp "${chromium_dir}/tools/diagnosis/crbug_1001171.py" utils/ipc/tools/diagnosis/
 
 # Copy the rest of mojo
-cp "${chromium_dir}/mojo/public/LICENSE" "${ipc_dir}/mojo/public"
-
-rm -rf "${ipc_dir}/mojo/public/tools/*"
+mkdir -p utils/ipc/mojo/public/
+cp "${chromium_dir}/mojo/public/LICENSE" utils/ipc/mojo/public/
 
 (
 	cd "${chromium_dir}" || exit
@@ -55,12 +69,22 @@
 EOF
 )
 
-echo "$readme" > "${ipc_dir}/mojo/README"
-echo "$readme" > "${ipc_dir}/tools/README"
+echo "$readme" > utils/ipc/mojo/README
+echo "$readme" > utils/ipc/tools/README
 
-cat <<EOF
-------------------------------------------------------------
-mojo updated. Please review and up-port local changes before
-committing.
-------------------------------------------------------------
-EOF
+# Commit the update. Use 'git commit -n' to avoid checkstyle pre-commit hook
+# failures, as mojo doesn't comply with the Python coding style enforced by
+# checkstyle.py.
+git add utils/ipc/mojo/
+git add utils/ipc/tools/
+
+echo "utils: ipc: Update mojo
+
+Update mojo from commit
+
+$(git -C "${chromium_dir}" show --pretty='%H "%s"' --no-patch)
+
+from the Chromium repository.
+
+The update-mojo.sh script was used for this update." | \
+git commit -n -s -F -