summaryrefslogtreecommitdiff
path: root/subprojects
diff options
context:
space:
mode:
Diffstat (limited to 'subprojects')
-rw-r--r--subprojects/pixpat/.github/workflows/ci.yml86
-rw-r--r--subprojects/pixpat/.gitignore9
-rw-r--r--subprojects/pixpat/LICENSE.md157
-rw-r--r--subprojects/pixpat/README.md502
-rw-r--r--subprojects/pixpat/meson.build102
-rw-r--r--subprojects/pixpat/meson.options4
-rw-r--r--subprojects/pixpat/pixpat-native/codegen/gen_pixpat.py267
-rw-r--r--subprojects/pixpat/pixpat-native/cross/aarch64-linux-gnu.txt11
-rw-r--r--subprojects/pixpat/pixpat-native/inc/pixpat/pixpat.h264
-rw-r--r--subprojects/pixpat/pixpat-native/profiles/no_hotpath.toml12
-rw-r--r--subprojects/pixpat/pixpat-native/profiles/pattern_only.toml11
-rw-r--r--subprojects/pixpat/pixpat-native/profiles/pixpat.toml31
-rw-r--r--subprojects/pixpat/pixpat-native/profiles/to_bgr888.toml17
-rw-r--r--subprojects/pixpat/pixpat-native/src/color.h199
-rw-r--r--subprojects/pixpat/pixpat-native/src/error.h16
-rw-r--r--subprojects/pixpat/pixpat-native/src/format_catalog.h140
-rw-r--r--subprojects/pixpat/pixpat-native/src/formats.h13
-rw-r--r--subprojects/pixpat/pixpat-native/src/formats/bayer.h97
-rw-r--r--subprojects/pixpat/pixpat-native/src/formats/grayscale.h78
-rw-r--r--subprojects/pixpat/pixpat-native/src/formats/rgb.h267
-rw-r--r--subprojects/pixpat/pixpat-native/src/formats/yuv_packed.h136
-rw-r--r--subprojects/pixpat/pixpat-native/src/formats/yuv_planar.h76
-rw-r--r--subprojects/pixpat/pixpat-native/src/formats/yuv_semiplanar.h79
-rw-r--r--subprojects/pixpat/pixpat-native/src/io.h13
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/bayer.h318
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/csi2.h80
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/detail.h62
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/gray.h153
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/gray_packed.h78
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/mono_rgb.h72
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/packed.h106
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/packed_yuv.h89
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/planar.h257
-rw-r--r--subprojects/pixpat/pixpat-native/src/io/semiplanar.h242
-rw-r--r--subprojects/pixpat/pixpat-native/src/layout.h141
-rw-r--r--subprojects/pixpat/pixpat-native/src/params.h219
-rw-r--r--subprojects/pixpat/pixpat-native/src/pattern.h597
-rw-r--r--subprojects/pixpat/pixpat-native/src/pattern_catalog.h64
-rw-r--r--subprojects/pixpat/pixpat-native/src/pipeline.h44
-rw-r--r--subprojects/pixpat/pixpat-native/src/pixpat.cpp355
-rw-r--r--subprojects/pixpat/pixpat-native/src/pixpat_convert.cpp201
-rw-r--r--subprojects/pixpat/pixpat-native/src/pixpat_internal.h89
-rw-r--r--subprojects/pixpat/pixpat-native/src/pixpat_pattern.cpp168
-rw-r--r--subprojects/pixpat/pixpat-native/src/threading.h95
-rw-r--r--subprojects/pixpat/pixpat-native/tests/meson.build11
-rw-r--r--subprojects/pixpat/pixpat-native/tests/test_pixpat.cpp107
-rw-r--r--subprojects/pixpat/pixpat-native/tests/test_pixpat_c.c68
-rw-r--r--subprojects/pixpat/pixpat-python/pixpat/__init__.py489
-rw-r--r--subprojects/pixpat/pixpat-python/pixpat/_lib/.gitkeep3
-rw-r--r--subprojects/pixpat/pixpat-python/pixpat/_native.py203
-rwxr-xr-xsubprojects/pixpat/pixpat-python/scripts/build_wheel.sh39
-rwxr-xr-xsubprojects/pixpat/pixpat-python/scripts/perf_test.py434
-rw-r--r--subprojects/pixpat/pixpat-python/tests/test_basic.py539
-rw-r--r--subprojects/pixpat/pixpat-python/tests/test_numpy.py110
-rw-r--r--subprojects/pixpat/pixpat-python/tests/test_threading.py187
-rw-r--r--subprojects/pixpat/pyproject.toml26
-rwxr-xr-xsubprojects/pixpat/scripts/build_profiles.sh135
-rwxr-xr-xsubprojects/pixpat/scripts/format-all.sh3
-rw-r--r--subprojects/pixpat/setup.py152
-rw-r--r--subprojects/pixpat/uncrustify.cfg3708
60 files changed, 12231 insertions, 0 deletions
diff --git a/subprojects/pixpat/.github/workflows/ci.yml b/subprojects/pixpat/.github/workflows/ci.yml
new file mode 100644
index 0000000..3eb168a
--- /dev/null
+++ b/subprojects/pixpat/.github/workflows/ci.yml
@@ -0,0 +1,86 @@
+name: CI
+
+on:
+ pull_request:
+ branches: [master]
+ push:
+ branches: [master, test]
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ lint:
+ name: lint (uncrustify + ruff)
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.x'
+
+ - name: Install uncrustify
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y uncrustify
+
+ - name: Install ruff
+ run: pip install ruff
+
+ - name: uncrustify --check
+ run: |
+ files=$(git ls-files '*.cpp' '*.h')
+ uncrustify -c uncrustify.cfg -l CPP --check $files
+
+ - name: ruff check
+ run: ruff check .
+
+ - name: ruff format --check
+ run: ruff format --check .
+
+ build:
+ name: build (${{ matrix.compiler }})
+ runs-on: ubuntu-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ compiler: [gcc, clang]
+ steps:
+ - uses: actions/checkout@v6
+
+ - uses: actions/setup-python@v6
+ with:
+ python-version: '3.x'
+
+ - name: Install build dependencies
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y meson ninja-build
+ if [ "${{ matrix.compiler }}" = "clang" ]; then
+ sudo apt-get install -y clang
+ fi
+
+ - name: Configure meson
+ run: |
+ if [ "${{ matrix.compiler }}" = "clang" ]; then
+ export CC=clang CXX=clang++
+ else
+ export CC=gcc CXX=g++
+ fi
+ meson setup build
+
+ - name: Compile
+ run: meson compile -C build
+
+ - name: Run native tests
+ run: meson test -C build --print-errorlogs
+
+ - name: Install pixpat wheel
+ run: pip install .
+
+ - name: Run pytest
+ run: |
+ pip install pytest
+ pytest pixpat-python/tests
diff --git a/subprojects/pixpat/.gitignore b/subprojects/pixpat/.gitignore
new file mode 100644
index 0000000..48c95cf
--- /dev/null
+++ b/subprojects/pixpat/.gitignore
@@ -0,0 +1,9 @@
+/build/
+/build-*/
+/pixpat-python/build/
+/pixpat-python/build-*/
+/dist/
+/wheelhouse/
+pixpat-python/pixpat/_lib/libpixpat.so*
+*.egg-info/
+__pycache__/
diff --git a/subprojects/pixpat/LICENSE.md b/subprojects/pixpat/LICENSE.md
new file mode 100644
index 0000000..6fb6a01
--- /dev/null
+++ b/subprojects/pixpat/LICENSE.md
@@ -0,0 +1,157 @@
+# GNU LESSER GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+<https://fsf.org/>
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+This version of the GNU Lesser General Public License incorporates the
+terms and conditions of version 3 of the GNU General Public License,
+supplemented by the additional permissions listed below.
+
+## 0. Additional Definitions.
+
+As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the
+GNU General Public License.
+
+"The Library" refers to a covered work governed by this License, other
+than an Application or a Combined Work as defined below.
+
+An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+## 1. Exception to Section 3 of the GNU GPL.
+
+You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+## 2. Conveying Modified Versions.
+
+If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+- a) under this License, provided that you make a good faith effort
+ to ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+- b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+## 3. Object Code Incorporating Material from Library Header Files.
+
+The object code form of an Application may incorporate material from a
+header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+- a) Give prominent notice with each copy of the object code that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+- b) Accompany the object code with a copy of the GNU GPL and this
+ license document.
+
+## 4. Combined Works.
+
+You may convey a Combined Work under terms of your choice that, taken
+together, effectively do not restrict modification of the portions of
+the Library contained in the Combined Work and reverse engineering for
+debugging such modifications, if you also do each of the following:
+
+- a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+- b) Accompany the Combined Work with a copy of the GNU GPL and this
+ license document.
+- c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+- d) Do one of the following:
+ - 0) Convey the Minimal Corresponding Source under the terms of
+ this License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+ - 1) Use a suitable shared library mechanism for linking with
+ the Library. A suitable mechanism is one that (a) uses at run
+ time a copy of the Library already present on the user's
+ computer system, and (b) will operate properly with a modified
+ version of the Library that is interface-compatible with the
+ Linked Version.
+- e) Provide Installation Information, but only if you would
+ otherwise be required to provide such information under section 6
+ of the GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the Application
+ with a modified version of the Linked Version. (If you use option
+ 4d0, the Installation Information must accompany the Minimal
+ Corresponding Source and Corresponding Application Code. If you
+ use option 4d1, you must provide the Installation Information in
+ the manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.)
+
+## 5. Combined Libraries.
+
+You may place library facilities that are a work based on the Library
+side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+- a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities, conveyed under the terms of this License.
+- b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+## 6. Revised Versions of the GNU Lesser General Public License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+as you received it specifies that a certain numbered version of the
+GNU Lesser General Public License "or any later version" applies to
+it, you have the option of following the terms and conditions either
+of that published version or of any later version published by the
+Free Software Foundation. If the Library as you received it does not
+specify a version number of the GNU Lesser General Public License, you
+may choose any version of the GNU Lesser General Public License ever
+published by the Free Software Foundation.
+
+If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
diff --git a/subprojects/pixpat/README.md b/subprojects/pixpat/README.md
new file mode 100644
index 0000000..c09e97e
--- /dev/null
+++ b/subprojects/pixpat/README.md
@@ -0,0 +1,502 @@
+# pixpat
+
+A small C++ library for **pixel format conversion** and **test pattern
+generation**, with a C API and Python bindings.
+
+## Why pixpat
+
+- **Templated C++ core.** Each pixel format is described once as a
+ *layout* — component order, bit widths, plane shape — and the
+ conversion code is generated from those descriptions by the C++
+ compiler. Adding a new format or component order is a few lines of
+ layout, not a new conversion routine.
+- **16-bit normalized pivot.** Conversions don't go format-to-format.
+ They unpack into a 16-bit RGB or YUV intermediate and pack from it,
+ so cross-color-kind conversions (RGB ↔ YUV, with arbitrary matrix
+ and range) cost the same as same-color-kind ones, precision is
+ preserved when both endpoints are 8-bit, and the work scales as N+M
+ instead of N×M.
+- **Built to drop into pipelines.** Caller-owned buffers and a
+ freestanding C++ core (`-fno-exceptions -fno-rtti`, no libstdc++
+ runtime dep) mean pixpat fits inside the inner loop of a DRM/KMS,
+ V4L2, or GPU-upload path without copies or runtime baggage.
+
+## Why not pixpat
+
+- **You need maximum throughput.** Hand-tuned conversion paths —
+ OpenCV with its SIMD intrinsics, vendor-supplied codecs, ffmpeg's
+ `swscale` — still outrun pixpat on the heavily-trafficked format
+ pairs. pixpat aims for "fast enough" out of a small generated
+ codebase, not for raw peak speed.
+- **You want a pure-Python library.** The Python package is a thin
+ `ctypes` wrapper around `libpixpat.so`. You need a native wheel
+ matching your architecture; there is no pure-Python fallback.
+- **You need GPU conversion.** pixpat is CPU-only.
+
+## What it offers
+
+- **Test patterns** — `kmstest`, SMPTE RP 219-1 bars, `plain` solid
+ fill, R/G/B/gray ramps, checkerboard, color-bar overlays, zone
+ plate. All take a single per-pattern parameter string.
+- **Format conversion** between packed / semiplanar / planar YUV,
+ RGB, raw Bayer, and grayscale. Cross-color-kind conversions
+ (RGB ↔ YUV) honor BT.601 / BT.709 / BT.2020 matrices and
+ limited / full quantization range.
+- **Caller-owned pixel memory.** Callers pass plane pointers and
+ strides; pixpat never allocates pixel buffers. The only internal
+ allocation is a small per-thread normalized line buffer reused
+ across cold-path calls.
+- **DRM / kms++ / pixutils format names** (`XRGB8888`, `NV12`, …)
+ rather than DRM/V4L2 four-character codes (`XR24`, …). See
+ [Format names and byte order](#format-names-and-byte-order) — the
+ convention disagrees with OpenCV's.
+- **Optional multi-threading** via a single `num_threads` knob on
+ both entry points.
+- **Build-time tailoring.** A small TOML config selects which formats
+ and patterns are compiled in, which formats are read-only /
+ write-only, and which formats get a fully-fused fast path.
+
+## Supported formats
+
+The default build ships every format in the catalog. Each one works as
+a `pixpat_draw_pattern` target, a `pixpat_convert` source, and a
+`pixpat_convert` destination.
+
+- **RGB packed** — `RGB332`, `RGB565`, `BGR565`, the four 1555
+ permutations `{XRGB,ARGB,XBGR,ABGR}1555`, the six 4444 permutations
+ `{XRGB,ARGB,XBGR,ABGR,RGBX,RGBA}4444`, `RGB888`, `BGR888`, the
+ eight 8888 permutations `{XRGB,ARGB,XBGR,ABGR,RGBX,RGBA,BGRX,BGRA}8888`,
+ the eight 10-bit permutations `{XRGB,ARGB,XBGR,ABGR}2101010` and
+ `{RGBX,RGBA,BGRX,BGRA}1010102`, and `ABGR16161616`. `R8` is a
+ single-channel form; on read the unspecified channels are
+ synthesized as G=B=R.
+- **YUV packed** — `YUYV`, `YVYU`, `UYVY`, `VYUY`, `Y210`, `Y212`,
+ `Y216`, `VUY888`, `XVUY8888`, `XVUY2101010`, `AVUY16161616`.
+- **YUV semiplanar** — `NV12`, `NV21`, `NV16`, `NV61`, `P030`, `P230`.
+- **YUV planar** — `YUV444`, `YVU444`, `YUV422`, `YVU422`, `YUV420`,
+ `YVU420`, `T430`.
+- **Grayscale** — `Y8`, `Y10`, `Y12`, `Y16`, `XYYY2101010`, `Y10P`,
+ `Y12P`.
+- **Bayer unpacked** — `SRGGB` / `SBGGR` / `SGRBG` / `SGBRG` at
+ 8 / 10 / 12 / 16 bit. Reads use a bilinear demosaic.
+- **Bayer MIPI-packed** — the same four phases at 10P / 12P.
+
+### Format names and byte order
+
+Format names follow the DRM / kms++ / pixutils convention:
+components are listed **MSB-first** inside the storage word. So
+`XRGB8888` means a 32-bit word with X in the highest byte and B in
+the lowest, and `BGR888` is a 24-bit format with B at the highest
+byte and R at byte 0.
+
+OpenCV uses the **opposite** convention — its `BGR` is byte-order, so
+OpenCV `BGR` and pixpat `RGB888` describe the same in-memory layout.
+Keep this in mind when comparing pipelines.
+
+## Quickstart
+
+### C
+
+```c
+#include <pixpat/pixpat.h>
+
+uint8_t pixels[1920 * 1080 * 4];
+pixpat_buffer buf = {
+ .format = "XRGB8888",
+ .width = 1920,
+ .height = 1080,
+ .num_planes = 1,
+ .planes = { pixels },
+ .strides = { 1920 * 4 },
+};
+
+pixpat_draw_pattern(&buf, "smpte", NULL); /* NULL opts → BT.601 / limited / auto threads */
+```
+
+A `pixpat_convert(dst, src, opts)` call has the same shape: two
+`pixpat_buffer`s plus a small options struct (also nullable). See the
+public header at `pixpat-native/inc/pixpat/pixpat.h`.
+
+### Python
+
+```python
+import pixpat
+
+w, h = 1920, 1080
+data = bytearray(w * h * 4)
+buf = pixpat.Buffer(planes=[data], fmt="XRGB8888",
+ width=w, height=h, strides=[w * 4])
+
+pixpat.draw_pattern(buf, "smpte")
+```
+
+The Python `Buffer` accepts anything that supports the buffer protocol
+— `bytearray`, `array.array`, `numpy.ndarray`, `mmap.mmap`,
+`memoryview`. Source buffers may be read-only; destination buffers
+must be writable.
+
+## Components
+
+- **`libpixpat`** — the C++ implementation, exposed through a small
+ C ABI in [`pixpat-native/inc/pixpat/pixpat.h`](pixpat-native/inc/pixpat/pixpat.h).
+ Built with Meson; produces both shared and static libraries plus a
+ pkg-config file.
+- **`pixpat` (Python)** — thin `ctypes` bindings over the C ABI. No
+ CPython extension, so a single wheel works on any CPython ≥ 3.9 for
+ a given architecture.
+
+## Building
+
+`libpixpat` is built with [Meson](https://mesonbuild.com/). A C++20
+compiler and Python 3 (used by the build-time codegen) are required;
+there are no third-party runtime dependencies.
+
+```sh
+meson setup build
+meson compile -C build
+```
+
+This produces `build/libpixpat.so` (and `.a`), the public header at
+`pixpat-native/inc/pixpat/pixpat.h`, and a `pixpat.pc` pkg-config
+file. To install system-wide:
+
+```sh
+meson install -C build
+```
+
+### Cross-compiling
+
+A cross file for aarch64 Linux ships in the tree:
+
+```sh
+meson setup build-aarch64 --cross-file pixpat-native/cross/aarch64-linux-gnu.txt
+meson compile -C build-aarch64
+```
+
+### Native tests
+
+```sh
+meson test -C build
+```
+
+These are smoke tests that exercise the public ABI from C and C++.
+Behavioral coverage (matrix correctness, threading, subsampling,
+Bayer demosaic, …) lives in the Python test suite.
+
+### Selecting a build profile
+
+The default profile compiles every format and pattern. To pick a
+different one, point Meson at an alternate TOML file via the `config`
+option:
+
+```sh
+meson setup build-min -Dconfig=pixpat-native/profiles/no_hotpath.toml
+```
+
+A few example profiles ship in `pixpat-native/profiles/`. See
+[Build-time configuration and codegen](#build-time-configuration-and-codegen)
+below for what the TOML controls.
+
+### Python install
+
+The Python package wraps the C ABI via `ctypes`, so installing it
+just means compiling `libpixpat.so` and bundling it as package data.
+For a native install:
+
+```sh
+pip install .
+```
+
+`setup.py` invokes meson during the wheel build (into
+`pixpat-python/build/native/`), copies the resulting `.so` into the
+package, and stamps the wheel for the host architecture. Requires
+`meson`, `ninja`, and a C++ compiler on the host.
+
+To cross-compile a wheel for another architecture, use the helper:
+
+```sh
+pixpat-python/scripts/build_wheel.sh x86_64 # or aarch64
+```
+
+The resulting wheel lands in `dist/`, tagged for the chosen
+architecture; meson's per-arch build dir lands at
+`pixpat-python/build-<arch>/native/`.
+
+### Editable Python install for development
+
+```sh
+meson setup build
+meson compile -C build
+pip install -e .
+```
+
+The editable install symlinks `build/libpixpat.so.0.0.0` into the
+package, so rebuilding the native side is picked up without
+re-installing.
+
+### Python tests
+
+From the repo root, after an editable install:
+
+```sh
+pytest pixpat-python/tests
+```
+
+For micro-benchmarking the `draw_pattern` and `convert` paths across
+formats, see `pixpat-python/scripts/perf_test.py`. This is a
+development tool, not part of the supported API surface.
+
+## Architecture
+
+The rest of this document covers how `libpixpat` is put together
+internally: how a conversion is structured, how formats are
+described, how the build is configured, and the supported compiler
+and runtime.
+
+### Conversion pipeline
+
+A conversion is the composition of three stages:
+
+```
+Source → ColorXfm → Sink
+```
+
+- A **source** unpacks caller-memory pixels into a **normalized
+ pixel** — `RGB16` or `YUV16`, four `uint16_t` components.
+- A **ColorXfm** maps one normalized pixel type to another: identity
+ for same-color-kind conversions, the selected matrix/range for
+ cross-color-kind ones. Template-specialized, so the identity case
+ vanishes at compile time.
+- A **sink** packs normalized pixels into destination memory.
+
+Each sink declares a `block_h × block_w` block matching its chroma
+subsampling (1×1 for unsubsampled, e.g. 2×2 for `NV12`). The
+converter materializes one block on the stack per iteration; under
+`-O3` it stays in registers for most sinks.
+
+#### Hot path vs cold path
+
+The normalized pixel type does double duty:
+
+- On the **hot path**, with `-O3`, the compiler keeps it in registers
+ across the source / ColorXfm / sink boundary — no per-line buffer
+ is involved.
+- On the **cold path**, two short legs — *unpack to norm* and
+ *pack from norm* — share a per-thread normalized line buffer.
+ Each leg is a templated function: one body per source, one per
+ sink. Cross-color-kind conversions add an in-place ColorXfm pass over
+ the buffer between the two legs.
+
+Whether a particular conversion runs on the hot or cold path is
+decided by the dispatch tier described in [Two-tier
+dispatch](#two-tier-dispatch).
+
+### Layout descriptor
+
+A pixel format is described once, declaratively, as a C++20
+non-type-template-parameter (NTTP) value. Three small types are
+enough to describe any format:
+
+```cpp
+enum class C : uint8_t { X, A, R, G, B, Y, U, V };
+
+struct Comp { C c; uint8_t bits; uint8_t shift; };
+
+template <typename Storage, Comp... Cs>
+struct Plane;
+
+template <ColorKind Kind, size_t Hsub, size_t Vsub, typename... Planes>
+struct Layout;
+```
+
+A `Plane` describes one storage word (`uint32_t`, `uint16_t`, …) and
+the components packed into it at given bit offsets. A `Layout` lists
+the planes, the color kind (RGB or YUV), and the chroma subsampling
+factors. Two named formats for comparison:
+
+```cpp
+using XRGB8888 = Layout<ColorKind::RGB, 1, 1,
+ Plane<uint32_t, Comp{C::B,8,0}, Comp{C::G,8,8},
+ Comp{C::R,8,16}, Comp{C::X,8,24}>>;
+
+using NV12 = Layout<ColorKind::YUV, 2, 2,
+ Plane<uint8_t, Comp{C::Y,8,0}>,
+ Plane<uint16_t, Comp{C::U,8,0}, Comp{C::V,8,8}>>;
+```
+
+`Plane` exposes `constexpr` helpers — `find_pos<C>`, `pack(values)`,
+`unpack(word)`, `bytes_per_pixel`, … — that the I/O templates use to
+emit per-format read and write code.
+
+### Patterns
+
+A pattern is a synthetic source: a small C++ struct exposing
+`sample(x, y, W, H)` that returns one normalized pixel. The list of
+supported names and their parameters is in
+[`pixpat-native/inc/pixpat/pixpat.h`](pixpat-native/inc/pixpat/pixpat.h).
+
+Dispatch is intentionally simple: every (pattern, sink) pair takes
+one normalized-pivot path. Per-pattern fill writes the normalized
+line buffer in the destination's color kind — folding the
+cross-color-kind `ColorXfm` into the per-pixel fill so that constant
+patterns collapse to `memset` under `-O3`. Per-sink pack encodes the
+line into the destination memory layout. Total cost is *O(N + M)*:
+adding a pattern is one fill specialization, adding a format is
+automatic.
+
+The SMPTE pattern's pixel values are spec-defined in BT.709 /
+Limited. Other rec/range settings are accepted but produce
+visibly-wrong colors — pixpat does not silently override the caller's
+color spec.
+
+`pixpat_pattern_opts::params` is parsed once at the C entry point
+into a `Params` instance, then handed to the pattern constructor;
+patterns query keys by name and never see raw strings. The Python
+wrapper accepts either the wire string or a `Mapping[str, Any]`.
+
+### Two-tier dispatch
+
+The naïve approach — instantiate `Converter<Source, Sink>` for every
+source/sink pair — produces an N×N matrix of fully-fused inner loops.
+With this many formats the binary balloons quickly. So pixpat splits
+the conversion table into two tiers.
+
+**Hot pivot.** A small set of pivot formats gets the fully-fused
+treatment. Real-world conversion paths almost always have a common
+interchange format on at least one endpoint — OpenCV, Qt,
+framebuffers, and video pipelines all gravitate toward a single
+8-bit RGB form. Picking that form as the pivot makes the typical
+user path fully inlined; the rarer hardware-to-hardware path stays
+on the cold path. The default profile uses **`BGR888`** as its
+single pivot, the format OpenCV, Qt, and framebuffers all use. Each
+pivot covers itself as both source and destination, paired with
+every other format.
+
+**Cold path.** Every other source/sink pair walks each row group
+through the per-thread normalized line buffer:
+
+```
+for each row group:
+ unpack_to_norm<Source>(buf, src, ...)
+ if RGB ↔ YUV:
+ in-place ColorXfm pass over buf
+ pack_from_norm<Sink>(dst, buf, ...)
+```
+
+`unpack_to_norm<Source>` and `pack_from_norm<Sink>` are plain
+templated functions — one body per source, one per sink, not per
+pair. With ~2N legs plus two cross-color-kind helpers, the cold path fits
+in a small fixed code budget independent of the number of pairs.
+
+Adding a hot pivot is mechanical (one entry in the build config —
+see the next section). Whether a pivot is worth the code size
+depends on whether real workloads actually use that format on one
+endpoint, so the choice is workload-driven.
+
+### Source / sink shapes
+
+Internally the I/O templates are grouped by *iteration shape*. Every
+format reuses one of these template shapes, plus its own `Layout`:
+
+- **Packed** (RGB or YUV) — `XRGB8888`, `BGR888`, …
+- **Packed-YUV** — `YUYV` group.
+- **Semiplanar** — `NV12` group.
+- **Multi-pixel semiplanar** — `P030`, `P230`. Sink uses the
+ streaming entry point.
+- **Planar** — `YUV420`, `YUV444`, …
+- **Multi-pixel planar** — `T430`.
+- **Gray** — single-component YUV; chroma synthesized at neutral on
+ read.
+- **Mono RGB** — RGB counterpart of Gray (`R8`); G=B=R synthesized on
+ read.
+- **Multi-pixel gray** — `XYYY2101010`.
+- **Gray MIPI-packed** — `Y10P`, `Y12P`.
+- **Bayer** — phase-aware R/G/B selection. Reads use a 3×3 bilinear
+ demosaic; edges clamp.
+- **Bayer MIPI-packed** — the 10P / 12P byte layout, hand-rolled
+ because the bit packing doesn't fit `Plane<Storage, Comp...>`.
+
+Adding a new format usually means one of: writing a new `Layout` and
+reusing one of these templates verbatim; adding a new layout shape to
+an existing template group; or, rarely, adding a new template group.
+
+### Threading
+
+A worker fan-out helper splits the image into row stripes — one
+disjoint `[start, end)` row range per worker — and runs the same
+converter body on each stripe. Stripe boundaries are aligned to the
+destination's vertical subsampling so chroma blocks aren't split
+across workers. Workers are joined before the call returns.
+
+`num_threads = 0` selects a sensible default (one per online CPU,
+capped); `1` runs the conversion inline on the calling thread with
+no thread-spawn overhead; `N > 1` uses exactly `N` workers.
+
+### Color math and bit depth
+
+Color math runs in `float`. Normalized components are `uint16_t`;
+N-bit stored values bit-replicate to 16 bits on decode (so `0xFF →
+0xFFFF`) and truncate via `norm >> (16 - N)` on encode. Per-component
+decode→encode at the same bit depth is exact; full conversions are
+not — they go through float color math and, where bit depths differ,
+truncation.
+
+Alpha rules:
+
+- Source without A → `a = 0`.
+- Same-color-kind ColorXfm → `a` unchanged.
+- Cross-color-kind ColorXfm → `a` reset to `0xFFFF`.
+- Sinks with A encode `a`; sinks with X write zero.
+
+### Build-time configuration and codegen
+
+Which formats and patterns are compiled in, which formats are
+read-only / write-only, and which formats get the fully-fused
+hot-pivot treatment are all decided at build time by a small TOML
+config. The default lives at `pixpat-native/profiles/pixpat.toml`; pick
+a different one with Meson's `-Dconfig=…` option.
+
+The TOML options:
+
+```toml
+hot_pivots = ["BGR888"] # which formats get fully-fused arms
+patterns = ["kmstest", "smpte"] # which patterns to compile in
+[features]
+pattern = true # toggle the draw-pattern entry point
+convert = true # toggle the convert entry point
+default_format_caps = "rw" # default per-format read/write
+[formats]
+# RGB888 = "r" # readable only
+# YUV420 = "off" # not in this build
+```
+
+At configure time, a Python codegen step reads the TOML and the format
+and pattern catalogs and emits two generated files:
+
+- `pixpat_config.h` — the C-side feature flags.
+- `pixpat_caps.inc` — two parallel arrays, `FormatCaps[]` and
+ `PatternCaps[]`. Each entry carries booleans like `readable`,
+ `writable`, `hot_src`, `hot_dst` (formats) or `enabled` (patterns).
+
+The dispatch code reads those caps inside `if constexpr` guards, so a
+disabled format / pattern / hot arm produces no template
+instantiation at all — the corresponding code is never generated.
+This is how a constrained target shrinks the binary: turn off the
+patterns and formats it doesn't need; the rest disappears from the
+output.
+
+Three example profiles ship in `pixpat-native/profiles/`
+illustrating the knobs (no hot path, pattern-only, hot pivot moved
+to a different format).
+
+### Compiler
+
+Hot-path performance depends heavily on the inlined inner loop being
+auto-vectorized. CI builds and tests under both gcc and clang; both
+work, but performance differs:
+
+- Under clang 18 (built with `-O3 -march=native`) the templated inner
+ loops vectorize cleanly, the normalized pixel stays register-resident,
+ and constant patterns collapse to `memset`.
+- Under gcc 13 the same loops vectorize for some shapes but not others —
+ RGB→RGB in particular drops considerably.
diff --git a/subprojects/pixpat/meson.build b/subprojects/pixpat/meson.build
new file mode 100644
index 0000000..bd8d5e0
--- /dev/null
+++ b/subprojects/pixpat/meson.build
@@ -0,0 +1,102 @@
+project('pixpat', 'cpp',
+ default_options : [
+ 'cpp_std=c++20',
+ 'buildtype=debugoptimized',
+ # Strip asserts in release builds; keep them live in
+ # debug / debugoptimized (the project default) so deep-level
+ # invariant checks remain a debugging aid.
+ 'b_ndebug=if-release',
+ ],
+ version : '1.0.0',
+)
+
+python3 = find_program('python3')
+gen_pixpat = files('pixpat-native/codegen/gen_pixpat.py')
+format_catalog_h = files('pixpat-native/src/format_catalog.h')
+pattern_catalog_h = files('pixpat-native/src/pattern_catalog.h')
+config_toml = files(get_option('config'))
+
+generated = custom_target('pixpat-codegen',
+ input : config_toml,
+ output : [
+ 'pixpat_config.h',
+ 'pixpat_caps.inc',
+ ],
+ command : [
+ python3, gen_pixpat,
+ '--config', '@INPUT@',
+ '--format-catalog', format_catalog_h,
+ '--pattern-catalog', pattern_catalog_h,
+ '--out-config-h', '@OUTPUT0@',
+ '--out-caps-inc', '@OUTPUT1@',
+ ],
+ depend_files : [gen_pixpat, format_catalog_h, pattern_catalog_h],
+)
+
+# Ask the generator which features are enabled. Drives the conditional
+# source list (pixpat_pattern.cpp / pixpat_convert.cpp) and gates the
+# C/C++ smoke tests, which assume both APIs work.
+feature_pattern = run_command(
+ python3, gen_pixpat,
+ '--config', config_toml,
+ '--format-catalog', format_catalog_h,
+ '--pattern-catalog', pattern_catalog_h,
+ '--query', 'feature_pattern',
+ check : true,
+).stdout().strip() == '1'
+
+feature_convert = run_command(
+ python3, gen_pixpat,
+ '--config', config_toml,
+ '--format-catalog', format_catalog_h,
+ '--pattern-catalog', pattern_catalog_h,
+ '--query', 'feature_convert',
+ check : true,
+).stdout().strip() == '1'
+
+have_both = feature_pattern and feature_convert
+
+pixpat_sources = files([
+ 'pixpat-native/src/pixpat.cpp',
+])
+if feature_pattern
+ pixpat_sources += files(['pixpat-native/src/pixpat_pattern.cpp'])
+endif
+if feature_convert
+ pixpat_sources += files(['pixpat-native/src/pixpat_convert.cpp'])
+endif
+
+private_includes = include_directories(
+ 'pixpat-native/src',
+ 'pixpat-native/inc',
+)
+# Build-dir include for the generated pixpat_config.h and .inc files.
+config_includes = include_directories('.')
+public_includes = include_directories('pixpat-native/inc')
+
+pixpat_args = ['-fvisibility-inlines-hidden']
+
+libpixpat = both_libraries('pixpat',
+ pixpat_sources, generated,
+ install : true,
+ include_directories : [private_includes, config_includes],
+ cpp_args : pixpat_args,
+ gnu_symbol_visibility : 'hidden',
+ version : meson.project_version())
+
+libpixpat_dep = declare_dependency(include_directories : public_includes,
+ link_with : libpixpat.get_shared_lib())
+
+# Internal dep that embeds pixpat into the linking target, so consumers can
+# avoid a runtime libpixpat.so dep.
+libpixpat_static_dep = declare_dependency(include_directories : public_includes,
+ link_with : libpixpat.get_static_lib())
+
+install_headers('pixpat-native/inc/pixpat/pixpat.h', subdir : 'pixpat')
+
+pkg = import('pkgconfig')
+pkg.generate(libpixpat.get_shared_lib())
+
+if have_both
+ subdir('pixpat-native/tests')
+endif
diff --git a/subprojects/pixpat/meson.options b/subprojects/pixpat/meson.options
new file mode 100644
index 0000000..625f243
--- /dev/null
+++ b/subprojects/pixpat/meson.options
@@ -0,0 +1,4 @@
+option('config',
+ type : 'string',
+ value : 'pixpat-native/profiles/pixpat.toml',
+ description : 'Path (relative to project source root) to the pixpat codegen TOML config.')
diff --git a/subprojects/pixpat/pixpat-native/codegen/gen_pixpat.py b/subprojects/pixpat/pixpat-native/codegen/gen_pixpat.py
new file mode 100644
index 0000000..36fac3b
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/codegen/gen_pixpat.py
@@ -0,0 +1,267 @@
+#!/usr/bin/env python3
+"""pixpat code generator.
+
+Reads a small user TOML config plus the hand-written catalogs
+(format_catalog.h, pattern_catalog.h — both X-macro lists) and emits
+the per-build capability tables consumed by pixpat.cpp /
+pixpat_convert.cpp / pixpat_pattern.cpp.
+
+Outputs:
+ pixpat_config.h — PIXPAT_FEATURE_PATTERN / _CONVERT defines
+ pixpat_caps.inc — s_format_caps[] indexed by FormatId
+ s_pattern_caps[] indexed by PatternId
+
+The convert dispatch (dispatch_dst_convert / dispatch_src_convert /
+dispatch_convert) and pattern dispatch (try_pattern / try_default
+arms) are hand-written and consume the capability arrays via
+`if constexpr`.
+
+A --query mode prints 0/1 to stdout for use from meson.
+"""
+
+import argparse
+import re
+import sys
+import tomllib
+from pathlib import Path
+
+
+# === Catalog parsers =============================================================
+#
+# Both catalogs are hand-written X-macro lists; we parse them here so
+# that adding a format/pattern is a single-file edit. The macro form
+# is rigid enough that a simple regex over the body works.
+
+
+def _parse_x_macro(path: Path, macro_name: str, row_re: re.Pattern) -> list:
+ """Find `#define <macro_name>(X) ...` and return row_re's captures.
+
+ The macro body extends from the `#define` line through the first
+ line that does not end with a backslash.
+ """
+ lines = path.read_text().splitlines()
+ header_re = re.compile(rf'#define\s+{macro_name}\s*\(\s*X\s*\)')
+ i = 0
+ while i < len(lines) and not header_re.search(lines[i]):
+ i += 1
+ if i == len(lines):
+ raise SystemExit(f'{macro_name} not found in {path}')
+ body_lines: list[str] = []
+ while True:
+ body_lines.append(lines[i])
+ if not lines[i].rstrip().endswith('\\'):
+ break
+ i += 1
+ if i == len(lines):
+ break
+ rows = row_re.findall('\n'.join(body_lines))
+ if not rows:
+ raise SystemExit(f'{macro_name} is empty in {path}')
+ return rows
+
+
+_FORMAT_ROW = re.compile(r'X\(\s*(\w+)\s*\)')
+_PATTERN_ROW = re.compile(r'X\(\s*(\w+)\s*,\s*(\w+)\s*,\s*(\w+)\s*,\s*"([^"]+)"\s*\)')
+
+
+def parse_format_catalog(path: Path) -> list[str]:
+ return _parse_x_macro(path, 'PIXPAT_FORMAT_LIST', _FORMAT_ROW)
+
+
+def parse_pattern_catalog(path: Path) -> list[tuple[str, str, str, str]]:
+ """Return [(label, rgb_type, yuv_type, name), …] from pattern_catalog.h.
+
+ `label` is the C++ identifier doubling as the PatternId enum value
+ and the s_pattern_caps[] index. `rgb_type` and `yuv_type` are the
+ C++ classes implementing the pattern in each color kind, or the
+ sentinel `void` if the pattern has no variant in that kind.
+ `name` is the lowercase string exposed via the C ABI.
+ """
+ return _parse_x_macro(path, 'PIXPAT_PATTERN_LIST', _PATTERN_ROW)
+
+
+VALID_CAPS = {'rw', 'r', 'w', 'off'}
+
+
+# === Resolution =================================================================
+
+
+def resolve(
+ cfg: dict, format_catalog: list[str], pattern_catalog: list[tuple[str, str, str, str]]
+) -> dict:
+ """Combine catalogs + user config; return concrete settings."""
+ features = cfg.get('features', {})
+ have_pattern = bool(features.get('pattern', True))
+ have_convert = bool(features.get('convert', True))
+ default_caps = features.get('default_format_caps', 'rw')
+ if default_caps not in VALID_CAPS:
+ raise SystemExit(f'invalid default_format_caps: {default_caps!r}')
+
+ catalog_names = set(format_catalog)
+ overrides = cfg.get('formats', {}) or {}
+ for name in overrides:
+ if name not in catalog_names:
+ raise SystemExit(f'[formats] override for unknown format: {name!r}')
+ if overrides[name] not in VALID_CAPS:
+ raise SystemExit(f'invalid caps for {name!r}: {overrides[name]!r}')
+
+ formats = []
+ for name in format_catalog:
+ caps = overrides.get(name, default_caps)
+ read = 'r' in caps and caps != 'off'
+ write = 'w' in caps and caps != 'off'
+ # Reading only matters for the convert path; if convert is off
+ # we suppress all reads so unpack_for<Read=false, ...> is the
+ # only instantiation seen and unpack_to_norm never gets
+ # referenced.
+ if not have_convert:
+ read = False
+ formats.append(
+ {
+ 'name': name,
+ 'read': read,
+ 'write': write,
+ }
+ )
+
+ enabled_names = {f['name'] for f in formats if f['read'] or f['write']}
+ hot_pivots = cfg.get('hot_pivots', []) or []
+ for p in hot_pivots:
+ if p not in enabled_names:
+ raise SystemExit(
+ f'hot_pivot {p!r} is not an enabled format (must have read or write enabled)'
+ )
+
+ catalog_names = {n for (_lbl, _rgb, _yuv, n) in pattern_catalog}
+ patterns = cfg.get('patterns', []) or []
+ for p in patterns:
+ if p not in catalog_names:
+ raise SystemExit(f'unknown pattern: {p!r} (known: {sorted(catalog_names)})')
+
+ # An empty pattern list collapses pattern feature to 'off'.
+ have_pattern = have_pattern and bool(patterns)
+
+ return {
+ 'have_pattern': have_pattern,
+ 'have_convert': have_convert,
+ 'hot_pivots': hot_pivots,
+ 'patterns': patterns,
+ 'formats': formats,
+ 'pattern_catalog': pattern_catalog,
+ }
+
+
+# === Emitters ===================================================================
+
+HEADER = '// Auto-generated by gen_pixpat.py. Do not edit by hand.'
+
+
+def emit_config_h(r: dict) -> str:
+ return '\n'.join(
+ [
+ HEADER,
+ '#pragma once',
+ '',
+ f'#define PIXPAT_FEATURE_PATTERN {1 if r["have_pattern"] else 0}',
+ f'#define PIXPAT_FEATURE_CONVERT {1 if r["have_convert"] else 0}',
+ '',
+ ]
+ )
+
+
+def _caps_row(f: dict, hot: set[str]) -> str:
+ cells = [
+ 'true,' if f['read'] else 'false,',
+ 'true,' if f['write'] else 'false,',
+ 'true,' if (f['name'] in hot) and f['read'] else 'false,',
+ 'true' if (f['name'] in hot) and f['write'] else 'false',
+ ]
+ return f'\t{{ {cells[0]:<7}{cells[1]:<7}{cells[2]:<7}{cells[3]:<6}}}, // {f["name"]}'
+
+
+def emit_caps_inc(r: dict) -> str:
+ out = [HEADER, '']
+
+ # Per-format build capabilities, one row per FormatId (catalog
+ # order). The FormatCaps schema and the size sanity-check live in
+ # static source (pixpat_internal.h / pixpat.cpp); this file is pure
+ # data. Consumers pull individual bools via .readable / .writable /
+ # .hot_src / .hot_dst — member accesses on a constexpr array
+ # element are themselves constant expressions, so they work as
+ # `if constexpr` conditions and as non-type template arguments.
+ out += [
+ 'inline constexpr FormatCaps s_format_caps[] = {',
+ '\t// readable writable hot_src hot_dst',
+ ]
+ hot = set(r['hot_pivots'])
+ for f in r['formats']:
+ out.append(_caps_row(f, hot))
+ out.append('};')
+ out.append('')
+
+ # Per-pattern build capabilities, indexed by PatternId. An unknown
+ # or disabled pattern name in pixpat_draw_pattern is an error, so
+ # there's no default-fallback row.
+ user_patterns = set(r['patterns'])
+ out += [
+ 'inline constexpr PatternCaps s_pattern_caps[] = {',
+ '\t// enabled',
+ ]
+ for _label, _rgb, _yuv, str_name in r['pattern_catalog']:
+ enabled = 'true' if str_name in user_patterns else 'false'
+ out.append(f'\t{{ {enabled:<5} }}, // {str_name}')
+ out.append('};')
+ out.append('')
+ return '\n'.join(out)
+
+
+# === CLI ========================================================================
+
+
+def main() -> int:
+ ap = argparse.ArgumentParser()
+ ap.add_argument('--config', type=Path, required=True)
+ ap.add_argument('--format-catalog', type=Path, required=True, help='Path to format_catalog.h.')
+ ap.add_argument(
+ '--pattern-catalog', type=Path, required=True, help='Path to pattern_catalog.h.'
+ )
+ ap.add_argument('--out-config-h', type=Path)
+ ap.add_argument('--out-caps-inc', type=Path)
+ ap.add_argument(
+ '--query',
+ type=str,
+ default=None,
+ help='Print 0/1 for: feature_pattern, feature_convert, have_both (= pattern && convert).',
+ )
+ args = ap.parse_args()
+
+ format_catalog = parse_format_catalog(args.format_catalog)
+ pattern_catalog = parse_pattern_catalog(args.pattern_catalog)
+ with args.config.open('rb') as fh:
+ cfg = tomllib.load(fh)
+ r = resolve(cfg, format_catalog, pattern_catalog)
+
+ if args.query:
+ flag = {
+ 'feature_pattern': r['have_pattern'],
+ 'feature_convert': r['have_convert'],
+ 'have_both': r['have_pattern'] and r['have_convert'],
+ }.get(args.query)
+ if flag is None:
+ raise SystemExit(f'unknown query: {args.query!r}')
+ print(1 if flag else 0)
+ return 0
+
+ outs = [
+ (args.out_config_h, emit_config_h(r)),
+ (args.out_caps_inc, emit_caps_inc(r)),
+ ]
+ if any(o[0] is None for o in outs):
+ raise SystemExit('all --out-* flags are required when not in --query mode')
+ for path, content in outs:
+ path.write_text(content)
+ return 0
+
+
+if __name__ == '__main__':
+ sys.exit(main())
diff --git a/subprojects/pixpat/pixpat-native/cross/aarch64-linux-gnu.txt b/subprojects/pixpat/pixpat-native/cross/aarch64-linux-gnu.txt
new file mode 100644
index 0000000..0c33261
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/cross/aarch64-linux-gnu.txt
@@ -0,0 +1,11 @@
+[binaries]
+c = 'aarch64-linux-gnu-gcc'
+cpp = 'aarch64-linux-gnu-g++'
+ar = 'aarch64-linux-gnu-ar'
+strip = 'aarch64-linux-gnu-strip'
+
+[host_machine]
+system = 'linux'
+cpu_family = 'aarch64'
+cpu = 'aarch64'
+endian = 'little'
diff --git a/subprojects/pixpat/pixpat-native/inc/pixpat/pixpat.h b/subprojects/pixpat/pixpat-native/inc/pixpat/pixpat.h
new file mode 100644
index 0000000..dd7c24e
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/inc/pixpat/pixpat.h
@@ -0,0 +1,264 @@
+#pragma once
+
+#include <stddef.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * libpixpat — draw test patterns and convert pixel data in a wide range
+ * of pixel formats (planar / semi-planar / packed YUV, RGB, raw, ...).
+ *
+ * The library writes directly into caller-owned pixel buffers. The
+ * caller is responsible for allocating each plane with the correct size
+ * and stride for the chosen format; pixpat does not own or allocate any
+ * pixel memory itself.
+ *
+ * Format names follow the convention used by kms++ and pixutils
+ * (e.g. "XRGB8888", "NV12", "YUYV") — not the DRM/V4L2 four-character
+ * codes ("XR24", etc.). Use pixpat_format_count() / pixpat_format_name()
+ * to enumerate the names accepted by pixpat_buffer::format, and
+ * pixpat_format_supported() to test a single name.
+ *
+ * All functions are thread-safe in the sense that independent calls on
+ * disjoint buffers may run concurrently from different threads. Pixpat
+ * does not spawn threads internally.
+ */
+
+/* Maximum number of planes any pixpat_buffer may reference. */
+#define PIXPAT_MAX_PLANES 4
+
+/*
+ * Description of a pixel buffer passed to or from the library.
+ *
+ * format - Pixel-format name (NUL-terminated ASCII), e.g.
+ * "XRGB8888", "NV12", "YUYV". Must be a name accepted by
+ * pixpat_format_supported(). The string is read during
+ * the call and need not outlive it.
+ * width - Image width in pixels. Some formats constrain this:
+ * chroma-subsampled YUV formats require a multiple of
+ * the horizontal subsampling factor (2 for NV12 /
+ * YUV420 / YUYV), and packed formats add a further
+ * multiple from their pixel-group size (e.g. 6 for
+ * P030, 4 for SBGGR10P).
+ * height - Image height in pixels. Vertically-subsampled formats
+ * (e.g. NV12, YUV420) require a multiple of the vertical
+ * subsampling factor.
+ * num_planes - Number of planes the format uses. Must be in
+ * [1, PIXPAT_MAX_PLANES] and match `format`.
+ * planes - Per-plane base pointers. Each plane must have at least
+ * strides[i] * plane_height bytes addressable, where
+ * plane_height is `height` for the main plane and
+ * height / vertical_subsampling for chroma planes (e.g.
+ * height / 2 for the UV plane of NV12). Entries beyond
+ * num_planes are ignored.
+ * strides - Per-plane row stride in bytes. Strides larger than the
+ * minimum row size are allowed — useful for hardware
+ * that requires aligned rows. Entries beyond num_planes
+ * are ignored.
+ */
+typedef struct {
+ const char* format;
+ uint32_t width;
+ uint32_t height;
+ uint32_t num_planes;
+ void* planes[PIXPAT_MAX_PLANES];
+ uint32_t strides[PIXPAT_MAX_PLANES];
+} pixpat_buffer;
+
+/*
+ * YCbCr color encoding standard.
+ *
+ * Selects the matrix used to convert between RGB and YUV. Has no effect
+ * when the operation does not cross the RGB/YUV boundary (e.g. drawing
+ * into an RGB or raw format, or converting between two formats of the
+ * same color kind).
+ *
+ * PIXPAT_REC_BT601 - ITU-R BT.601 (standard definition).
+ * PIXPAT_REC_BT709 - ITU-R BT.709 (HD).
+ * PIXPAT_REC_BT2020 - ITU-R BT.2020 (UHD / HDR, non-constant
+ * luminance).
+ */
+typedef enum {
+ PIXPAT_REC_BT601 = 0,
+ PIXPAT_REC_BT709 = 1,
+ PIXPAT_REC_BT2020 = 2,
+} pixpat_rec;
+
+/*
+ * Quantization range for YUV components.
+ *
+ * PIXPAT_RANGE_LIMITED - "TV" / studio range: Y in [16, 235], C in
+ * [16, 240] (scaled to bit depth). What most
+ * video pipelines expect.
+ * PIXPAT_RANGE_FULL - "PC" / full range: every component uses the
+ * full code range (e.g. [0, 255] for 8-bit).
+ */
+typedef enum {
+ PIXPAT_RANGE_LIMITED = 0,
+ PIXPAT_RANGE_FULL = 1,
+} pixpat_range;
+
+/*
+ * Optional settings for pixpat_draw_pattern().
+ *
+ * rec - YCbCr matrix used when drawing into a YUV format.
+ * Ignored for RGB / raw formats.
+ * range - Quantization range used when drawing into a YUV format.
+ * Ignored for RGB / raw formats.
+ * num_threads - Worker-thread count. Zero selects a sensible default
+ * (one per online CPU, capped to a sane maximum); one runs
+ * single-threaded with no thread-spawn overhead; N > 1 uses
+ * exactly N workers. Negative values are rejected. Output
+ * is bit-identical regardless of the chosen count.
+ * params - Pattern-specific parameters as a NUL-terminated ASCII
+ * string of comma-separated "key=value" items, e.g.
+ * "color=ff0000". Whitespace around tokens is trimmed;
+ * keys and values are case-insensitive and may not contain
+ * ',' or '='. NULL or "" selects per-pattern defaults.
+ * Unknown keys are silently ignored; malformed input
+ * (missing '=', empty key, …) makes the call fail with
+ * -1. Per-pattern keys are documented per pattern.
+ */
+typedef struct {
+ pixpat_rec rec;
+ pixpat_range range;
+ int num_threads;
+ const char* params;
+} pixpat_pattern_opts;
+
+/*
+ * Draw a test pattern into `dst`.
+ *
+ * `dst` must be non-NULL; `dst->format` must name a supported format,
+ * and `dst->width` / `dst->height` / strides must satisfy the format's
+ * constraints (see pixpat_buffer).
+ *
+ * `pattern` selects the pattern to draw (NUL-terminated ASCII). NULL
+ * selects the default ("kmstest"); any other unrecognized name returns
+ * -1. Recognized values:
+ * "kmstest" - default test pattern (color gradients with ramps),
+ * originally from kmstest.
+ * "smpte" - SMPTE RP 219-1 color bars. Pixel values are spec-
+ * defined in BT.709 / Limited; pass rec=BT709,
+ * range=Limited for spec-correct output. Other settings
+ * are accepted and produce visibly-wrong colors when
+ * drawing into RGB sinks (the caller's matrix is applied
+ * to BT.709-encoded values). Callers are trusted.
+ * "plain" - solid color fill from opts->params. Reads "color=<hex>"
+ * (hex, case-insensitive, optional "0x" prefix). The
+ * number of hex digits selects the layout:
+ * 6 digits: 8-bit RRGGBB
+ * 8 digits: 8-bit AARRGGBB (alpha first)
+ * 12 digits: 16-bit RRRRGGGGBBBB
+ * 16 digits: 16-bit AAAARRRRGGGGBBBB (alpha first)
+ * 8-bit components are byte-replicated to the internal
+ * 16-bit normalized form (0xFF -> 0xFFFF). Missing or
+ * malformed `color` returns -1.
+ * "checker" - black/white checkerboard. Reads optional "cell=<N>"
+ * (positive integer; default 8) for cell size in pixels.
+ * "cell=1" gives the 1-pixel chroma-subsampling stress
+ * test. A non-positive or non-numeric value returns -1.
+ * "hramp" - four horizontal stripes (R, G, B, gray), each a
+ * 0->max ramp along x. Per-channel and luma
+ * quantization in one pattern.
+ * "vramp" - same as hramp rotated 90°: four vertical columns
+ * (R, G, B, gray), each a 0->max ramp along y.
+ * "hbar" - horizontal bar (full image width, narrow along y)
+ * over a black background. Reads required "pos=<N>"
+ * (signed integer, top edge in pixels; negative values
+ * clip at the top) and optional "width=<N>" (positive
+ * integer, bar thickness in pixels; default 32). The
+ * bar is split into seven equal-width regions colored
+ * white/red/white/green/white/blue/white. Missing or
+ * non-numeric pos, or non-positive width, returns -1.
+ * "vbar" - same as hbar rotated 90°: vertical bar (full image
+ * height, narrow along x), with "pos" measured along x
+ * and the bar split into seven equal-height regions
+ * with the same color sequence.
+ * "dramp" - diagonal RGB ramp (R sweeps with x, G with y,
+ * B with x+y).
+ * "zoneplate"- centered radial cosine pattern; spatial frequency
+ * ramps from DC at the center to Nyquist at the longer
+ * edge. Useful for spotting scaling/aliasing artifacts.
+ *
+ * `opts` may be NULL: equivalent to passing a zero-initialised
+ * pixpat_pattern_opts. That is BT.601 / limited-range color math, the
+ * auto thread count (one worker per online CPU, capped), and no
+ * pattern parameters.
+ *
+ * Returns 0 on success, -1 on failure — typically a NULL `dst`, an
+ * unknown format name, or zero-sized dimensions. Strides and plane
+ * sizes are not validated against the buffers; an undersized plane
+ * leads to undefined behavior.
+ */
+int pixpat_draw_pattern(const pixpat_buffer* dst,
+ const char* pattern,
+ const pixpat_pattern_opts* opts);
+
+/*
+ * Options for pixpat_convert().
+ *
+ * rec - YCbCr matrix used when the conversion crosses the
+ * RGB/YUV boundary. Ignored when src and dst share the
+ * same color kind.
+ * range - Quantization range used when the conversion crosses the
+ * RGB/YUV boundary. Ignored when src and dst share the
+ * same color kind.
+ * num_threads - Worker-thread count. Zero selects a sensible default
+ * (one per online CPU, capped to a sane maximum); one runs
+ * single-threaded with no thread-spawn overhead; N > 1 uses
+ * exactly N workers. Negative values are rejected. Output
+ * is bit-identical regardless of the chosen count.
+ */
+typedef struct {
+ pixpat_rec rec;
+ pixpat_range range;
+ int num_threads;
+} pixpat_convert_opts;
+
+/*
+ * Convert pixel data from src into dst. Both buffers must have matching
+ * width and height.
+ *
+ * Cross-color-kind conversions (RGB <-> YUV) use opts->rec/range for
+ * the color-space math; same-color-kind conversions ignore both fields.
+ *
+ * Bayer sources are decoded with a 3x3 bilinear demosaic.
+ *
+ * `opts` may be NULL: equivalent to passing a zero-initialised
+ * pixpat_convert_opts. That is BT.601 / limited-range color math and
+ * the auto thread count (one worker per online CPU, capped).
+ *
+ * Returns 0 on success, -1 on failure.
+ */
+int pixpat_convert(const pixpat_buffer* dst,
+ const pixpat_buffer* src,
+ const pixpat_convert_opts* opts);
+
+/*
+ * Return non-zero if `format` is a known pixel-format name accepted by
+ * pixpat_buffer::format, zero otherwise. Passing NULL returns zero.
+ */
+int pixpat_format_supported(const char* format);
+
+/*
+ * Return the number of pixel-format names known to the library. Use
+ * with pixpat_format_name() to enumerate the formats.
+ */
+size_t pixpat_format_count(void);
+
+/*
+ * Return the name of the format at index `idx`, or NULL if `idx` is
+ * out of range (>= pixpat_format_count()).
+ *
+ * The returned pointer references storage owned by the library and is
+ * valid for the lifetime of the process; the caller must not free it.
+ */
+const char* pixpat_format_name(size_t idx);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/subprojects/pixpat/pixpat-native/profiles/no_hotpath.toml b/subprojects/pixpat/pixpat-native/profiles/no_hotpath.toml
new file mode 100644
index 0000000..e6fdda9
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/profiles/no_hotpath.toml
@@ -0,0 +1,12 @@
+# Same as the default config, but with no hot pivots — every conversion
+# routes through the normalized (cold) path.
+
+hot_pivots = []
+patterns = ["kmstest", "smpte", "plain", "checker", "hramp", "vramp", "hbar", "vbar", "dramp", "zoneplate"]
+
+[features]
+pattern = true
+convert = true
+default_format_caps = "rw"
+
+[formats]
diff --git a/subprojects/pixpat/pixpat-native/profiles/pattern_only.toml b/subprojects/pixpat/pixpat-native/profiles/pattern_only.toml
new file mode 100644
index 0000000..4410f50
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/profiles/pattern_only.toml
@@ -0,0 +1,11 @@
+# kms++ profile: pattern drawing only, all formats targetable.
+
+hot_pivots = []
+patterns = ["kmstest", "smpte", "plain", "checker", "hramp", "vramp", "hbar", "vbar", "dramp", "zoneplate"]
+
+[features]
+pattern = true
+convert = false
+default_format_caps = "rw"
+
+[formats]
diff --git a/subprojects/pixpat/pixpat-native/profiles/pixpat.toml b/subprojects/pixpat/pixpat-native/profiles/pixpat.toml
new file mode 100644
index 0000000..fb27009
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/profiles/pixpat.toml
@@ -0,0 +1,31 @@
+# pixpat user config (consumed by gen_pixpat.py).
+#
+# This file expresses *selection*: which top-level features, which
+# hot pivots, which patterns, and per-format capability overrides.
+# The fixed catalog of formats (each one's Source/Sink/Layout) lives
+# in gen_pixpat.py and is the same across all profiles.
+
+# Hot pivots. Each named format gets fully-fused
+# Converter<Src, all-write-sinks> and Converter<all-read-sources, Snk>
+# instantiations. Empty list = every conversion takes the normalized
+# (cold) path.
+hot_pivots = ["BGR888"]
+
+# Patterns to compile in. The first entry is the default for
+# unrecognized pattern names. Empty list disables the pattern feature.
+patterns = ["kmstest", "smpte", "plain", "checker", "hramp", "vramp", "hbar", "vbar", "dramp", "zoneplate"]
+
+[features]
+# Top-level toggles. False stubs the matching C entry point to -1.
+pattern = true
+convert = true
+
+# Default capability ("rw", "r", "w", "off") applied to every format
+# in the catalog. Per-format overrides go in the [formats] table.
+default_format_caps = "rw"
+
+# Per-format capability overrides. Anything not listed uses
+# default_format_caps. Use "off" to drop a format entirely.
+[formats]
+# RGB888 = "r" # readable only
+# YUV420 = "off" # not in this build
diff --git a/subprojects/pixpat/pixpat-native/profiles/to_bgr888.toml b/subprojects/pixpat/pixpat-native/profiles/to_bgr888.toml
new file mode 100644
index 0000000..66fbcae
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/profiles/to_bgr888.toml
@@ -0,0 +1,17 @@
+# "Decode many formats into BGR888" profile.
+#
+# - convert is on, pattern is off
+# - BGR888 is the only writable format (so all conversions land there)
+# - every other format is readable only — no per-format Sink instantiations
+# - BGR888 hot pivot fuses every (read-only-source → BGR888) loop
+
+hot_pivots = ["BGR888"]
+patterns = []
+
+[features]
+pattern = false
+convert = true
+default_format_caps = "r"
+
+[formats]
+BGR888 = "w"
diff --git a/subprojects/pixpat/pixpat-native/src/color.h b/subprojects/pixpat/pixpat-native/src/color.h
new file mode 100644
index 0000000..16dfb7d
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/color.h
@@ -0,0 +1,199 @@
+#pragma once
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+
+#include "layout.h"
+
+namespace pixpat
+{
+
+// BT.601 / BT.709 / BT.2020 × Limited / Full range, dispatched at
+// runtime via a small `ColorCoeffs` struct that the caller hoists out
+// of the per-pixel loop. The convert and pattern entry points compute
+// `coeffs_for(spec)` once before the stripe loop, then pass the
+// resulting struct into every `ColorXfm::apply()` in the inner loop.
+// This avoids per-pixel matrix branching and also the alternative of
+// a 6×-instantiated template (would push the hot pivot from 121 to
+// 726 `Converter` bodies). The coefficient values are loop-invariant
+// broadcast scalars, so the compiler vectorizes the inner loop with
+// vbroadcastss + vmulps in place of constant folds.
+//
+// Math runs in float.
+
+enum class Rec : uint8_t { BT601, BT709, BT2020 };
+enum class Range : uint8_t { Limited, Full };
+
+struct ColorSpec {
+ Rec rec;
+ Range range;
+ constexpr bool operator==(const ColorSpec&) const = default;
+};
+
+inline constexpr ColorSpec kDefaultColorSpec{ Rec::BT601, Range::Limited };
+
+struct ColorCoeffs {
+ // RGB->YUV
+ float kr, kg, kb;
+ float y_scale, y_offset;
+ float c_scale, c_offset;
+ float u_factor, v_factor;
+ // YUV->RGB
+ float y_inv, c_inv;
+ float gu, gv, ru, bv;
+ // normalized 16-bit scale (kNormMax in float, plus its inverse)
+ float norm_scale, norm_inv_scale;
+};
+
+namespace detail
+{
+constexpr ColorCoeffs make_coeffs(float kr, float kg, float kb, bool full) noexcept
+{
+ const float y_min = full ? 0.0f : 16.0f / 255.0f;
+ const float y_max = full ? 1.0f : 235.0f / 255.0f;
+ const float c_min = full ? 0.0f : 16.0f / 255.0f;
+ const float c_max = full ? 1.0f : 240.0f / 255.0f;
+
+ const float y_scale = y_max - y_min;
+ const float y_offset = y_min;
+ const float c_scale = c_max - c_min;
+ const float c_offset = (c_max + c_min) * 0.5f;
+
+ const float u_factor = 1.0f / (2.0f * (1.0f - kb));
+ const float v_factor = 1.0f / (2.0f * (1.0f - kr));
+ const float y_inv = 1.0f / y_scale;
+ const float c_inv = 1.0f / c_scale;
+ const float gu = -2.0f * (1.0f - kb) * kb / kg;
+ const float gv = -2.0f * (1.0f - kr) * kr / kg;
+ const float ru = 2.0f * (1.0f - kr);
+ const float bv = 2.0f * (1.0f - kb);
+
+ const float norm_scale = float(kNormMax);
+ const float norm_inv_scale = 1.0f / norm_scale;
+
+ return ColorCoeffs{
+ kr, kg, kb,
+ y_scale, y_offset,
+ c_scale, c_offset,
+ u_factor, v_factor,
+ y_inv, c_inv,
+ gu, gv, ru, bv,
+ norm_scale, norm_inv_scale,
+ };
+}
+} // namespace detail
+
+constexpr ColorCoeffs coeffs_for(ColorSpec spec) noexcept
+{
+ const bool full = spec.range == Range::Full;
+ switch (spec.rec) {
+ case Rec::BT601: return detail::make_coeffs(0.299f, 0.587f, 0.114f, full);
+ case Rec::BT2020: return detail::make_coeffs(0.2627f, 0.6780f, 0.0593f, full);
+ default: return detail::make_coeffs(0.2126f, 0.7152f, 0.0722f, full);
+ }
+}
+
+template <typename SrcPix, typename DstPix>
+struct ColorXfm;
+
+template <>
+struct ColorXfm<RGB16, RGB16> {
+ static constexpr RGB16 apply(RGB16 p) noexcept {
+ return p;
+ }
+ static constexpr RGB16 apply(RGB16 p, const ColorCoeffs&) noexcept {
+ return p;
+ }
+};
+
+template <>
+struct ColorXfm<YUV16, YUV16> {
+ static constexpr YUV16 apply(YUV16 p) noexcept {
+ return p;
+ }
+ static constexpr YUV16 apply(YUV16 p, const ColorCoeffs&) noexcept {
+ return p;
+ }
+};
+
+// Cross-color-kind conversions reset `a` to kNormMax (sinks with X
+// write 0; sinks with A see fully opaque pixels). Within the same
+// color kind, identity ColorXfm propagates `a` unchanged.
+template <>
+struct ColorXfm<RGB16, YUV16> {
+ static YUV16 apply(RGB16 rgb, const ColorCoeffs& c) noexcept
+ {
+ const float r = float(rgb.r) * c.norm_inv_scale;
+ const float g = float(rgb.g) * c.norm_inv_scale;
+ const float b = float(rgb.b) * c.norm_inv_scale;
+
+ const float yp = c.kr * r + c.kg * g + c.kb * b;
+ const float u = (b - yp) * c.u_factor;
+ const float v = (r - yp) * c.v_factor;
+
+ // No clamp on RGB→YUV: for any uint16_t (RGB) input the
+ // output Y/U/V is structurally in [0, 1] (limited-range
+ // chroma stays within [c_min, c_max] ⊂ [0, 1]). The +0.5
+ // rounds half-up before the integer cast.
+ return YUV16{
+ uint16_t((yp * c.y_scale + c.y_offset) * c.norm_scale + 0.5f),
+ uint16_t((u * c.c_scale + c.c_offset) * c.norm_scale + 0.5f),
+ uint16_t((v * c.c_scale + c.c_offset) * c.norm_scale + 0.5f),
+ kNormMax,
+ };
+ }
+};
+
+template <>
+struct ColorXfm<YUV16, RGB16> {
+ static RGB16 apply(YUV16 yuv, const ColorCoeffs& c) noexcept
+ {
+ const float yp = (float(yuv.y) * c.norm_inv_scale - c.y_offset) * c.y_inv;
+ const float u = (float(yuv.u) * c.norm_inv_scale - c.c_offset) * c.c_inv;
+ const float v = (float(yuv.v) * c.norm_inv_scale - c.c_offset) * c.c_inv;
+
+ const float r = yp + c.ru * v;
+ const float g = yp + c.gu * u + c.gv * v;
+ const float b = yp + c.bv * u;
+
+ // Clamp on YUV→RGB: the inverse matrix produces out-of-range
+ // RGB for some valid YUV inputs. Written as min/max so it
+ // vectorizes to vminps/vmaxps; std::clamp can defeat that.
+ auto pack = [&](float x) -> uint16_t {
+ x = x * c.norm_scale + 0.5f;
+ x = std::min(std::max(x, 0.0f), c.norm_scale);
+ return uint16_t(x);
+ };
+
+ return RGB16{
+ pack(r), pack(g), pack(b),
+ kNormMax,
+ };
+ }
+};
+
+// In-place cross-color-kind passes over a normalized line buffer.
+// RGB16 and YUV16 are both 4 uint16_t with identical layout, so we
+// can memcpy through the same buffer pixel-by-pixel without aliasing.
+inline void norm_rgb_to_yuv(uint8_t* buf, size_t n, const ColorCoeffs& c) noexcept
+{
+ for (size_t i = 0; i < n; ++i) {
+ RGB16 rgb;
+ std::memcpy(&rgb, buf + i * sizeof(RGB16), sizeof(RGB16));
+ YUV16 yuv = ColorXfm<RGB16, YUV16>::apply(rgb, c);
+ std::memcpy(buf + i * sizeof(YUV16), &yuv, sizeof(YUV16));
+ }
+}
+
+inline void norm_yuv_to_rgb(uint8_t* buf, size_t n, const ColorCoeffs& c) noexcept
+{
+ for (size_t i = 0; i < n; ++i) {
+ YUV16 yuv;
+ std::memcpy(&yuv, buf + i * sizeof(YUV16), sizeof(YUV16));
+ RGB16 rgb = ColorXfm<YUV16, RGB16>::apply(yuv, c);
+ std::memcpy(buf + i * sizeof(RGB16), &rgb, sizeof(RGB16));
+ }
+}
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/error.h b/subprojects/pixpat/pixpat-native/src/error.h
new file mode 100644
index 0000000..83a3596
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/error.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include <stdexcept>
+
+namespace pixpat
+{
+
+struct error : std::runtime_error {
+ using std::runtime_error::runtime_error;
+};
+
+struct invalid_argument : error {
+ using error::error;
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/format_catalog.h b/subprojects/pixpat/pixpat-native/src/format_catalog.h
new file mode 100644
index 0000000..287d773
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/format_catalog.h
@@ -0,0 +1,140 @@
+#pragma once
+
+// Catalog of every pixel format the C++ side handles. The X-macro is a
+// flat list of names:
+//
+// X(name)
+//
+// `name` is the canonical format identifier — both an internal FormatId
+// enum entry and the public string accepted by pixpat_buffer::format.
+// Each name resolves to a struct in `pixpat::formats::` (defined under
+// pixpat-native/src/formats/) that carries:
+//
+// - the layout (subsampling, planes, components)
+// - nested `Source` / `Sink` aliases for the matching I/O templates
+//
+// Adding a format = a row here AND a struct in the right
+// pixpat-native/src/formats/*.h. The codegen
+// (pixpat-native/codegen/gen_pixpat.py) parses this X-macro to learn
+// the format set; pixpat.cpp re-expands it to build s_format_info via
+// `formats::name::Source` / `formats::name::Sink`.
+//
+// FormatId is internal — the public C ABI deals in format names only.
+
+#include <cstddef>
+
+namespace pixpat
+{
+
+#define PIXPAT_FORMAT_LIST(X) \
+ X(XRGB8888) \
+ X(ARGB8888) \
+ X(XBGR8888) \
+ X(ABGR8888) \
+ X(RGBX8888) \
+ X(RGBA8888) \
+ X(BGRX8888) \
+ X(BGRA8888) \
+ X(RGB888) \
+ X(BGR888) \
+ X(RGB332) \
+ X(RGB565) \
+ X(BGR565) \
+ X(XRGB1555) \
+ X(ARGB1555) \
+ X(XBGR1555) \
+ X(ABGR1555) \
+ X(XRGB4444) \
+ X(ARGB4444) \
+ X(XBGR4444) \
+ X(ABGR4444) \
+ X(RGBX4444) \
+ X(RGBA4444) \
+ X(XRGB2101010) \
+ X(ARGB2101010) \
+ X(XBGR2101010) \
+ X(ABGR2101010) \
+ X(RGBX1010102) \
+ X(RGBA1010102) \
+ X(BGRX1010102) \
+ X(BGRA1010102) \
+ X(ABGR16161616) \
+ X(NV12) \
+ X(NV21) \
+ X(NV16) \
+ X(NV61) \
+ X(P030) \
+ X(P230) \
+ X(YUV420) \
+ X(YVU420) \
+ X(YUV422) \
+ X(YVU422) \
+ X(YUV444) \
+ X(YVU444) \
+ X(T430) \
+ X(VUY888) \
+ X(XVUY8888) \
+ X(XVUY2101010) \
+ X(AVUY16161616) \
+ X(YUYV) \
+ X(YVYU) \
+ X(UYVY) \
+ X(VYUY) \
+ X(Y210) \
+ X(Y212) \
+ X(Y216) \
+ X(Y8) \
+ X(Y10) \
+ X(Y12) \
+ X(Y16) \
+ X(R8) \
+ X(XYYY2101010) \
+ X(Y10P) \
+ X(Y12P) \
+ X(SRGGB8) \
+ X(SBGGR8) \
+ X(SGRBG8) \
+ X(SGBRG8) \
+ X(SRGGB10) \
+ X(SBGGR10) \
+ X(SGRBG10) \
+ X(SGBRG10) \
+ X(SRGGB12) \
+ X(SBGGR12) \
+ X(SGRBG12) \
+ X(SGBRG12) \
+ X(SRGGB16) \
+ X(SBGGR16) \
+ X(SGRBG16) \
+ X(SGBRG16) \
+ X(SRGGB10P) \
+ X(SBGGR10P) \
+ X(SGRBG10P) \
+ X(SGBRG10P) \
+ X(SRGGB12P) \
+ X(SBGGR12P) \
+ X(SGRBG12P) \
+ X(SGBRG12P)
+
+enum class FormatId {
+#define X(name) name,
+ PIXPAT_FORMAT_LIST(X)
+#undef X
+ Unknown,
+};
+
+struct FormatEntry {
+ const char* name;
+ FormatId id;
+};
+
+inline constexpr FormatEntry s_format_table[] = {
+#define X(name) { #name, FormatId::name },
+ PIXPAT_FORMAT_LIST(X)
+#undef X
+};
+
+inline constexpr size_t s_format_catalog_count =
+ sizeof(s_format_table) / sizeof(s_format_table[0]);
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/formats.h b/subprojects/pixpat/pixpat-native/src/formats.h
new file mode 100644
index 0000000..68bdeec
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/formats.h
@@ -0,0 +1,13 @@
+#pragma once
+
+// Aggregator: every named layout the X-macro registers lives in one of
+// the headers under formats/, organized by color kind. Format names
+// follow the kms++/pixutils convention (see formats/rgb.h for the
+// longer note; the YUYV group is an exception, see formats/yuv_packed.h).
+
+#include "formats/rgb.h"
+#include "formats/yuv_semiplanar.h"
+#include "formats/yuv_planar.h"
+#include "formats/yuv_packed.h"
+#include "formats/grayscale.h"
+#include "formats/bayer.h"
diff --git a/subprojects/pixpat/pixpat-native/src/formats/bayer.h b/subprojects/pixpat/pixpat-native/src/formats/bayer.h
new file mode 100644
index 0000000..057c342
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/formats/bayer.h
@@ -0,0 +1,97 @@
+#pragma once
+
+// Bayer raw layouts. Each pixel carries one of R/G/B selected by
+// (x mod 2, y mod 2) and BayerOrder; the pattern is on the
+// BayerSource/BayerSink template, not the layout itself. Storage shape
+// is single-component (C::Y reused as the storage tag) so the same
+// 8/10/12/16-bit shapes apply across all four phase patterns.
+//
+// Each format is a distinct struct (rather than a type alias of one
+// another) so each format type can carry its own pattern-specific
+// Source/Sink aliases. The shared bit layout lives in a base struct per
+// (depth,packing) combination.
+//
+// ColorKind is RGB because the normalized pixel passed through ColorXfm
+// is RGB16 — the sink picks one of r/g/b at write time, and the
+// source nearest-neighbor demosaics into RGB16 at read time.
+
+#include "../layout.h"
+#include "../io/bayer.h"
+
+namespace pixpat::formats
+{
+
+namespace bayer_detail
+{
+
+// Per-(depth,packing) base layouts. Every Bayer format derives from
+// one of these and pins its own pattern-specific I/O templates.
+using Bayer8 = Layout<ColorKind::RGB, 1, 1,
+ Plane<uint8_t, Comp { C::Y, 8, 0 }> >;
+using Bayer10 = Layout<ColorKind::RGB, 1, 1,
+ Plane<uint16_t, Comp { C::Y, 10, 0 }, Comp { C::X, 6, 10 }> >;
+using Bayer12 = Layout<ColorKind::RGB, 1, 1,
+ Plane<uint16_t, Comp { C::Y, 12, 0 }, Comp { C::X, 4, 12 }> >;
+using Bayer16 = Layout<ColorKind::RGB, 1, 1,
+ Plane<uint16_t, Comp { C::Y, 16, 0 }> >;
+// MIPI CSI-2 packed Bayer (10P: 4 pix in 5 bytes; 12P: 2 pix in 3
+// bytes). The Layout doesn't capture the packed bit layout — the
+// BayerPackedSink hand-rolls the byte writes. uint8_t plane shape is
+// a placeholder so the dispatch plumbing is uniform.
+using Bayer10P = Layout<ColorKind::RGB, 1, 1,
+ Plane<uint8_t, Comp { C::Y, 8, 0 }> >;
+using Bayer12P = Layout<ColorKind::RGB, 1, 1,
+ Plane<uint8_t, Comp { C::Y, 8, 0 }> >;
+
+} // namespace bayer_detail
+
+// Unpacked Bayer (4 patterns × 4 bit depths).
+#define PIXPAT_BAYER(name, base, pat) \
+ struct name : bayer_detail::base { \
+ using Source = BayerSource_ ## pat<name>; \
+ using Sink = BayerSink_ ## pat<name>; \
+ }
+
+PIXPAT_BAYER(SRGGB8, Bayer8, RGGB);
+PIXPAT_BAYER(SBGGR8, Bayer8, BGGR);
+PIXPAT_BAYER(SGRBG8, Bayer8, GRBG);
+PIXPAT_BAYER(SGBRG8, Bayer8, GBRG);
+
+PIXPAT_BAYER(SRGGB10, Bayer10, RGGB);
+PIXPAT_BAYER(SBGGR10, Bayer10, BGGR);
+PIXPAT_BAYER(SGRBG10, Bayer10, GRBG);
+PIXPAT_BAYER(SGBRG10, Bayer10, GBRG);
+
+PIXPAT_BAYER(SRGGB12, Bayer12, RGGB);
+PIXPAT_BAYER(SBGGR12, Bayer12, BGGR);
+PIXPAT_BAYER(SGRBG12, Bayer12, GRBG);
+PIXPAT_BAYER(SGBRG12, Bayer12, GBRG);
+
+PIXPAT_BAYER(SRGGB16, Bayer16, RGGB);
+PIXPAT_BAYER(SBGGR16, Bayer16, BGGR);
+PIXPAT_BAYER(SGRBG16, Bayer16, GRBG);
+PIXPAT_BAYER(SGBRG16, Bayer16, GBRG);
+
+#undef PIXPAT_BAYER
+
+// MIPI-packed Bayer: pattern + bit depth both encoded in the I/O
+// template name (BayerPackedSource_RGGB10, ...).
+#define PIXPAT_BAYER_PACKED(name, base, pat_depth) \
+ struct name : bayer_detail::base { \
+ using Source = BayerPackedSource_ ## pat_depth<name>; \
+ using Sink = BayerPackedSink_ ## pat_depth<name>; \
+ }
+
+PIXPAT_BAYER_PACKED(SRGGB10P, Bayer10P, RGGB10);
+PIXPAT_BAYER_PACKED(SBGGR10P, Bayer10P, BGGR10);
+PIXPAT_BAYER_PACKED(SGRBG10P, Bayer10P, GRBG10);
+PIXPAT_BAYER_PACKED(SGBRG10P, Bayer10P, GBRG10);
+
+PIXPAT_BAYER_PACKED(SRGGB12P, Bayer12P, RGGB12);
+PIXPAT_BAYER_PACKED(SBGGR12P, Bayer12P, BGGR12);
+PIXPAT_BAYER_PACKED(SGRBG12P, Bayer12P, GRBG12);
+PIXPAT_BAYER_PACKED(SGBRG12P, Bayer12P, GBRG12);
+
+#undef PIXPAT_BAYER_PACKED
+
+} // namespace pixpat::formats
diff --git a/subprojects/pixpat/pixpat-native/src/formats/grayscale.h b/subprojects/pixpat/pixpat-native/src/formats/grayscale.h
new file mode 100644
index 0000000..b1cd294
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/formats/grayscale.h
@@ -0,0 +1,78 @@
+#pragma once
+
+// Single-component-per-pixel formats. Most are grayscale (Y) modeled as
+// a YUV format with synthesized neutral chroma; R8 is the RGB-kind
+// counterpart, modeled grey-style with G=B=R on read. Y10/Y12 carry an
+// explicit X padding bitfield. XYYY2101010 is multi-pixel-per-word: 3 Y
+// samples in 32 bits.
+
+#include "../layout.h"
+#include "../io/gray.h"
+#include "../io/gray_packed.h"
+#include "../io/mono_rgb.h"
+
+namespace pixpat::formats
+{
+
+#define PIXPAT_GRAY(name, ...) \
+ struct name : Layout<ColorKind::YUV, 1, 1, __VA_ARGS__> { \
+ using Source = GraySource<name>; \
+ using Sink = GraySink<name>; \
+ }
+
+PIXPAT_GRAY(Y8,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>);
+
+PIXPAT_GRAY(Y10,
+ Plane<uint16_t, Comp{ C::Y, 10, 0 }, Comp{ C::X, 6, 10 }>);
+
+PIXPAT_GRAY(Y12,
+ Plane<uint16_t, Comp{ C::Y, 12, 0 }, Comp{ C::X, 4, 12 }>);
+
+PIXPAT_GRAY(Y16,
+ Plane<uint16_t, Comp{ C::Y, 16, 0 }>);
+
+#undef PIXPAT_GRAY
+
+// R8: single 8-bit R channel. Read synthesizes G=B=R; write encodes R
+// and drops G/B/A. Symmetric to Y8 but ColorKind::RGB so cross-pipeline
+// conversions go through the RGB->YUV ColorXfm direction.
+struct R8 : Layout<ColorKind::RGB, 1, 1,
+ Plane<uint8_t, Comp{ C::R, 8, 0 }> > {
+ using Source = MonoRGBSource<R8>;
+ using Sink = MonoRGBSink<R8>;
+};
+
+struct XYYY2101010 : Layout<ColorKind::YUV, 1, 1,
+ Plane<uint32_t,
+ Comp{ C::Y, 10, 0 },
+ Comp{ C::Y, 10, 10 },
+ Comp{ C::Y, 10, 20 },
+ Comp{ C::X, 2, 30 }> > {
+ using Source = MultiPixelGraySource<XYYY2101010>;
+ using Sink = MultiPixelGraySink<XYYY2101010>;
+};
+
+// MIPI CSI-2 packed grayscale (Y10P / Y12P). The Layout doesn't capture
+// the packed bit layout — GrayPackedSource/Sink delegate to the shared
+// CSI-2 helper (io/csi2.h). uint8_t plane shape is a placeholder so
+// dispatch plumbing is uniform (mirrors bayer_detail::Bayer10P/12P).
+namespace gray_csi2_detail
+{
+using Gray10P = Layout<ColorKind::YUV, 1, 1,
+ Plane<uint8_t, Comp { C::Y, 8, 0 }> >;
+using Gray12P = Layout<ColorKind::YUV, 1, 1,
+ Plane<uint8_t, Comp { C::Y, 8, 0 }> >;
+} // namespace gray_csi2_detail
+
+struct Y10P : gray_csi2_detail::Gray10P {
+ using Source = GrayPackedSource<Y10P, 10>;
+ using Sink = GrayPackedSink<Y10P, 10>;
+};
+
+struct Y12P : gray_csi2_detail::Gray12P {
+ using Source = GrayPackedSource<Y12P, 12>;
+ using Sink = GrayPackedSink<Y12P, 12>;
+};
+
+} // namespace pixpat::formats
diff --git a/subprojects/pixpat/pixpat-native/src/formats/rgb.h b/subprojects/pixpat/pixpat-native/src/formats/rgb.h
new file mode 100644
index 0000000..19d007a
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/formats/rgb.h
@@ -0,0 +1,267 @@
+#pragma once
+
+// RGB packed layouts: 8-bit / 16-bit (sub-byte) / 32-bit (10-bit) /
+// 64-bit-normalized, all single-plane single-pixel-per-storage-word.
+// Names follow the kms++/pixutils register-order convention (MSB-first
+// in the storage word), so XRGB8888 has X at bits 31..24 and B at 7..0.
+
+#include "../layout.h"
+#include "../io/packed.h"
+
+namespace pixpat::formats
+{
+
+// Helper: every format in this file pairs with PackedSource/PackedSink.
+// Each format struct exposes Source / Sink aliases so the catalog row
+// in format_catalog.h can stay name-only.
+#define PIXPAT_RGB_PACKED(name, ...) \
+ struct name : Layout<ColorKind::RGB, 1, 1, __VA_ARGS__> { \
+ using Source = PackedSource<name>; \
+ using Sink = PackedSink<name>; \
+ }
+
+// ---------------------------------------------------------------------
+// 32-bit packed RGB, 8-bit components.
+// ---------------------------------------------------------------------
+
+PIXPAT_RGB_PACKED(XRGB8888,
+ Plane<uint32_t,
+ Comp{ C::B, 8, 0 },
+ Comp{ C::G, 8, 8 },
+ Comp{ C::R, 8, 16 },
+ Comp{ C::X, 8, 24 }>);
+
+PIXPAT_RGB_PACKED(ARGB8888,
+ Plane<uint32_t,
+ Comp{ C::B, 8, 0 },
+ Comp{ C::G, 8, 8 },
+ Comp{ C::R, 8, 16 },
+ Comp{ C::A, 8, 24 }>);
+
+PIXPAT_RGB_PACKED(XBGR8888,
+ Plane<uint32_t,
+ Comp{ C::R, 8, 0 },
+ Comp{ C::G, 8, 8 },
+ Comp{ C::B, 8, 16 },
+ Comp{ C::X, 8, 24 }>);
+
+PIXPAT_RGB_PACKED(ABGR8888,
+ Plane<uint32_t,
+ Comp{ C::R, 8, 0 },
+ Comp{ C::G, 8, 8 },
+ Comp{ C::B, 8, 16 },
+ Comp{ C::A, 8, 24 }>);
+
+PIXPAT_RGB_PACKED(RGBX8888,
+ Plane<uint32_t,
+ Comp{ C::X, 8, 0 },
+ Comp{ C::B, 8, 8 },
+ Comp{ C::G, 8, 16 },
+ Comp{ C::R, 8, 24 }>);
+
+PIXPAT_RGB_PACKED(RGBA8888,
+ Plane<uint32_t,
+ Comp{ C::A, 8, 0 },
+ Comp{ C::B, 8, 8 },
+ Comp{ C::G, 8, 16 },
+ Comp{ C::R, 8, 24 }>);
+
+PIXPAT_RGB_PACKED(BGRX8888,
+ Plane<uint32_t,
+ Comp{ C::X, 8, 0 },
+ Comp{ C::R, 8, 8 },
+ Comp{ C::G, 8, 16 },
+ Comp{ C::B, 8, 24 }>);
+
+PIXPAT_RGB_PACKED(BGRA8888,
+ Plane<uint32_t,
+ Comp{ C::A, 8, 0 },
+ Comp{ C::R, 8, 8 },
+ Comp{ C::G, 8, 16 },
+ Comp{ C::B, 8, 24 }>);
+
+// ---------------------------------------------------------------------
+// 24-bit packed RGB, three bytes per pixel. storage_t is uint32_t but
+// only bytes_per_pixel = 3 are read/written via memcpy.
+// ---------------------------------------------------------------------
+
+PIXPAT_RGB_PACKED(RGB888,
+ Plane<uint32_t,
+ Comp{ C::B, 8, 0 },
+ Comp{ C::G, 8, 8 },
+ Comp{ C::R, 8, 16 }>);
+
+PIXPAT_RGB_PACKED(BGR888,
+ Plane<uint32_t,
+ Comp{ C::R, 8, 0 },
+ Comp{ C::G, 8, 8 },
+ Comp{ C::B, 8, 16 }>);
+
+// ---------------------------------------------------------------------
+// 16-bit packed RGB, sub-byte components.
+// ---------------------------------------------------------------------
+
+PIXPAT_RGB_PACKED(RGB565,
+ Plane<uint16_t,
+ Comp{ C::B, 5, 0 },
+ Comp{ C::G, 6, 5 },
+ Comp{ C::R, 5, 11 }>);
+
+PIXPAT_RGB_PACKED(BGR565,
+ Plane<uint16_t,
+ Comp{ C::R, 5, 0 },
+ Comp{ C::G, 6, 5 },
+ Comp{ C::B, 5, 11 }>);
+
+// 8-bit packed RGB: 3-bit R / 3-bit G / 2-bit B in a single byte.
+
+PIXPAT_RGB_PACKED(RGB332,
+ Plane<uint8_t,
+ Comp{ C::B, 2, 0 },
+ Comp{ C::G, 3, 2 },
+ Comp{ C::R, 3, 5 }>);
+
+PIXPAT_RGB_PACKED(XRGB1555,
+ Plane<uint16_t,
+ Comp{ C::B, 5, 0 },
+ Comp{ C::G, 5, 5 },
+ Comp{ C::R, 5, 10 },
+ Comp{ C::X, 1, 15 }>);
+
+PIXPAT_RGB_PACKED(ARGB1555,
+ Plane<uint16_t,
+ Comp{ C::B, 5, 0 },
+ Comp{ C::G, 5, 5 },
+ Comp{ C::R, 5, 10 },
+ Comp{ C::A, 1, 15 }>);
+
+PIXPAT_RGB_PACKED(XBGR1555,
+ Plane<uint16_t,
+ Comp{ C::R, 5, 0 },
+ Comp{ C::G, 5, 5 },
+ Comp{ C::B, 5, 10 },
+ Comp{ C::X, 1, 15 }>);
+
+PIXPAT_RGB_PACKED(ABGR1555,
+ Plane<uint16_t,
+ Comp{ C::R, 5, 0 },
+ Comp{ C::G, 5, 5 },
+ Comp{ C::B, 5, 10 },
+ Comp{ C::A, 1, 15 }>);
+
+PIXPAT_RGB_PACKED(XRGB4444,
+ Plane<uint16_t,
+ Comp{ C::B, 4, 0 },
+ Comp{ C::G, 4, 4 },
+ Comp{ C::R, 4, 8 },
+ Comp{ C::X, 4, 12 }>);
+
+PIXPAT_RGB_PACKED(ARGB4444,
+ Plane<uint16_t,
+ Comp{ C::B, 4, 0 },
+ Comp{ C::G, 4, 4 },
+ Comp{ C::R, 4, 8 },
+ Comp{ C::A, 4, 12 }>);
+
+PIXPAT_RGB_PACKED(XBGR4444,
+ Plane<uint16_t,
+ Comp{ C::R, 4, 0 },
+ Comp{ C::G, 4, 4 },
+ Comp{ C::B, 4, 8 },
+ Comp{ C::X, 4, 12 }>);
+
+PIXPAT_RGB_PACKED(ABGR4444,
+ Plane<uint16_t,
+ Comp{ C::R, 4, 0 },
+ Comp{ C::G, 4, 4 },
+ Comp{ C::B, 4, 8 },
+ Comp{ C::A, 4, 12 }>);
+
+PIXPAT_RGB_PACKED(RGBX4444,
+ Plane<uint16_t,
+ Comp{ C::X, 4, 0 },
+ Comp{ C::B, 4, 4 },
+ Comp{ C::G, 4, 8 },
+ Comp{ C::R, 4, 12 }>);
+
+PIXPAT_RGB_PACKED(RGBA4444,
+ Plane<uint16_t,
+ Comp{ C::A, 4, 0 },
+ Comp{ C::B, 4, 4 },
+ Comp{ C::G, 4, 8 },
+ Comp{ C::R, 4, 12 }>);
+
+// ---------------------------------------------------------------------
+// 32-bit packed RGB, 10-bit components.
+// ---------------------------------------------------------------------
+
+PIXPAT_RGB_PACKED(XRGB2101010,
+ Plane<uint32_t,
+ Comp{ C::B, 10, 0 },
+ Comp{ C::G, 10, 10 },
+ Comp{ C::R, 10, 20 },
+ Comp{ C::X, 2, 30 }>);
+
+PIXPAT_RGB_PACKED(ARGB2101010,
+ Plane<uint32_t,
+ Comp{ C::B, 10, 0 },
+ Comp{ C::G, 10, 10 },
+ Comp{ C::R, 10, 20 },
+ Comp{ C::A, 2, 30 }>);
+
+PIXPAT_RGB_PACKED(XBGR2101010,
+ Plane<uint32_t,
+ Comp{ C::R, 10, 0 },
+ Comp{ C::G, 10, 10 },
+ Comp{ C::B, 10, 20 },
+ Comp{ C::X, 2, 30 }>);
+
+PIXPAT_RGB_PACKED(ABGR2101010,
+ Plane<uint32_t,
+ Comp{ C::R, 10, 0 },
+ Comp{ C::G, 10, 10 },
+ Comp{ C::B, 10, 20 },
+ Comp{ C::A, 2, 30 }>);
+
+PIXPAT_RGB_PACKED(RGBX1010102,
+ Plane<uint32_t,
+ Comp{ C::X, 2, 0 },
+ Comp{ C::B, 10, 2 },
+ Comp{ C::G, 10, 12 },
+ Comp{ C::R, 10, 22 }>);
+
+PIXPAT_RGB_PACKED(RGBA1010102,
+ Plane<uint32_t,
+ Comp{ C::A, 2, 0 },
+ Comp{ C::B, 10, 2 },
+ Comp{ C::G, 10, 12 },
+ Comp{ C::R, 10, 22 }>);
+
+PIXPAT_RGB_PACKED(BGRX1010102,
+ Plane<uint32_t,
+ Comp{ C::X, 2, 0 },
+ Comp{ C::R, 10, 2 },
+ Comp{ C::G, 10, 12 },
+ Comp{ C::B, 10, 22 }>);
+
+PIXPAT_RGB_PACKED(BGRA1010102,
+ Plane<uint32_t,
+ Comp{ C::A, 2, 0 },
+ Comp{ C::R, 10, 2 },
+ Comp{ C::G, 10, 12 },
+ Comp{ C::B, 10, 22 }>);
+
+// ---------------------------------------------------------------------
+// 64-bit normalized wide RGB (16 bits per component).
+// ---------------------------------------------------------------------
+
+PIXPAT_RGB_PACKED(ABGR16161616,
+ Plane<uint64_t,
+ Comp{ C::R, 16, 0 },
+ Comp{ C::G, 16, 16 },
+ Comp{ C::B, 16, 32 },
+ Comp{ C::A, 16, 48 }>);
+
+#undef PIXPAT_RGB_PACKED
+
+} // namespace pixpat::formats
diff --git a/subprojects/pixpat/pixpat-native/src/formats/yuv_packed.h b/subprojects/pixpat/pixpat-native/src/formats/yuv_packed.h
new file mode 100644
index 0000000..8e88f10
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/formats/yuv_packed.h
@@ -0,0 +1,136 @@
+#pragma once
+
+// Packed YUV layouts:
+// VUY888 — 1 pixel / 24-bit, 8-bit Y/U/V (storage uint32_t,
+// bytes_per_pixel = 3; parallels BGR888 in the YUV
+// register order)
+// XVUY8888 — 1 pixel / 32-bit word, 8-bit Y/U/V + 8-bit padding
+// XVUY2101010 — 1 pixel / 32-bit word, 10-bit Y/U/V + 2-bit padding
+// AVUY16161616 — 1 pixel / 64-bit word, 16-bit Y/U/V/A (normalized)
+// YUYV / YVYU / UYVY / VYUY — 4:2:2, 2 pixels / 32-bit word
+// Y210 / Y212 / Y216 — 4:2:2, 2 pixels / 64-bit word, with
+// each component MSB-aligned in a 16-bit slot
+//
+// XVUY/AVUY name is register MSB-first (X/A in the top bits). The
+// YUYV names follow V4L2 / pixpat memory-byte order (Y0 in byte 0),
+// so shifts ascend in name order — opposite of XRGB-style.
+
+#include "../layout.h"
+#include "../io/packed.h"
+#include "../io/packed_yuv.h"
+
+namespace pixpat::formats
+{
+
+// 1-pixel-per-word packed (single Pixel/Word; uses PackedSource/Sink).
+
+struct VUY888 : Layout<ColorKind::YUV, 1, 1,
+ Plane<uint32_t,
+ Comp{ C::Y, 8, 0 },
+ Comp{ C::U, 8, 8 },
+ Comp{ C::V, 8, 16 }> > {
+ using Source = PackedSource<VUY888>;
+ using Sink = PackedSink<VUY888>;
+};
+
+struct XVUY8888 : Layout<ColorKind::YUV, 1, 1,
+ Plane<uint32_t,
+ Comp{ C::Y, 8, 0 },
+ Comp{ C::U, 8, 8 },
+ Comp{ C::V, 8, 16 },
+ Comp{ C::X, 8, 24 }> > {
+ using Source = PackedSource<XVUY8888>;
+ using Sink = PackedSink<XVUY8888>;
+};
+
+struct XVUY2101010 : Layout<ColorKind::YUV, 1, 1,
+ Plane<uint32_t,
+ Comp{ C::Y, 10, 0 },
+ Comp{ C::U, 10, 10 },
+ Comp{ C::V, 10, 20 },
+ Comp{ C::X, 2, 30 }> > {
+ using Source = PackedSource<XVUY2101010>;
+ using Sink = PackedSink<XVUY2101010>;
+};
+
+struct AVUY16161616 : Layout<ColorKind::YUV, 1, 1,
+ Plane<uint64_t,
+ Comp{ C::Y, 16, 0 },
+ Comp{ C::U, 16, 16 },
+ Comp{ C::V, 16, 32 },
+ Comp{ C::A, 16, 48 }> > {
+ using Source = PackedSource<AVUY16161616>;
+ using Sink = PackedSink<AVUY16161616>;
+};
+
+// 2-pixel-per-word 4:2:2 (uses PackedYUVSource/Sink).
+
+#define PIXPAT_PACKED_YUV422(name, ...) \
+ struct name : Layout<ColorKind::YUV, 2, 1, \
+ Plane<uint32_t, __VA_ARGS__> > { \
+ using Source = PackedYUVSource<name>; \
+ using Sink = PackedYUVSink<name>; \
+ }
+
+PIXPAT_PACKED_YUV422(YUYV,
+ Comp{ C::Y, 8, 0 }, Comp{ C::U, 8, 8 },
+ Comp{ C::Y, 8, 16 }, Comp{ C::V, 8, 24 });
+
+PIXPAT_PACKED_YUV422(YVYU,
+ Comp{ C::Y, 8, 0 }, Comp{ C::V, 8, 8 },
+ Comp{ C::Y, 8, 16 }, Comp{ C::U, 8, 24 });
+
+PIXPAT_PACKED_YUV422(UYVY,
+ Comp{ C::U, 8, 0 }, Comp{ C::Y, 8, 8 },
+ Comp{ C::V, 8, 16 }, Comp{ C::Y, 8, 24 });
+
+PIXPAT_PACKED_YUV422(VYUY,
+ Comp{ C::V, 8, 0 }, Comp{ C::Y, 8, 8 },
+ Comp{ C::U, 8, 16 }, Comp{ C::Y, 8, 24 });
+
+#undef PIXPAT_PACKED_YUV422
+
+// Y210 / Y212 / Y216: 4:2:2, 2 pixels per 64-bit word, MSB-aligned in
+// 16-bit slots. Y210 has 6 unused LSBs per slot, Y212 has 4, Y216 has
+// none. The X padding entries pad total_bits to 64 so bytes_per_pixel
+// resolves to 8; PackedYUVSink leaves their slots zero via the
+// value-array zero-init (see io/packed_yuv.h).
+struct Y210 : Layout<ColorKind::YUV, 2, 1,
+ Plane<uint64_t,
+ Comp{ C::X, 6, 0 },
+ Comp{ C::Y, 10, 6 },
+ Comp{ C::X, 6, 16 },
+ Comp{ C::U, 10, 22 },
+ Comp{ C::X, 6, 32 },
+ Comp{ C::Y, 10, 38 },
+ Comp{ C::X, 6, 48 },
+ Comp{ C::V, 10, 54 }> > {
+ using Source = PackedYUVSource<Y210>;
+ using Sink = PackedYUVSink<Y210>;
+};
+
+struct Y212 : Layout<ColorKind::YUV, 2, 1,
+ Plane<uint64_t,
+ Comp{ C::X, 4, 0 },
+ Comp{ C::Y, 12, 4 },
+ Comp{ C::X, 4, 16 },
+ Comp{ C::U, 12, 20 },
+ Comp{ C::X, 4, 32 },
+ Comp{ C::Y, 12, 36 },
+ Comp{ C::X, 4, 48 },
+ Comp{ C::V, 12, 52 }> > {
+ using Source = PackedYUVSource<Y212>;
+ using Sink = PackedYUVSink<Y212>;
+};
+
+struct Y216 : Layout<ColorKind::YUV, 2, 1,
+ Plane<uint64_t,
+ Comp{ C::Y, 16, 0 },
+ Comp{ C::U, 16, 16 },
+ Comp{ C::Y, 16, 32 },
+ Comp{ C::V, 16, 48 }> > {
+ using Source = PackedYUVSource<Y216>;
+ using Sink = PackedYUVSink<Y216>;
+};
+
+} // namespace pixpat::formats
diff --git a/subprojects/pixpat/pixpat-native/src/formats/yuv_planar.h b/subprojects/pixpat/pixpat-native/src/formats/yuv_planar.h
new file mode 100644
index 0000000..bb6a415
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/formats/yuv_planar.h
@@ -0,0 +1,76 @@
+#pragma once
+
+// YUV planar layouts: 3 separate planes (Y, then U/V or V/U), 8-bit
+// components.
+// YUV420/YVU420 — h_sub=2, v_sub=2 (a.k.a. I420 / YV12)
+// YUV422/YVU422 — h_sub=2, v_sub=1
+// YUV444/YVU444 — h_sub=1, v_sub=1
+// T430 — multi-pixel-per-word planar 4:4:4.
+
+#include "../layout.h"
+#include "../io/planar.h"
+
+namespace pixpat::formats
+{
+
+#define PIXPAT_PLANAR(name, ...) \
+ struct name : Layout<ColorKind::YUV, __VA_ARGS__> { \
+ using Source = PlanarSource<name>; \
+ using Sink = PlanarSink<name>; \
+ }
+
+PIXPAT_PLANAR(YUV420, 2, 2,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::U, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::V, 8, 0 }>);
+
+PIXPAT_PLANAR(YVU420, 2, 2,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::V, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::U, 8, 0 }>);
+
+PIXPAT_PLANAR(YUV422, 2, 1,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::U, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::V, 8, 0 }>);
+
+PIXPAT_PLANAR(YVU422, 2, 1,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::V, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::U, 8, 0 }>);
+
+PIXPAT_PLANAR(YUV444, 1, 1,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::U, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::V, 8, 0 }>);
+
+PIXPAT_PLANAR(YVU444, 1, 1,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::V, 8, 0 }>,
+ Plane<uint8_t, Comp{ C::U, 8, 0 }>);
+
+#undef PIXPAT_PLANAR
+
+// T430: 3-plane multi-pixel-per-word planar 4:4:4. Each plane carries
+// 3 × 10-bit samples per uint32_t plus a 2-bit X padding bit-field.
+struct T430 : Layout<ColorKind::YUV, 1, 1,
+ Plane<uint32_t,
+ Comp{ C::Y, 10, 0 },
+ Comp{ C::Y, 10, 10 },
+ Comp{ C::Y, 10, 20 },
+ Comp{ C::X, 2, 30 }>,
+ Plane<uint32_t,
+ Comp{ C::U, 10, 0 },
+ Comp{ C::U, 10, 10 },
+ Comp{ C::U, 10, 20 },
+ Comp{ C::X, 2, 30 }>,
+ Plane<uint32_t,
+ Comp{ C::V, 10, 0 },
+ Comp{ C::V, 10, 10 },
+ Comp{ C::V, 10, 20 },
+ Comp{ C::X, 2, 30 }> > {
+ using Source = MultiPixelPlanarSource<T430>;
+ using Sink = MultiPixelPlanarSink<T430>;
+};
+
+} // namespace pixpat::formats
diff --git a/subprojects/pixpat/pixpat-native/src/formats/yuv_semiplanar.h b/subprojects/pixpat/pixpat-native/src/formats/yuv_semiplanar.h
new file mode 100644
index 0000000..34aea22
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/formats/yuv_semiplanar.h
@@ -0,0 +1,79 @@
+#pragma once
+
+// YUV semiplanar layouts: Y plane + interleaved UV plane.
+// NV12/NV21 — 4:2:0 (h_sub=2, v_sub=2)
+// NV16/NV61 — 4:2:2 (h_sub=2, v_sub=1)
+// P030/P230 — multi-pixel-per-word semiplanar (10-bit Y triplets).
+
+#include "../layout.h"
+#include "../io/semiplanar.h"
+
+namespace pixpat::formats
+{
+
+struct NV12 : Layout<ColorKind::YUV, 2, 2,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint16_t, Comp{ C::U, 8, 0 }, Comp{ C::V, 8, 8 }> > {
+ using Source = SemiplanarSource<NV12>;
+ using Sink = SemiplanarSink<NV12>;
+};
+
+struct NV21 : Layout<ColorKind::YUV, 2, 2,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint16_t, Comp{ C::V, 8, 0 }, Comp{ C::U, 8, 8 }> > {
+ using Source = SemiplanarSource<NV21>;
+ using Sink = SemiplanarSink<NV21>;
+};
+
+struct NV16 : Layout<ColorKind::YUV, 2, 1,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint16_t, Comp{ C::U, 8, 0 }, Comp{ C::V, 8, 8 }> > {
+ using Source = SemiplanarSource<NV16>;
+ using Sink = SemiplanarSink<NV16>;
+};
+
+struct NV61 : Layout<ColorKind::YUV, 2, 1,
+ Plane<uint8_t, Comp{ C::Y, 8, 0 }>,
+ Plane<uint16_t, Comp{ C::V, 8, 0 }, Comp{ C::U, 8, 8 }> > {
+ using Source = SemiplanarSource<NV61>;
+ using Sink = SemiplanarSink<NV61>;
+};
+
+// Multi-pixel-per-word semiplanar (P030: 4:2:0, P230: 4:2:2). Y plane
+// holds 3 × 10-bit Y samples per uint32_t (top 2 bits unused). UV plane
+// holds 3 × (Cb,Cr) pairs per uint64_t (10 bits each, with 2-bit gaps
+// at bits 30-31 and 62-63 — left implicit, no X declared).
+
+struct P030 : Layout<ColorKind::YUV, 2, 2,
+ Plane<uint32_t,
+ Comp{ C::Y, 10, 0 },
+ Comp{ C::Y, 10, 10 },
+ Comp{ C::Y, 10, 20 }>,
+ Plane<uint64_t,
+ Comp{ C::U, 10, 0 },
+ Comp{ C::V, 10, 10 },
+ Comp{ C::U, 10, 20 },
+ Comp{ C::V, 10, 32 },
+ Comp{ C::U, 10, 42 },
+ Comp{ C::V, 10, 52 }> > {
+ using Source = MultiPixelSemiplanarSource<P030>;
+ using Sink = MultiPixelSemiplanarSink<P030>;
+};
+
+struct P230 : Layout<ColorKind::YUV, 2, 1,
+ Plane<uint32_t,
+ Comp{ C::Y, 10, 0 },
+ Comp{ C::Y, 10, 10 },
+ Comp{ C::Y, 10, 20 }>,
+ Plane<uint64_t,
+ Comp{ C::U, 10, 0 },
+ Comp{ C::V, 10, 10 },
+ Comp{ C::U, 10, 20 },
+ Comp{ C::V, 10, 32 },
+ Comp{ C::U, 10, 42 },
+ Comp{ C::V, 10, 52 }> > {
+ using Source = MultiPixelSemiplanarSource<P230>;
+ using Sink = MultiPixelSemiplanarSink<P230>;
+};
+
+} // namespace pixpat::formats
diff --git a/subprojects/pixpat/pixpat-native/src/io.h b/subprojects/pixpat/pixpat-native/src/io.h
new file mode 100644
index 0000000..af24232
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io.h
@@ -0,0 +1,13 @@
+#pragma once
+
+// Aggregator: every Source / Sink template lives in one of the
+// per-iteration-shape headers under io/. Encode/decode helpers and
+// load_word/store_word are in io/detail.h, used by all the others.
+
+#include "io/detail.h"
+#include "io/packed.h"
+#include "io/semiplanar.h"
+#include "io/planar.h"
+#include "io/packed_yuv.h"
+#include "io/gray.h"
+#include "io/bayer.h"
diff --git a/subprojects/pixpat/pixpat-native/src/io/bayer.h b/subprojects/pixpat/pixpat-native/src/io/bayer.h
new file mode 100644
index 0000000..6b30c0e
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/bayer.h
@@ -0,0 +1,318 @@
+#pragma once
+
+// Bayer raw read/write support.
+//
+// Write side: each pixel carries one of R/G/B selected by (x mod 2,
+// y mod 2) and a fixed BayerOrder. Two missing channels per pixel are
+// dropped on encode.
+//
+// Read side: bilinear demosaic over a 3x3 window. The pixel's own
+// channel comes from self; missing channels are averaged from the
+// same-channel neighbours that the Bayer phase guarantees to exist:
+//
+// * At an R or B pixel, all four cardinal (N, E, S, W) neighbours
+// carry G and all four diagonal (NE, NW, SE, SW) neighbours carry
+// the other colour, so each missing channel averages four samples.
+// * At a G pixel, one missing colour sits in the row neighbours
+// (W, E) and the other in the column neighbours (N, S), so each
+// missing channel averages two samples.
+//
+// Sampled coordinates are clamped to the image bounds.
+//
+// The Layout shape is the same as a Y-only single-plane format
+// (storage carries one component plus optional X padding); the
+// BayerOrder is a separate template parameter on the Source / Sink.
+
+#include <array>
+#include <cstdint>
+
+#include "../layout.h"
+#include "csi2.h"
+#include "detail.h"
+
+namespace pixpat
+{
+
+enum class BayerOrder { RGGB, BGGR, GRBG, GBRG };
+
+namespace detail
+{
+constexpr C bayer_pick(BayerOrder o, bool x_even, bool y_even) noexcept
+{
+ switch (o) {
+ case BayerOrder::RGGB:
+ return y_even ? (x_even ? C::R : C::G)
+ : (x_even ? C::G : C::B);
+ case BayerOrder::BGGR:
+ return y_even ? (x_even ? C::B : C::G)
+ : (x_even ? C::G : C::R);
+ case BayerOrder::GRBG:
+ return y_even ? (x_even ? C::G : C::R)
+ : (x_even ? C::B : C::G);
+ case BayerOrder::GBRG:
+ return y_even ? (x_even ? C::G : C::B)
+ : (x_even ? C::R : C::G);
+ }
+ return C::G;
+}
+
+constexpr size_t clamp_coord(int v, size_t max_excl) noexcept
+{
+ if (v < 0)
+ return 0;
+ if (size_t(v) >= max_excl)
+ return max_excl - 1;
+ return size_t(v);
+}
+} // namespace detail
+
+template <typename L, BayerOrder Order>
+struct BayerSource {
+ using Layout = L;
+ using Pixel = RGB16;
+
+ static_assert(L::kind == ColorKind::RGB);
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t y_idx = P::template find_pos<C::Y>();
+ static_assert(y_idx < P::num_comps);
+
+ static uint16_t read_sample(const Buffer<1>& buf, size_t x, size_t y) noexcept
+ {
+ const uint8_t* p = buf.data[0] + y * buf.stride[0]
+ + x * P::bytes_per_pixel;
+ const auto vals = P::unpack(detail::load_word<P>(p));
+ return detail::decode_norm(P::comps[y_idx].bits, vals[y_idx]);
+ }
+
+ static RGB16 read(const Buffer<1>& buf, size_t x, size_t y,
+ size_t W, size_t H) noexcept
+ {
+ const bool x_even = (x & 1) == 0;
+ const bool y_even = (y & 1) == 0;
+ const C self = detail::bayer_pick(Order, x_even, y_even);
+
+ const size_t xL = detail::clamp_coord(int(x) - 1, W);
+ const size_t xR = detail::clamp_coord(int(x) + 1, W);
+ const size_t yT = detail::clamp_coord(int(y) - 1, H);
+ const size_t yB = detail::clamp_coord(int(y) + 1, H);
+
+ const uint16_t s = read_sample(buf, x, y);
+
+ uint16_t r = 0, g = 0, b = 0;
+
+ if (self == C::G) {
+ const C h_color = detail::bayer_pick(Order, !x_even, y_even);
+ const uint16_t h_avg = uint16_t(
+ (uint32_t(read_sample(buf, xL, y))
+ + read_sample(buf, xR, y) + 1u) >> 1);
+ const uint16_t v_avg = uint16_t(
+ (uint32_t(read_sample(buf, x, yT))
+ + read_sample(buf, x, yB) + 1u) >> 1);
+ g = s;
+ if (h_color == C::R) { r = h_avg; b = v_avg; }
+ else { b = h_avg; r = v_avg; }
+ } else {
+ const uint16_t g_avg = uint16_t(
+ (uint32_t(read_sample(buf, x, yT))
+ + read_sample(buf, x, yB)
+ + read_sample(buf, xL, y)
+ + read_sample(buf, xR, y) + 2u) >> 2);
+ const uint16_t o_avg = uint16_t(
+ (uint32_t(read_sample(buf, xL, yT))
+ + read_sample(buf, xR, yT)
+ + read_sample(buf, xL, yB)
+ + read_sample(buf, xR, yB) + 2u) >> 2);
+ g = g_avg;
+ if (self == C::R) { r = s; b = o_avg; }
+ else { b = s; r = o_avg; }
+ }
+
+ return RGB16{ r, g, b, uint16_t(0) };
+ }
+};
+
+template <typename L, BayerOrder Order>
+struct BayerSink {
+ using Layout = L;
+ using Pixel = RGB16;
+
+ static_assert(L::kind == ColorKind::RGB);
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t y_idx = P::template find_pos<C::Y>();
+ static constexpr size_t x_idx = P::template find_pos<C::X>();
+ static constexpr bool has_x = (x_idx < P::num_comps);
+ static_assert(y_idx < P::num_comps);
+
+ static constexpr size_t block_h = 1;
+ static constexpr size_t block_w = 1;
+
+ static void write_block(Buffer<1>& buf, size_t bx, size_t by,
+ const RGB16 (&block)[1][1]) noexcept
+ {
+ const C pick = detail::bayer_pick(Order, (bx & 1) == 0,
+ (by & 1) == 0);
+ const uint16_t val = pick == C::R ? block[0][0].r
+ : pick == C::G ? block[0][0].g
+ : block[0][0].b;
+
+ std::array<uint16_t, P::num_comps> v{};
+ v[y_idx] = detail::encode_norm(P::comps[y_idx].bits, val);
+ if constexpr (has_x)
+ v[x_idx] = 0;
+
+ uint8_t* p = buf.data[0] + by * buf.stride[0]
+ + bx * P::bytes_per_pixel;
+ detail::store_word<P>(p, P::pack(v));
+ }
+};
+
+// Aliases so X-macro can register without nested template-template params.
+template <typename L> using BayerSource_RGGB = BayerSource<L, BayerOrder::RGGB>;
+template <typename L> using BayerSource_BGGR = BayerSource<L, BayerOrder::BGGR>;
+template <typename L> using BayerSource_GRBG = BayerSource<L, BayerOrder::GRBG>;
+template <typename L> using BayerSource_GBRG = BayerSource<L, BayerOrder::GBRG>;
+
+template <typename L> using BayerSink_RGGB = BayerSink<L, BayerOrder::RGGB>;
+template <typename L> using BayerSink_BGGR = BayerSink<L, BayerOrder::BGGR>;
+template <typename L> using BayerSink_GRBG = BayerSink<L, BayerOrder::GRBG>;
+template <typename L> using BayerSink_GBRG = BayerSink<L, BayerOrder::GBRG>;
+
+// MIPI CSI-2 packed Bayer. The bit layout doesn't fit
+// `Plane<Storage, Comp...>` because each pixel's bits span two
+// non-contiguous bytes, so we use the shared CSI-2 helper (io/csi2.h)
+// to (un)pack samples.
+//
+// The Layout slot is a placeholder (matches the unpacked Bayer of the
+// same bit-depth so the user-facing API can pick the right buffer
+// shape); bytes_per_pixel from the Plane is unused.
+template <typename L, BayerOrder Order, size_t BitDepth>
+struct BayerPackedSource {
+ using Layout = L;
+ using Pixel = RGB16;
+
+ static_assert(L::kind == ColorKind::RGB);
+ static_assert(L::num_planes == 1);
+ static_assert(BitDepth == 10 || BitDepth == 12);
+
+ using Traits = detail::csi2::packed_traits<BitDepth>;
+ static constexpr size_t ppg = Traits::ppg;
+ static constexpr size_t bpg = Traits::bpg;
+
+ // Stored N-bit value upshifts to normalized-16 by `<< (16-N)`,
+ // matching the unpacked Bayer source.
+ static constexpr unsigned shift = 16 - BitDepth;
+
+ static uint16_t read_sample(const Buffer<1>& buf, size_t x, size_t y) noexcept
+ {
+ const uint8_t* src = buf.data[0] + y * buf.stride[0]
+ + (x / ppg) * bpg;
+ const uint16_t val = detail::csi2::unpack_sample<BitDepth>(src, x % ppg);
+ return uint16_t(val << shift);
+ }
+
+ static RGB16 read(const Buffer<1>& buf, size_t x, size_t y,
+ size_t W, size_t H) noexcept
+ {
+ const bool x_even = (x & 1) == 0;
+ const bool y_even = (y & 1) == 0;
+ const C self = detail::bayer_pick(Order, x_even, y_even);
+
+ const size_t xL = detail::clamp_coord(int(x) - 1, W);
+ const size_t xR = detail::clamp_coord(int(x) + 1, W);
+ const size_t yT = detail::clamp_coord(int(y) - 1, H);
+ const size_t yB = detail::clamp_coord(int(y) + 1, H);
+
+ const uint16_t s = read_sample(buf, x, y);
+
+ uint16_t r = 0, g = 0, b = 0;
+
+ if (self == C::G) {
+ const C h_color = detail::bayer_pick(Order, !x_even, y_even);
+ const uint16_t h_avg = uint16_t(
+ (uint32_t(read_sample(buf, xL, y))
+ + read_sample(buf, xR, y) + 1u) >> 1);
+ const uint16_t v_avg = uint16_t(
+ (uint32_t(read_sample(buf, x, yT))
+ + read_sample(buf, x, yB) + 1u) >> 1);
+ g = s;
+ if (h_color == C::R) { r = h_avg; b = v_avg; }
+ else { b = h_avg; r = v_avg; }
+ } else {
+ const uint16_t g_avg = uint16_t(
+ (uint32_t(read_sample(buf, x, yT))
+ + read_sample(buf, x, yB)
+ + read_sample(buf, xL, y)
+ + read_sample(buf, xR, y) + 2u) >> 2);
+ const uint16_t o_avg = uint16_t(
+ (uint32_t(read_sample(buf, xL, yT))
+ + read_sample(buf, xR, yT)
+ + read_sample(buf, xL, yB)
+ + read_sample(buf, xR, yB) + 2u) >> 2);
+ g = g_avg;
+ if (self == C::R) { r = s; b = o_avg; }
+ else { b = s; r = o_avg; }
+ }
+
+ return RGB16{ r, g, b, uint16_t(0) };
+ }
+};
+
+template <typename L, BayerOrder Order, size_t BitDepth>
+struct BayerPackedSink {
+ using Layout = L;
+ using Pixel = RGB16;
+
+ static_assert(L::kind == ColorKind::RGB);
+ static_assert(L::num_planes == 1);
+ static_assert(BitDepth == 10 || BitDepth == 12);
+
+ using Traits = detail::csi2::packed_traits<BitDepth>;
+ static constexpr size_t ppg = Traits::ppg;
+ static constexpr size_t bpg = Traits::bpg;
+
+ static constexpr size_t block_h = 1;
+ static constexpr size_t block_w = ppg;
+
+ static void write_block(Buffer<1>& buf, size_t bx, size_t by,
+ const RGB16 (&block)[1][ppg]) noexcept
+ {
+ std::array<uint16_t, ppg> vals{};
+ for (size_t i = 0; i < ppg; ++i) {
+ const C pick = detail::bayer_pick(
+ Order, ((bx + i) & 1) == 0, (by & 1) == 0);
+ const uint16_t norm =
+ pick == C::R ? block[0][i].r
+ : pick == C::G ? block[0][i].g
+ : block[0][i].b;
+ vals[i] = uint16_t(norm >> (16 - BitDepth));
+ }
+
+ uint8_t* dst = buf.data[0] + by * buf.stride[0]
+ + (bx / ppg) * bpg;
+ detail::csi2::pack_group<BitDepth>(dst, vals);
+ }
+};
+
+template <typename L> using BayerPackedSource_RGGB10 = BayerPackedSource<L, BayerOrder::RGGB, 10>;
+template <typename L> using BayerPackedSource_BGGR10 = BayerPackedSource<L, BayerOrder::BGGR, 10>;
+template <typename L> using BayerPackedSource_GRBG10 = BayerPackedSource<L, BayerOrder::GRBG, 10>;
+template <typename L> using BayerPackedSource_GBRG10 = BayerPackedSource<L, BayerOrder::GBRG, 10>;
+template <typename L> using BayerPackedSource_RGGB12 = BayerPackedSource<L, BayerOrder::RGGB, 12>;
+template <typename L> using BayerPackedSource_BGGR12 = BayerPackedSource<L, BayerOrder::BGGR, 12>;
+template <typename L> using BayerPackedSource_GRBG12 = BayerPackedSource<L, BayerOrder::GRBG, 12>;
+template <typename L> using BayerPackedSource_GBRG12 = BayerPackedSource<L, BayerOrder::GBRG, 12>;
+
+template <typename L> using BayerPackedSink_RGGB10 = BayerPackedSink<L, BayerOrder::RGGB, 10>;
+template <typename L> using BayerPackedSink_BGGR10 = BayerPackedSink<L, BayerOrder::BGGR, 10>;
+template <typename L> using BayerPackedSink_GRBG10 = BayerPackedSink<L, BayerOrder::GRBG, 10>;
+template <typename L> using BayerPackedSink_GBRG10 = BayerPackedSink<L, BayerOrder::GBRG, 10>;
+template <typename L> using BayerPackedSink_RGGB12 = BayerPackedSink<L, BayerOrder::RGGB, 12>;
+template <typename L> using BayerPackedSink_BGGR12 = BayerPackedSink<L, BayerOrder::BGGR, 12>;
+template <typename L> using BayerPackedSink_GRBG12 = BayerPackedSink<L, BayerOrder::GRBG, 12>;
+template <typename L> using BayerPackedSink_GBRG12 = BayerPackedSink<L, BayerOrder::GBRG, 12>;
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/io/csi2.h b/subprojects/pixpat/pixpat-native/src/io/csi2.h
new file mode 100644
index 0000000..59a8f8d
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/csi2.h
@@ -0,0 +1,80 @@
+#pragma once
+
+// Shared MIPI CSI-2 byte (un)packing for the 10P / 12P forms used by
+// Bayer raw and Y-only grayscale.
+//
+// 10P: 4 samples in 5 bytes — bytes 0..3 hold the high 8 bits of
+// samples 0..3; byte 4 holds 4 x 2 LSBs (sample 0 in bits 6..7,
+// sample 1 in bits 4..5, ...).
+// 12P: 2 samples in 3 bytes — bytes 0..1 hold the high 8 bits of
+// samples 0..1; byte 2 holds 2 x 4 LSBs (sample 0 in bits 4..7,
+// sample 1 in bits 0..3).
+//
+// Helpers deal in the stored integer (low BitDepth bits set);
+// normalization to/from the 16-bit pivot stays in the caller.
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+
+namespace pixpat::detail::csi2
+{
+
+template <size_t BitDepth>
+struct packed_traits;
+
+template <>
+struct packed_traits<10> {
+ static constexpr size_t ppg = 4;
+ static constexpr size_t bpg = 5;
+};
+
+template <>
+struct packed_traits<12> {
+ static constexpr size_t ppg = 2;
+ static constexpr size_t bpg = 3;
+};
+
+// Extract one BitDepth-bit sample from a packed group, where `i` is the
+// in-group index (0..ppg-1). The returned value occupies the low
+// BitDepth bits.
+template <size_t BitDepth>
+inline uint16_t unpack_sample(const uint8_t* src, size_t i) noexcept
+{
+ if constexpr (BitDepth == 10) {
+ const uint8_t hi = src[i];
+ const uint8_t lsb = (src[4] >> ((3 - i) * 2)) & 0x03;
+ return uint16_t((hi << 2) | lsb);
+ } else { // 12
+ const uint8_t hi = src[i];
+ const uint8_t lsb = (i == 0) ? ((src[2] >> 4) & 0x0F)
+ : (src[2] & 0x0F);
+ return uint16_t((hi << 4) | lsb);
+ }
+}
+
+// Write `ppg` BitDepth-bit samples (low BitDepth bits significant) into
+// a packed group of `bpg` bytes.
+template <size_t BitDepth>
+inline void pack_group(
+ uint8_t* dst,
+ const std::array<uint16_t, packed_traits<BitDepth>::ppg>& vals) noexcept
+{
+ if constexpr (BitDepth == 10) {
+ dst[0] = (vals[0] >> 2) & 0xFF;
+ dst[1] = (vals[1] >> 2) & 0xFF;
+ dst[2] = (vals[2] >> 2) & 0xFF;
+ dst[3] = (vals[3] >> 2) & 0xFF;
+ dst[4] = ((vals[0] & 0x03) << 6)
+ | ((vals[1] & 0x03) << 4)
+ | ((vals[2] & 0x03) << 2)
+ | ((vals[3] & 0x03) << 0);
+ } else { // 12
+ dst[0] = (vals[0] >> 4) & 0xFF;
+ dst[1] = (vals[1] >> 4) & 0xFF;
+ dst[2] = ((vals[0] & 0x0F) << 4)
+ | ((vals[1] & 0x0F) << 0);
+ }
+}
+
+} // namespace pixpat::detail::csi2
diff --git a/subprojects/pixpat/pixpat-native/src/io/detail.h b/subprojects/pixpat/pixpat-native/src/io/detail.h
new file mode 100644
index 0000000..cb2b9fb
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/detail.h
@@ -0,0 +1,62 @@
+#pragma once
+
+// Per-component encode/decode against the descriptor + memcpy-based
+// load/store_word helpers. Shared by every Source / Sink template.
+
+#include <cstdint>
+#include <cstring>
+
+#include "../layout.h"
+
+namespace pixpat::detail
+{
+
+// Decode an N-bit stored value into the 16-bit normalized space and
+// encode it back. Decode bit-replicates the stored value across the 16
+// bits so that N-bit max maps to normalized max (e.g. 8-bit 0xFF →
+// 0xFFFF, not 0xFF00). Encode is a plain truncating right-shift: the
+// replicated bits land in the low (16-N) bits and get dropped, so
+// stored→norm→stored is exact for any N in [1, 16].
+//
+// `bits` is taken at runtime; in every call site it traces back to a
+// constexpr Plane::comps[I].bits read, which the optimizer constant-
+// folds after inlining.
+
+constexpr uint16_t decode_norm(unsigned bits, uint16_t stored) noexcept
+{
+ const int N = int(bits);
+ // Loop, not a single OR: one replication only covers 2N bits, so
+ // N < 8 (RGB565, RGBA4444, 1-bit alpha, ...) needs multiple tiles.
+ uint32_t result = 0;
+ for (int s = 16 - N; s > -N; s -= N) {
+ if (s >= 0)
+ result |= uint32_t(stored) << s;
+ else
+ result |= uint32_t(stored) >> -s;
+ }
+ return uint16_t(result);
+}
+
+constexpr uint16_t encode_norm(unsigned bits, uint16_t norm) noexcept
+{
+ return uint16_t(norm >> (16u - bits));
+}
+
+// Read one storage word from `p`. memcpy is uniform for tight and
+// non-tight (e.g. BGR888 24-bit) layouts; the optimizer folds it to a
+// single load when the size is constant.
+template <typename Plane>
+inline typename Plane::storage_t load_word(const uint8_t* p) noexcept
+{
+ typename Plane::storage_t word{};
+ std::memcpy(&word, p, Plane::bytes_per_pixel);
+ return word;
+}
+
+template <typename Plane>
+inline void store_word(uint8_t* p, typename Plane::storage_t word) noexcept
+{
+ std::memcpy(p, &word, Plane::bytes_per_pixel);
+}
+
+} // namespace pixpat::detail
diff --git a/subprojects/pixpat/pixpat-native/src/io/gray.h b/subprojects/pixpat/pixpat-native/src/io/gray.h
new file mode 100644
index 0000000..d175b68
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/gray.h
@@ -0,0 +1,153 @@
+#pragma once
+
+// Grayscale (Y8 / Y10 / Y12 / Y16) and multi-pixel-per-word grayscale
+// (XYYY2101010: 3 Y components in one uint32_t). Modeled as a YUV format
+// with neutral chroma synthesized on read so cross-color-kind ColorXfm
+// produces R=G=B=Y'. The sink encodes Y from YUV16 and ignores U/V.
+// Y10/Y12 carry an X padding bitfield which we zero out on write.
+// Neutral chroma in normalized-16 is 0x8000 (the midpoint of [0, 0xFFFF]).
+
+#include <array>
+
+#include "../layout.h"
+#include "detail.h"
+
+namespace pixpat
+{
+
+template <typename L>
+struct GraySource {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t y_idx = P::template find_pos<C::Y>();
+ static_assert(y_idx < P::num_comps);
+
+ static YUV16 read(const Buffer<1>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ const uint8_t* p = buf.data[0] + y * buf.stride[0]
+ + x * P::bytes_per_pixel;
+ const auto vals = P::unpack(detail::load_word<P>(p));
+ return YUV16{
+ detail::decode_norm(P::comps[y_idx].bits, vals[y_idx]),
+ 0x8000, 0x8000, uint16_t(0),
+ };
+ }
+};
+
+template <typename L>
+struct GraySink {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t y_idx = P::template find_pos<C::Y>();
+ static constexpr size_t x_idx = P::template find_pos<C::X>();
+ static constexpr bool has_x = (x_idx < P::num_comps);
+ static_assert(y_idx < P::num_comps);
+
+ static constexpr size_t block_h = 1;
+ static constexpr size_t block_w = 1;
+
+ static void write_block(Buffer<1>& buf, size_t bx, size_t by,
+ const YUV16 (&block)[1][1]) noexcept
+ {
+ std::array<uint16_t, P::num_comps> v{};
+ v[y_idx] = detail::encode_norm(P::comps[y_idx].bits, block[0][0].y);
+ if constexpr (has_x)
+ v[x_idx] = 0;
+
+ uint8_t* p = buf.data[0] + by * buf.stride[0]
+ + bx * P::bytes_per_pixel;
+ detail::store_word<P>(p, P::pack(v));
+ }
+};
+
+// Multi-pixel-per-word grayscale. The Layout carries one C::Y entry per
+// pixel in the group; pixels_per_word is derived from how many C::Y
+// entries the layout has. All Y components must share the same bit width
+// (so the encode/decode shift is shared). block_w = ppw so the sink
+// writes one storage word per block.
+template <typename L>
+struct MultiPixelGraySource {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t ppw = P::template component_count<C::Y>();
+ static_assert(ppw >= 1);
+
+ // All Y positions share the same bit width.
+ static constexpr unsigned y_bits = P::comps[P::template find_pos<C::Y>(0)].bits;
+
+ static YUV16 read(const Buffer<1>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ const size_t gx = x / ppw;
+ const size_t off = x % ppw;
+ const uint8_t* p = buf.data[0] + y * buf.stride[0]
+ + gx * P::bytes_per_pixel;
+ const auto vals = P::unpack(detail::load_word<P>(p));
+
+ // find_pos walks the comps array at runtime; comps is constexpr
+ // and num_comps is small (≤4 for these formats), so it inlines.
+ const size_t y_pos = P::template find_pos<C::Y>(off);
+
+ return YUV16{
+ detail::decode_norm(y_bits, vals[y_pos]),
+ 0x8000, 0x8000, uint16_t(0),
+ };
+ }
+};
+
+template <typename L>
+struct MultiPixelGraySink {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t ppw = P::template component_count<C::Y>();
+ static constexpr size_t x_idx = P::template find_pos<C::X>();
+ static constexpr bool has_x = (x_idx < P::num_comps);
+ static_assert(ppw >= 1);
+
+ static constexpr size_t block_h = 1;
+ static constexpr size_t block_w = ppw;
+
+ static void write_block(Buffer<1>& buf, size_t bx, size_t by,
+ const YUV16 (&block)[1][ppw]) noexcept
+ {
+ std::array<uint16_t, P::num_comps> v{};
+ // All Y slots share the same bit width.
+ constexpr unsigned y_bits = P::comps[P::template find_pos<C::Y>(0)].bits;
+ for (size_t i = 0; i < ppw; ++i) {
+ const size_t pos = P::template find_pos<C::Y>(i);
+ v[pos] = detail::encode_norm(y_bits, block[0][i].y);
+ }
+
+ if constexpr (has_x)
+ v[x_idx] = 0;
+
+ uint8_t* p = buf.data[0] + by * buf.stride[0]
+ + (bx / ppw) * P::bytes_per_pixel;
+ detail::store_word<P>(p, P::pack(v));
+ }
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/io/gray_packed.h b/subprojects/pixpat/pixpat-native/src/io/gray_packed.h
new file mode 100644
index 0000000..dc1fa68
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/gray_packed.h
@@ -0,0 +1,78 @@
+#pragma once
+
+// MIPI CSI-2 packed grayscale (Y10P / Y12P). Same byte packing as
+// Bayer10P/Bayer12P (see io/csi2.h) but every sample is Y; the source
+// emits neutral chroma to keep cross-color-kind ColorXfm consistent
+// with GraySource.
+//
+// The Layout slot is a placeholder (matches the unpacked Y8 storage
+// shape so dispatch plumbing is uniform); bytes_per_pixel from the
+// Plane is unused.
+
+#include <array>
+#include <cstdint>
+
+#include "../layout.h"
+#include "csi2.h"
+
+namespace pixpat
+{
+
+template <typename L, size_t BitDepth>
+struct GrayPackedSource {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 1);
+ static_assert(BitDepth == 10 || BitDepth == 12);
+
+ using Traits = detail::csi2::packed_traits<BitDepth>;
+ static constexpr size_t ppg = Traits::ppg;
+ static constexpr size_t bpg = Traits::bpg;
+ static constexpr unsigned shift = 16 - BitDepth;
+
+ static YUV16 read(const Buffer<1>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ const uint8_t* src = buf.data[0] + y * buf.stride[0]
+ + (x / ppg) * bpg;
+ const uint16_t val = detail::csi2::unpack_sample<BitDepth>(src, x % ppg);
+ return YUV16{
+ uint16_t(val << shift),
+ 0x8000, 0x8000, uint16_t(0),
+ };
+ }
+};
+
+template <typename L, size_t BitDepth>
+struct GrayPackedSink {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 1);
+ static_assert(BitDepth == 10 || BitDepth == 12);
+
+ using Traits = detail::csi2::packed_traits<BitDepth>;
+ static constexpr size_t ppg = Traits::ppg;
+ static constexpr size_t bpg = Traits::bpg;
+
+ static constexpr size_t block_h = 1;
+ static constexpr size_t block_w = ppg;
+
+ static void write_block(Buffer<1>& buf, size_t bx, size_t by,
+ const YUV16 (&block)[1][ppg]) noexcept
+ {
+ std::array<uint16_t, ppg> vals{};
+ for (size_t i = 0; i < ppg; ++i)
+ vals[i] = uint16_t(block[0][i].y >> (16 - BitDepth));
+
+ uint8_t* dst = buf.data[0] + by * buf.stride[0]
+ + (bx / ppg) * bpg;
+ detail::csi2::pack_group<BitDepth>(dst, vals);
+ }
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/io/mono_rgb.h b/subprojects/pixpat/pixpat-native/src/io/mono_rgb.h
new file mode 100644
index 0000000..f2f8206
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/mono_rgb.h
@@ -0,0 +1,72 @@
+#pragma once
+
+// Single-channel RGB formats (R8). Storage carries one R component;
+// MonoRGBSource synthesizes G=B=R on read so cross-color-kind ColorXfm
+// produces sensible Y from R alone. MonoRGBSink encodes R and ignores
+// G/B/A (and zeroes any X padding). Symmetric to GraySource/GraySink
+// (io/gray.h) but for ColorKind::RGB on C::R.
+
+#include <array>
+
+#include "../layout.h"
+#include "detail.h"
+
+namespace pixpat
+{
+
+template <typename L>
+struct MonoRGBSource {
+ using Layout = L;
+ using Pixel = RGB16;
+
+ static_assert(L::kind == ColorKind::RGB);
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t r_idx = P::template find_pos<C::R>();
+ static_assert(r_idx < P::num_comps);
+
+ static RGB16 read(const Buffer<1>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ const uint8_t* p = buf.data[0] + y * buf.stride[0]
+ + x * P::bytes_per_pixel;
+ const auto vals = P::unpack(detail::load_word<P>(p));
+ const uint16_t r = detail::decode_norm(P::comps[r_idx].bits, vals[r_idx]);
+ return RGB16{ r, r, r, uint16_t(0) };
+ }
+};
+
+template <typename L>
+struct MonoRGBSink {
+ using Layout = L;
+ using Pixel = RGB16;
+
+ static_assert(L::kind == ColorKind::RGB);
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t r_idx = P::template find_pos<C::R>();
+ static constexpr size_t x_idx = P::template find_pos<C::X>();
+ static constexpr bool has_x = (x_idx < P::num_comps);
+ static_assert(r_idx < P::num_comps);
+
+ static constexpr size_t block_h = 1;
+ static constexpr size_t block_w = 1;
+
+ static void write_block(Buffer<1>& buf, size_t bx, size_t by,
+ const RGB16 (&block)[1][1]) noexcept
+ {
+ std::array<uint16_t, P::num_comps> v{};
+ v[r_idx] = detail::encode_norm(P::comps[r_idx].bits, block[0][0].r);
+ if constexpr (has_x)
+ v[x_idx] = 0;
+
+ uint8_t* p = buf.data[0] + by * buf.stride[0]
+ + bx * P::bytes_per_pixel;
+ detail::store_word<P>(p, P::pack(v));
+ }
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/io/packed.h b/subprojects/pixpat/pixpat-native/src/io/packed.h
new file mode 100644
index 0000000..9d953bc
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/packed.h
@@ -0,0 +1,106 @@
+#pragma once
+
+// Single-plane, single-pixel-per-storage-word formats. Works for both
+// RGB layouts (XRGB8888, RGB565, ABGR16161616, ...) and YUV
+// single-pixel layouts (XVUY2101010, AVUY16161616). Pixel type follows
+// L::kind; the three mandatory components are R/G/B for RGB or Y/U/V
+// for YUV. Both `RGB16` and `YUV16` are 4 uint16_t with the alpha last,
+// so aggregate-init by position works for either.
+
+#include <array>
+#include <type_traits>
+
+#include "../layout.h"
+#include "detail.h"
+
+namespace pixpat
+{
+
+template <typename L>
+struct PackedSource {
+ using Layout = L;
+ using Pixel = std::conditional_t<L::kind == ColorKind::RGB, RGB16, YUV16>;
+
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr C c0 = (L::kind == ColorKind::RGB) ? C::R : C::Y;
+ static constexpr C c1 = (L::kind == ColorKind::RGB) ? C::G : C::U;
+ static constexpr C c2 = (L::kind == ColorKind::RGB) ? C::B : C::V;
+
+ static constexpr size_t i0 = P::template find_pos<c0>();
+ static constexpr size_t i1 = P::template find_pos<c1>();
+ static constexpr size_t i2 = P::template find_pos<c2>();
+ static constexpr size_t a_idx = P::template find_pos<C::A>();
+ static constexpr bool has_a = (a_idx < P::num_comps);
+ static_assert(i0 < P::num_comps && i1 < P::num_comps && i2 < P::num_comps);
+
+ static Pixel read(const Buffer<1>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ const uint8_t* p = buf.data[0] + y * buf.stride[0] + x * P::bytes_per_pixel;
+ const auto vals = P::unpack(detail::load_word<P>(p));
+ Pixel out{
+ detail::decode_norm(P::comps[i0].bits, vals[i0]),
+ detail::decode_norm(P::comps[i1].bits, vals[i1]),
+ detail::decode_norm(P::comps[i2].bits, vals[i2]),
+ uint16_t(0),
+ };
+ if constexpr (has_a)
+ out.a = detail::decode_norm(P::comps[a_idx].bits, vals[a_idx]);
+ return out;
+ }
+};
+
+template <typename L>
+struct PackedSink {
+ using Layout = L;
+ using Pixel = std::conditional_t<L::kind == ColorKind::RGB, RGB16, YUV16>;
+
+ static_assert(L::num_planes == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr C c0 = (L::kind == ColorKind::RGB) ? C::R : C::Y;
+ static constexpr C c1 = (L::kind == ColorKind::RGB) ? C::G : C::U;
+ static constexpr C c2 = (L::kind == ColorKind::RGB) ? C::B : C::V;
+
+ static constexpr size_t i0 = P::template find_pos<c0>();
+ static constexpr size_t i1 = P::template find_pos<c1>();
+ static constexpr size_t i2 = P::template find_pos<c2>();
+ static constexpr size_t x_idx = P::template find_pos<C::X>();
+ static constexpr size_t a_idx = P::template find_pos<C::A>();
+ static constexpr bool has_x = (x_idx < P::num_comps);
+ static constexpr bool has_a = (a_idx < P::num_comps);
+ static_assert(i0 < P::num_comps && i1 < P::num_comps && i2 < P::num_comps);
+
+ static constexpr size_t block_h = 1;
+ static constexpr size_t block_w = 1;
+
+ // Aggregate-init access to RGB16/YUV16 by position: .r/.y, .g/.u, .b/.v.
+ // We use the field names corresponding to L::kind.
+ static void write_block(Buffer<1>& buf, size_t bx, size_t by,
+ const Pixel (&block)[1][1]) noexcept
+ {
+ const Pixel& pix = block[0][0];
+ std::array<uint16_t, P::num_comps> v{};
+ if constexpr (L::kind == ColorKind::RGB) {
+ v[i0] = detail::encode_norm(P::comps[i0].bits, pix.r);
+ v[i1] = detail::encode_norm(P::comps[i1].bits, pix.g);
+ v[i2] = detail::encode_norm(P::comps[i2].bits, pix.b);
+ } else {
+ v[i0] = detail::encode_norm(P::comps[i0].bits, pix.y);
+ v[i1] = detail::encode_norm(P::comps[i1].bits, pix.u);
+ v[i2] = detail::encode_norm(P::comps[i2].bits, pix.v);
+ }
+ if constexpr (has_x)
+ v[x_idx] = 0;
+ if constexpr (has_a)
+ v[a_idx] = detail::encode_norm(P::comps[a_idx].bits, pix.a);
+
+ uint8_t* p = buf.data[0] + by * buf.stride[0] + bx * P::bytes_per_pixel;
+ detail::store_word<P>(p, P::pack(v));
+ }
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/io/packed_yuv.h b/subprojects/pixpat/pixpat-native/src/io/packed_yuv.h
new file mode 100644
index 0000000..90c8b2f
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/packed_yuv.h
@@ -0,0 +1,89 @@
+#pragma once
+
+// Packed YUV 4:2:2 (YUYV / YVYU / UYVY / VYUY): two pixels per 32-bit
+// word, one shared chroma pair. The Layout uses two C::Y entries plus
+// one each of C::U / C::V; we resolve the duplicate Y via
+// find_pos<C::Y>(n).
+
+#include <array>
+
+#include "../layout.h"
+#include "detail.h"
+
+namespace pixpat
+{
+
+template <typename L>
+struct PackedYUVSource {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 1);
+ static_assert(L::h_sub == 2 && L::v_sub == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t y0_idx = P::template find_pos<C::Y>(0);
+ static constexpr size_t y1_idx = P::template find_pos<C::Y>(1);
+ static constexpr size_t u_idx = P::template find_pos<C::U>();
+ static constexpr size_t v_idx = P::template find_pos<C::V>();
+
+ static YUV16 read(const Buffer<1>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ const uint8_t* p = buf.data[0] + y * buf.stride[0]
+ + (x / 2) * P::bytes_per_pixel;
+ const auto vals = P::unpack(detail::load_word<P>(p));
+ const size_t y_pick = (x & 1) ? y1_idx : y0_idx;
+ // Both Y components share the same bit width, so the bit-width
+ // for y0 and y1 is identical — pick either.
+ return YUV16{
+ detail::decode_norm(P::comps[y0_idx].bits, vals[y_pick]),
+ detail::decode_norm(P::comps[u_idx].bits, vals[u_idx]),
+ detail::decode_norm(P::comps[v_idx].bits, vals[v_idx]),
+ uint16_t(0),
+ };
+ }
+};
+
+template <typename L>
+struct PackedYUVSink {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 1);
+ static_assert(L::h_sub == 2 && L::v_sub == 1);
+
+ using P = typename L::template plane<0>;
+ static constexpr size_t y0_idx = P::template find_pos<C::Y>(0);
+ static constexpr size_t y1_idx = P::template find_pos<C::Y>(1);
+ static constexpr size_t u_idx = P::template find_pos<C::U>();
+ static constexpr size_t v_idx = P::template find_pos<C::V>();
+
+ static constexpr size_t block_h = 1;
+ static constexpr size_t block_w = 2;
+
+ static void write_block(Buffer<1>& buf, size_t bx, size_t by,
+ const YUV16 (&block)[1][2]) noexcept
+ {
+ std::array<uint16_t, P::num_comps> v{};
+ v[y0_idx] = detail::encode_norm(P::comps[y0_idx].bits, block[0][0].y);
+ v[y1_idx] = detail::encode_norm(P::comps[y1_idx].bits, block[0][1].y);
+ // Integer chroma averaging in normalized-16 space. Truncates
+ // (no round-half-up).
+ v[u_idx] = detail::encode_norm(P::comps[u_idx].bits, uint16_t(
+ (uint32_t(block[0][0].u) +
+ uint32_t(block[0][1].u)) / 2));
+ v[v_idx] = detail::encode_norm(P::comps[v_idx].bits, uint16_t(
+ (uint32_t(block[0][0].v) +
+ uint32_t(block[0][1].v)) / 2));
+
+ uint8_t* p = buf.data[0] + by * buf.stride[0]
+ + (bx / 2) * P::bytes_per_pixel;
+ detail::store_word<P>(p, P::pack(v));
+ }
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/io/planar.h b/subprojects/pixpat/pixpat-native/src/io/planar.h
new file mode 100644
index 0000000..0dab685
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/planar.h
@@ -0,0 +1,257 @@
+#pragma once
+
+// 3-plane planar YUV. Two flavours:
+//
+// PlanarSource / PlanarSink — YUV/YVU 420/422/444, single Y per word,
+// single chroma per word. Chroma is averaged over h_sub × v_sub
+// on write.
+//
+// MultiPixelPlanarSource / MultiPixelPlanarSink — T430, multi-pixel-
+// per-word planar 4:4:4 (3 samples per uint32_t in each of 3
+// planes, plus 2-bit X padding). block_w = ppw, block_h = 1.
+//
+// Plane indices for Y / U / V are looked up via Layout::find_plane<C>(),
+// so swap_uv layouts (YVU vs YUV) work without separate templates.
+
+#include <array>
+
+#include "../layout.h"
+#include "detail.h"
+
+namespace pixpat
+{
+
+template <typename L>
+struct PlanarSource {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 3);
+
+ static constexpr size_t y_plane = L::template find_plane<C::Y>();
+ static constexpr size_t u_plane = L::template find_plane<C::U>();
+ static constexpr size_t v_plane = L::template find_plane<C::V>();
+
+ using YP = typename L::template plane<y_plane>;
+ using UP = typename L::template plane<u_plane>;
+ using VP = typename L::template plane<v_plane>;
+
+ static YUV16 read(const Buffer<3>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ const uint8_t* yp = buf.data[y_plane] + y * buf.stride[y_plane]
+ + x * YP::bytes_per_pixel;
+ const auto y_vals = YP::unpack(detail::load_word<YP>(yp));
+
+ const size_t cx = x / L::h_sub;
+ const size_t cy = y / L::v_sub;
+ const uint8_t* up = buf.data[u_plane] + cy * buf.stride[u_plane]
+ + cx * UP::bytes_per_pixel;
+ const uint8_t* vp = buf.data[v_plane] + cy * buf.stride[v_plane]
+ + cx * VP::bytes_per_pixel;
+ const auto u_vals = UP::unpack(detail::load_word<UP>(up));
+ const auto v_vals = VP::unpack(detail::load_word<VP>(vp));
+
+ return YUV16{
+ detail::decode_norm(YP::comps[0].bits, y_vals[0]),
+ detail::decode_norm(UP::comps[0].bits, u_vals[0]),
+ detail::decode_norm(VP::comps[0].bits, v_vals[0]),
+ uint16_t(0),
+ };
+ }
+};
+
+template <typename L>
+struct PlanarSink {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 3);
+
+ static constexpr size_t y_plane = L::template find_plane<C::Y>();
+ static constexpr size_t u_plane = L::template find_plane<C::U>();
+ static constexpr size_t v_plane = L::template find_plane<C::V>();
+
+ using YP = typename L::template plane<y_plane>;
+ using UP = typename L::template plane<u_plane>;
+ using VP = typename L::template plane<v_plane>;
+
+ static constexpr size_t block_h = L::v_sub;
+ static constexpr size_t block_w = L::h_sub;
+
+ static void write_block(Buffer<3>& buf, size_t bx, size_t by,
+ const YUV16 (&block)[block_h][block_w]) noexcept
+ {
+ // Y per pixel.
+ for (size_t dy = 0; dy < block_h; ++dy) {
+ uint8_t* y_row = buf.data[y_plane]
+ + (by + dy) * buf.stride[y_plane];
+ for (size_t dx = 0; dx < block_w; ++dx) {
+ std::array<uint16_t, YP::num_comps> v{};
+ v[0] = detail::encode_norm(YP::comps[0].bits, block[dy][dx].y);
+ detail::store_word<YP>(
+ y_row + (bx + dx) * YP::bytes_per_pixel,
+ YP::pack(v));
+ }
+ }
+
+ // One averaged U and V sample per block. Integer truncation
+ // (no round-half-up).
+ uint32_t u_sum = 0, v_sum = 0;
+ for (size_t dy = 0; dy < block_h; ++dy) {
+ for (size_t dx = 0; dx < block_w; ++dx) {
+ u_sum += block[dy][dx].u;
+ v_sum += block[dy][dx].v;
+ }
+ }
+ constexpr uint32_t n = block_h * block_w;
+
+ const size_t cx = bx / L::h_sub;
+ const size_t cy = by / L::v_sub;
+
+ std::array<uint16_t, UP::num_comps> uw{};
+ uw[0] = detail::encode_norm(UP::comps[0].bits, uint16_t(u_sum / n));
+ detail::store_word<UP>(
+ buf.data[u_plane] + cy * buf.stride[u_plane]
+ + cx * UP::bytes_per_pixel,
+ UP::pack(uw));
+
+ std::array<uint16_t, VP::num_comps> vw{};
+ vw[0] = detail::encode_norm(VP::comps[0].bits, uint16_t(v_sum / n));
+ detail::store_word<VP>(
+ buf.data[v_plane] + cy * buf.stride[v_plane]
+ + cx * VP::bytes_per_pixel,
+ VP::pack(vw));
+ }
+};
+
+// T430-style 3-plane multi-pixel-per-word planar 4:4:4. Each plane has
+// `ppw` samples of the same component (Y in plane 0, U in 1, V in 2 —
+// or whichever ordering find_plane resolves) packed into a single
+// storage word. block_w = ppw, block_h = 1. No chroma subsampling.
+template <typename L>
+struct MultiPixelPlanarSource {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 3);
+ static_assert(L::h_sub == 1 && L::v_sub == 1);
+
+ static constexpr size_t y_plane = L::template find_plane<C::Y>();
+ static constexpr size_t u_plane = L::template find_plane<C::U>();
+ static constexpr size_t v_plane = L::template find_plane<C::V>();
+
+ using YP = typename L::template plane<y_plane>;
+ using UP = typename L::template plane<u_plane>;
+ using VP = typename L::template plane<v_plane>;
+
+ static constexpr size_t ppw = YP::template component_count<C::Y>();
+ static_assert(ppw == UP::template component_count<C::U>());
+ static_assert(ppw == VP::template component_count<C::V>());
+
+ // All same-tag positions share the same bit width.
+ static constexpr unsigned y_bits = YP::comps[YP::template find_pos<C::Y>(0)].bits;
+ static constexpr unsigned u_bits = UP::comps[UP::template find_pos<C::U>(0)].bits;
+ static constexpr unsigned v_bits = VP::comps[VP::template find_pos<C::V>(0)].bits;
+
+ static YUV16 read(const Buffer<3>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ const size_t gx = x / ppw;
+ const size_t off = x % ppw;
+
+ const uint8_t* yp = buf.data[y_plane] + y * buf.stride[y_plane]
+ + gx * YP::bytes_per_pixel;
+ const uint8_t* up = buf.data[u_plane] + y * buf.stride[u_plane]
+ + gx * UP::bytes_per_pixel;
+ const uint8_t* vp = buf.data[v_plane] + y * buf.stride[v_plane]
+ + gx * VP::bytes_per_pixel;
+
+ const auto y_vals = YP::unpack(detail::load_word<YP>(yp));
+ const auto u_vals = UP::unpack(detail::load_word<UP>(up));
+ const auto v_vals = VP::unpack(detail::load_word<VP>(vp));
+
+ return YUV16{
+ detail::decode_norm(y_bits, y_vals[YP::template find_pos<C::Y>(off)]),
+ detail::decode_norm(u_bits, u_vals[UP::template find_pos<C::U>(off)]),
+ detail::decode_norm(v_bits, v_vals[VP::template find_pos<C::V>(off)]),
+ uint16_t(0),
+ };
+ }
+};
+
+template <typename L>
+struct MultiPixelPlanarSink {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 3);
+ static_assert(L::h_sub == 1 && L::v_sub == 1);
+
+ static constexpr size_t y_plane = L::template find_plane<C::Y>();
+ static constexpr size_t u_plane = L::template find_plane<C::U>();
+ static constexpr size_t v_plane = L::template find_plane<C::V>();
+
+ using YP = typename L::template plane<y_plane>;
+ using UP = typename L::template plane<u_plane>;
+ using VP = typename L::template plane<v_plane>;
+
+ static constexpr size_t ppw = YP::template component_count<C::Y>();
+
+ static constexpr size_t y_x_idx = YP::template find_pos<C::X>();
+ static constexpr size_t u_x_idx = UP::template find_pos<C::X>();
+ static constexpr size_t v_x_idx = VP::template find_pos<C::X>();
+ static constexpr bool y_has_x = (y_x_idx < YP::num_comps);
+ static constexpr bool u_has_x = (u_x_idx < UP::num_comps);
+ static constexpr bool v_has_x = (v_x_idx < VP::num_comps);
+
+ static constexpr size_t block_h = 1;
+ static constexpr size_t block_w = ppw;
+
+ static void write_block(Buffer<3>& buf, size_t bx, size_t by,
+ const YUV16 (&block)[1][ppw]) noexcept
+ {
+ std::array<uint16_t, YP::num_comps> yv{};
+ std::array<uint16_t, UP::num_comps> uv{};
+ std::array<uint16_t, VP::num_comps> vv{};
+
+ // All same-tag positions share the same bit width.
+ constexpr unsigned y_bits = YP::comps[YP::template find_pos<C::Y>(0)].bits;
+ constexpr unsigned u_bits = UP::comps[UP::template find_pos<C::U>(0)].bits;
+ constexpr unsigned v_bits = VP::comps[VP::template find_pos<C::V>(0)].bits;
+ for (size_t i = 0; i < ppw; ++i) {
+ yv[YP::template find_pos<C::Y>(i)] =
+ detail::encode_norm(y_bits, block[0][i].y);
+ uv[UP::template find_pos<C::U>(i)] =
+ detail::encode_norm(u_bits, block[0][i].u);
+ vv[VP::template find_pos<C::V>(i)] =
+ detail::encode_norm(v_bits, block[0][i].v);
+ }
+
+ if constexpr (y_has_x) yv[y_x_idx] = 0;
+ if constexpr (u_has_x) uv[u_x_idx] = 0;
+ if constexpr (v_has_x) vv[v_x_idx] = 0;
+
+ const size_t gx = bx / ppw;
+ detail::store_word<YP>(
+ buf.data[y_plane] + by * buf.stride[y_plane]
+ + gx * YP::bytes_per_pixel,
+ YP::pack(yv));
+ detail::store_word<UP>(
+ buf.data[u_plane] + by * buf.stride[u_plane]
+ + gx * UP::bytes_per_pixel,
+ UP::pack(uv));
+ detail::store_word<VP>(
+ buf.data[v_plane] + by * buf.stride[v_plane]
+ + gx * VP::bytes_per_pixel,
+ VP::pack(vv));
+ }
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/io/semiplanar.h b/subprojects/pixpat/pixpat-native/src/io/semiplanar.h
new file mode 100644
index 0000000..00e7731
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/io/semiplanar.h
@@ -0,0 +1,242 @@
+#pragma once
+
+// 2-plane semiplanar YUV. Two flavours:
+//
+// SemiplanarSource / SemiplanarSink — NV12/NV21/NV16/NV61, single
+// pixel per Y storage word, single chroma pair per chroma word.
+//
+// MultiPixelSemiplanarSource / MultiPixelSemiplanarSink — P030/P230,
+// multiple Y pixels per Y word and multiple chroma pairs per
+// chroma word. The Y plane has `ppw_y = component_count<Y>()` Y
+// samples per storage word; the chroma plane has `pairs =
+// component_count<U>()` U/V pairs per storage word. block_w =
+// pairs × h_sub, block_h = v_sub — each block exactly fills one
+// chroma word.
+
+#include <array>
+
+#include "../layout.h"
+#include "detail.h"
+
+namespace pixpat
+{
+
+template <typename L>
+struct SemiplanarSource {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 2);
+
+ using YP = typename L::template plane<0>;
+ using CP = typename L::template plane<1>;
+ static constexpr size_t y_idx = YP::template find_pos<C::Y>();
+ static constexpr size_t u_idx = CP::template find_pos<C::U>();
+ static constexpr size_t v_idx = CP::template find_pos<C::V>();
+
+ static YUV16 read(const Buffer<2>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ const uint8_t* yp = buf.data[0] + y * buf.stride[0] + x * YP::bytes_per_pixel;
+ const auto y_vals = YP::unpack(detail::load_word<YP>(yp));
+
+ const size_t cx = x / L::h_sub;
+ const size_t cy = y / L::v_sub;
+ const uint8_t* cp = buf.data[1] + cy * buf.stride[1] + cx * CP::bytes_per_pixel;
+ const auto c_vals = CP::unpack(detail::load_word<CP>(cp));
+
+ return YUV16{
+ detail::decode_norm(YP::comps[y_idx].bits, y_vals[y_idx]),
+ detail::decode_norm(CP::comps[u_idx].bits, c_vals[u_idx]),
+ detail::decode_norm(CP::comps[v_idx].bits, c_vals[v_idx]),
+ uint16_t(0),
+ };
+ }
+};
+
+template <typename L>
+struct SemiplanarSink {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 2);
+
+ using YP = typename L::template plane<0>;
+ using CP = typename L::template plane<1>;
+ static constexpr size_t y_idx = YP::template find_pos<C::Y>();
+ static constexpr size_t u_idx = CP::template find_pos<C::U>();
+ static constexpr size_t v_idx = CP::template find_pos<C::V>();
+
+ static constexpr size_t block_h = L::v_sub;
+ static constexpr size_t block_w = L::h_sub;
+
+ static void write_block(Buffer<2>& buf, size_t bx, size_t by,
+ const YUV16 (&block)[block_h][block_w]) noexcept
+ {
+ // Y per pixel.
+ for (size_t dy = 0; dy < block_h; ++dy) {
+ uint8_t* y_row = buf.data[0] + (by + dy) * buf.stride[0];
+ for (size_t dx = 0; dx < block_w; ++dx) {
+ std::array<uint16_t, YP::num_comps> v{};
+ v[y_idx] = detail::encode_norm(YP::comps[y_idx].bits,
+ block[dy][dx].y);
+ detail::store_word<YP>(
+ y_row + (bx + dx) * YP::bytes_per_pixel,
+ YP::pack(v));
+ }
+ }
+
+ // One averaged UV pair for the whole block. Integer truncation
+ // (no round-half-up).
+ uint32_t u_sum = 0, v_sum = 0;
+ for (size_t dy = 0; dy < block_h; ++dy) {
+ for (size_t dx = 0; dx < block_w; ++dx) {
+ u_sum += block[dy][dx].u;
+ v_sum += block[dy][dx].v;
+ }
+ }
+ constexpr uint32_t n = block_h * block_w;
+ const uint16_t u_avg = uint16_t(u_sum / n);
+ const uint16_t v_avg = uint16_t(v_sum / n);
+
+ std::array<uint16_t, CP::num_comps> uv{};
+ uv[u_idx] = detail::encode_norm(CP::comps[u_idx].bits, u_avg);
+ uv[v_idx] = detail::encode_norm(CP::comps[v_idx].bits, v_avg);
+
+ const size_t cx = bx / L::h_sub;
+ const size_t cy = by / L::v_sub;
+ uint8_t* cp = buf.data[1] + cy * buf.stride[1] + cx * CP::bytes_per_pixel;
+ detail::store_word<CP>(cp, CP::pack(uv));
+ }
+};
+
+// Multi-pixel-per-word semiplanar (P030: 4:2:0, P230: 4:2:2). All Y
+// components share the same bit width; same for U and V.
+template <typename L>
+struct MultiPixelSemiplanarSource {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 2);
+
+ using YP = typename L::template plane<0>;
+ using CP = typename L::template plane<1>;
+ static constexpr size_t ppw_y = YP::template component_count<C::Y>();
+ static constexpr size_t pairs = CP::template component_count<C::U>();
+ static_assert(ppw_y >= 1 && pairs >= 1);
+ static_assert(pairs == CP::template component_count<C::V>());
+
+ // All same-tag positions share the same bit width.
+ static constexpr unsigned y_bits = YP::comps[YP::template find_pos<C::Y>(0)].bits;
+ static constexpr unsigned u_bits = CP::comps[CP::template find_pos<C::U>(0)].bits;
+ static constexpr unsigned v_bits = CP::comps[CP::template find_pos<C::V>(0)].bits;
+
+ static YUV16 read(const Buffer<2>& buf, size_t x, size_t y,
+ [[maybe_unused]] size_t W,
+ [[maybe_unused]] size_t H) noexcept
+ {
+ // Y read.
+ const size_t y_gx = x / ppw_y;
+ const size_t y_off = x % ppw_y;
+ const uint8_t* yp = buf.data[0] + y * buf.stride[0]
+ + y_gx * YP::bytes_per_pixel;
+ const auto y_vals = YP::unpack(detail::load_word<YP>(yp));
+
+ // Chroma read.
+ const size_t cx = x / L::h_sub;
+ const size_t cy = y / L::v_sub;
+ const size_t c_gx = cx / pairs;
+ const size_t c_off = cx % pairs;
+ const uint8_t* cp = buf.data[1] + cy * buf.stride[1]
+ + c_gx * CP::bytes_per_pixel;
+ const auto c_vals = CP::unpack(detail::load_word<CP>(cp));
+
+ return YUV16{
+ detail::decode_norm(y_bits, y_vals[YP::template find_pos<C::Y>(y_off)]),
+ detail::decode_norm(u_bits, c_vals[CP::template find_pos<C::U>(c_off)]),
+ detail::decode_norm(v_bits, c_vals[CP::template find_pos<C::V>(c_off)]),
+ uint16_t(0),
+ };
+ }
+};
+
+template <typename L>
+struct MultiPixelSemiplanarSink {
+ using Layout = L;
+ using Pixel = YUV16;
+
+ static_assert(L::kind == ColorKind::YUV);
+ static_assert(L::num_planes == 2);
+
+ using YP = typename L::template plane<0>;
+ using CP = typename L::template plane<1>;
+ static constexpr size_t ppw_y = YP::template component_count<C::Y>();
+ static constexpr size_t pairs = CP::template component_count<C::U>();
+ static_assert(ppw_y >= 1 && pairs >= 1);
+
+ // One block exactly fills one chroma word: `pairs` chroma pairs,
+ // each covering h_sub luma columns × v_sub rows.
+ static constexpr size_t block_w = pairs * L::h_sub;
+ static constexpr size_t block_h = L::v_sub;
+ static_assert(block_w % ppw_y == 0,
+ "block width must be a multiple of Y-pixels-per-word");
+ static constexpr size_t y_words_per_row = block_w / ppw_y;
+
+ // All same-tag positions share the same bit width.
+ static constexpr unsigned y_bits = YP::comps[YP::template find_pos<C::Y>(0)].bits;
+ static constexpr unsigned u_bits = CP::comps[CP::template find_pos<C::U>(0)].bits;
+ static constexpr unsigned v_bits = CP::comps[CP::template find_pos<C::V>(0)].bits;
+
+ static void write_block(Buffer<2>& buf, size_t bx, size_t by,
+ const YUV16 (&block)[block_h][block_w]) noexcept
+ {
+ // Y plane: y_words_per_row Y-words per row, block_h rows.
+ for (size_t dy = 0; dy < block_h; ++dy) {
+ uint8_t* y_row = buf.data[0]
+ + (by + dy) * buf.stride[0];
+ for (size_t w = 0; w < y_words_per_row; ++w) {
+ std::array<uint16_t, YP::num_comps> v{};
+ for (size_t i = 0; i < ppw_y; ++i) {
+ const size_t pos = YP::template find_pos<C::Y>(i);
+ v[pos] = detail::encode_norm(
+ y_bits, block[dy][w * ppw_y + i].y);
+ }
+ detail::store_word<YP>(
+ y_row + (bx / ppw_y + w)
+ * YP::bytes_per_pixel,
+ YP::pack(v));
+ }
+ }
+
+ // One UV-word: `pairs` chroma pairs. Each pair averages h_sub
+ // horizontally × v_sub vertically luma values.
+ std::array<uint16_t, CP::num_comps> uv{};
+ constexpr uint32_t n = L::h_sub * L::v_sub;
+ for (size_t p = 0; p < pairs; ++p) {
+ uint32_t u_sum = 0, v_sum = 0;
+ for (size_t dy = 0; dy < block_h; ++dy) {
+ for (size_t dx = 0; dx < L::h_sub; ++dx) {
+ u_sum += block[dy][p * L::h_sub + dx].u;
+ v_sum += block[dy][p * L::h_sub + dx].v;
+ }
+ }
+ uv[CP::template find_pos<C::U>(p)] =
+ detail::encode_norm(u_bits, uint16_t(u_sum / n));
+ uv[CP::template find_pos<C::V>(p)] =
+ detail::encode_norm(v_bits, uint16_t(v_sum / n));
+ }
+
+ const size_t cy = by / L::v_sub;
+ const size_t uv_word_idx = bx / block_w;
+ detail::store_word<CP>(
+ buf.data[1] + cy * buf.stride[1]
+ + uv_word_idx * CP::bytes_per_pixel,
+ CP::pack(uv));
+ }
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/layout.h b/subprojects/pixpat/pixpat-native/src/layout.h
new file mode 100644
index 0000000..d092bb1
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/layout.h
@@ -0,0 +1,141 @@
+#pragma once
+
+#include <array>
+#include <cstddef>
+#include <cstdint>
+#include <tuple>
+
+namespace pixpat
+{
+
+enum class ColorKind { RGB, YUV };
+
+// Normalized inter-stage pixel types. 16-bit-per-component integer.
+// N-bit storage values bit-replicate up to normalized 16-bit (so
+// N-bit max maps to 0xFFFF); encoding back is a truncating
+// `normalized >> (16 - N)`. See io/detail.h for the round-trip
+// argument. Sources without an A component emit a=0; cross-color-kind
+// ColorXfm resets a=0xFFFF; sinks with X write 0, sinks with A
+// encode `a`.
+struct RGB16 {
+ static constexpr ColorKind kind = ColorKind::RGB;
+ uint16_t r, g, b, a;
+};
+
+struct YUV16 {
+ static constexpr ColorKind kind = ColorKind::YUV;
+ uint16_t y, u, v, a;
+};
+
+inline constexpr uint16_t kNormMax = 0xFFFF;
+
+enum class C : uint8_t { X, A, R, G, B, Y, U, V };
+
+struct Comp {
+ C c;
+ uint8_t bits;
+ uint8_t shift;
+};
+
+template <typename Storage, Comp... Cs>
+struct Plane {
+ using storage_t = Storage;
+
+ static constexpr size_t num_comps = sizeof...(Cs);
+ static constexpr std::array<Comp, num_comps> comps{ Cs ... };
+ static constexpr size_t total_bits = (size_t(Cs.bits) + ... + 0);
+ static constexpr size_t storage_bits = sizeof(Storage) * 8;
+ static constexpr size_t bytes_per_pixel = (total_bits + 7) / 8;
+
+ static_assert(total_bits <= storage_bits, "components overflow storage word");
+
+ // Index of the n-th component matching Tag, or num_comps if absent.
+ template <C Tag>
+ static constexpr size_t find_pos(size_t n = 0)
+ {
+ for (size_t i = 0; i < num_comps; ++i) {
+ if (comps[i].c == Tag) {
+ if (n == 0)
+ return i;
+ --n;
+ }
+ }
+ return num_comps;
+ }
+
+ // Count of components matching Tag. Used to derive
+ // pixels_per_word for multi-pixel-per-storage formats (XYYY2101010,
+ // P030, ...).
+ template <C Tag>
+ static constexpr size_t component_count()
+ {
+ size_t cnt = 0;
+ for (size_t i = 0; i < num_comps; ++i)
+ if (comps[i].c == Tag)
+ ++cnt;
+ return cnt;
+ }
+
+ // Mask each input value to its bit-width and OR-shift it into the
+ // storage word. The loop trip count and the comps[i] reads are
+ // compile-time constant, so the optimizer unrolls and folds.
+ static constexpr Storage pack(const std::array<uint16_t, num_comps>& v) noexcept
+ {
+ Storage out{};
+ for (size_t i = 0; i < num_comps; ++i) {
+ const Storage mask = (Storage{ 1 } << comps[i].bits) - 1;
+ out |= Storage(v[i] & mask) << comps[i].shift;
+ }
+ return out;
+ }
+
+ // Mirror of `pack`.
+ static constexpr std::array<uint16_t, num_comps> unpack(Storage word) noexcept
+ {
+ std::array<uint16_t, num_comps> out{};
+ for (size_t i = 0; i < num_comps; ++i) {
+ const Storage mask = (Storage{ 1 } << comps[i].bits) - 1;
+ out[i] = uint16_t((word >> comps[i].shift) & mask);
+ }
+ return out;
+ }
+};
+
+template <ColorKind Kind, size_t Hsub, size_t Vsub, typename ... Planes>
+struct Layout {
+ static constexpr ColorKind kind = Kind;
+ static constexpr size_t h_sub = Hsub;
+ static constexpr size_t v_sub = Vsub;
+ static constexpr size_t num_planes = sizeof...(Planes);
+
+ template <size_t N>
+ using plane = std::tuple_element_t<N, std::tuple<Planes...> >;
+
+ // Index of the first plane containing component Tag, or num_planes
+ // if no plane has it. Lets PlanarSource/Sink map C::U / C::V to a
+ // plane regardless of YUV vs YVU ordering.
+ // Comma-fold over plane indices: for each plane I check if it has
+ // Tag, and on the first hit assign `found = I`. Subsequent hits are
+ // suppressed by the `found == num_planes` guard. The whole fold
+ // evaluates to a discarded list of int 0s; the `found` capture
+ // carries the result out.
+ template <C Tag>
+ static constexpr size_t find_plane()
+ {
+ return [&]<size_t... I>(std::index_sequence<I...>) {
+ size_t found = num_planes;
+ ((plane<I>::template find_pos<Tag>() < plane<I>::num_comps
+ ? (found == num_planes ? (found = I, 0) : 0)
+ : 0), ...);
+ return found;
+ } (std::make_index_sequence<num_planes>{});
+ }
+};
+
+template <size_t N>
+struct Buffer {
+ std::array<uint8_t*, N> data;
+ std::array<size_t, N> stride;
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/params.h b/subprojects/pixpat/pixpat-native/src/params.h
new file mode 100644
index 0000000..aa2be67
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/params.h
@@ -0,0 +1,219 @@
+#pragma once
+
+#include <cctype>
+#include <cstddef>
+#include <cstdint>
+#include <optional>
+#include <string>
+#include <string_view>
+#include <utility>
+#include <vector>
+
+#include "layout.h"
+
+namespace pixpat
+{
+
+// Pattern-specific parameters parsed from pixpat_pattern_opts::params.
+// The wire format is a comma-separated list of "key=value" items;
+// whitespace around tokens is trimmed, keys and values are
+// case-insensitive ASCII, and neither may contain ',' or '='.
+// Malformed input leaves ok() returning false — the pattern dispatcher
+// fails the call when that happens.
+//
+// Patterns query keys by name via get() / get_int() / get_hex_color().
+// Unknown keys are ignored: each pattern handles forward compatibility,
+// not the parser.
+class Params
+{
+public:
+ explicit Params(const char* csv);
+
+ bool ok() const noexcept {
+ return ok_;
+ }
+
+ std::optional<std::string_view> get(std::string_view key) const noexcept;
+ std::optional<int> get_int(std::string_view key) const noexcept;
+ std::optional<RGB16> get_hex_color(std::string_view key) const noexcept;
+
+private:
+ std::vector<std::pair<std::string, std::string> > kv_;
+ bool ok_{ true };
+};
+
+namespace detail
+{
+
+inline char ascii_tolower(char c) noexcept
+{
+ return (c >= 'A' && c <= 'Z') ? char(c + ('a' - 'A')) : c;
+}
+
+inline std::string_view trim(std::string_view s) noexcept
+{
+ while (!s.empty() && std::isspace(static_cast<unsigned char>(s.front())))
+ s.remove_prefix(1);
+ while (!s.empty() && std::isspace(static_cast<unsigned char>(s.back())))
+ s.remove_suffix(1);
+ return s;
+}
+
+inline bool ieq(std::string_view a, std::string_view b) noexcept
+{
+ if (a.size() != b.size())
+ return false;
+ for (size_t i = 0; i < a.size(); ++i)
+ if (ascii_tolower(a[i]) != ascii_tolower(b[i]))
+ return false;
+ return true;
+}
+
+} // namespace detail
+
+
+inline Params::Params(const char* csv)
+{
+ if (!csv || *csv == '\0')
+ return;
+
+ std::string_view s(csv);
+ while (!s.empty()) {
+ const size_t comma = s.find(',');
+ std::string_view item = (comma == std::string_view::npos)
+ ? s : s.substr(0, comma);
+ s = (comma == std::string_view::npos)
+ ? std::string_view{} : s.substr(comma + 1);
+
+ item = detail::trim(item);
+ if (item.empty()) {
+ ok_ = false;
+ return;
+ }
+
+ const size_t eq = item.find('=');
+ if (eq == std::string_view::npos) {
+ ok_ = false;
+ return;
+ }
+ const std::string_view k = detail::trim(item.substr(0, eq));
+ const std::string_view v = detail::trim(item.substr(eq + 1));
+ if (k.empty()) {
+ ok_ = false;
+ return;
+ }
+ kv_.emplace_back(std::string(k), std::string(v));
+ }
+}
+
+inline std::optional<std::string_view>
+Params::get(std::string_view key) const noexcept
+{
+ for (const auto& [k, v] : kv_)
+ if (detail::ieq(k, key))
+ return std::string_view(v);
+ return std::nullopt;
+}
+
+inline std::optional<int>
+Params::get_int(std::string_view key) const noexcept
+{
+ auto v = get(key);
+ if (!v || v->empty())
+ return std::nullopt;
+ int sign = 1;
+ size_t i = 0;
+ if ((*v)[0] == '-') { sign = -1; ++i; }
+ else if ((*v)[0] == '+') { ++i; }
+ if (i == v->size())
+ return std::nullopt;
+ int out = 0;
+ for (; i < v->size(); ++i) {
+ const char c = (*v)[i];
+ if (c < '0' || c > '9')
+ return std::nullopt;
+ out = out * 10 + (c - '0');
+ }
+ return sign * out;
+}
+
+// Parses a hex color string. The optional `0x`/`0X` prefix is allowed.
+// The number of hex digits after the prefix selects the layout:
+// 6 digits — 8-bit RRGGBB (alpha defaults to opaque)
+// 8 digits — 8-bit AARRGGBB (alpha-first)
+// 12 digits — 16-bit RRRRGGGGBBBB (alpha defaults to opaque)
+// 16 digits — 16-bit AAAARRRRGGGGBBBB (alpha-first)
+// 8-bit components are byte-replicated to the normalized 16-bit form
+// (0xFF → 0xFFFF); 16-bit components are stored directly. Any other
+// length, malformed digits, or stray separators yield std::nullopt.
+inline std::optional<RGB16>
+Params::get_hex_color(std::string_view key) const noexcept
+{
+ auto v = get(key);
+ if (!v)
+ return std::nullopt;
+
+ std::string_view s = *v;
+ if (s.size() >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
+ s.remove_prefix(2);
+
+ const auto digit = [](char c) -> int {
+ if (c >= '0' && c <= '9') return c - '0';
+ if (c >= 'a' && c <= 'f') return c - 'a' + 10;
+ if (c >= 'A' && c <= 'F') return c - 'A' + 10;
+ return -1;
+ };
+ const auto parse_n = [&](size_t off, size_t n) -> std::optional<unsigned> {
+ unsigned out = 0;
+ for (size_t i = 0; i < n; ++i) {
+ const int d = digit(s[off + i]);
+ if (d < 0)
+ return std::nullopt;
+ out = (out << 4) | unsigned(d);
+ }
+ return out;
+ };
+
+ bool has_alpha;
+ bool is_16bpc;
+ switch (s.size()) {
+ case 6: has_alpha = false; is_16bpc = false; break;
+ case 8: has_alpha = true; is_16bpc = false; break;
+ case 12: has_alpha = false; is_16bpc = true; break;
+ case 16: has_alpha = true; is_16bpc = true; break;
+ default: return std::nullopt;
+ }
+
+ const size_t per = is_16bpc ? 4 : 2;
+ const unsigned full = is_16bpc ? 0xFFFFu : 0xFFu;
+ unsigned a = full, r, g, b;
+ size_t off = 0;
+ if (has_alpha) {
+ auto av = parse_n(off, per);
+ if (!av) return std::nullopt;
+ a = *av;
+ off += per;
+ }
+ auto rv = parse_n(off, per);
+ if (!rv) return std::nullopt;
+ r = *rv;
+ off += per;
+ auto gv = parse_n(off, per);
+ if (!gv) return std::nullopt;
+ g = *gv;
+ off += per;
+ auto bv = parse_n(off, per);
+ if (!bv) return std::nullopt;
+ b = *bv;
+
+ if (is_16bpc) {
+ return RGB16{ uint16_t(r), uint16_t(g), uint16_t(b), uint16_t(a) };
+ } else {
+ const auto rep = [](unsigned x) noexcept {
+ return uint16_t((x << 8) | x);
+ };
+ return RGB16{ rep(r), rep(g), rep(b), rep(a) };
+ }
+}
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/pattern.h b/subprojects/pixpat/pixpat-native/src/pattern.h
new file mode 100644
index 0000000..fbee683
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/pattern.h
@@ -0,0 +1,597 @@
+#pragma once
+
+#include <cmath>
+#include <cstdint>
+
+#include "color.h"
+#include "layout.h"
+#include "params.h"
+
+namespace pixpat::patterns
+{
+
+// Patterns emit opaque pixels (a=kNormMax) unless they encode their
+// own alpha (e.g. `plain`'s ARGB form). Alpha-bearing sinks
+// (ARGB8888 etc) therefore see the pattern's chosen alpha; convert
+// paths propagate the source's actual `a` instead (a=0 for X-only
+// sources).
+//
+// A pattern is an instance with:
+// using Pixel = RGB16 | YUV16;
+// explicit Pat(const Params&) noexcept;
+// Pixel sample(size_t x, size_t y, size_t W, size_t H) const noexcept;
+// bool ready() const noexcept; // optional, default true
+// Patterns that don't read params ignore the constructor argument.
+
+namespace detail
+{
+// 8-bit -> normalized 16 byte-replication. e.g. 255 -> 0xFFFF,
+// 1 -> 0x0101.
+constexpr RGB16 rgb8(uint8_t r, uint8_t g, uint8_t b) noexcept
+{
+ return RGB16{
+ uint16_t((uint16_t(r) << 8) | r),
+ uint16_t((uint16_t(g) << 8) | g),
+ uint16_t((uint16_t(b) << 8) | b),
+ kNormMax,
+ };
+}
+
+// 12-bit -> normalized 16 bit-replication.
+constexpr YUV16 yuv12(uint16_t y, uint16_t u, uint16_t v) noexcept
+{
+ return YUV16{
+ uint16_t((y << 4) | (y >> 8)),
+ uint16_t((u << 4) | (u >> 8)),
+ uint16_t((v << 4) | (v >> 8)),
+ kNormMax,
+ };
+}
+} // namespace detail
+
+// "kmstest" default pattern: white border + diagonals; blue rails on
+// the top/left edges; red rails on the bottom/right; an 8-step color
+// gradient block in the center.
+struct Kmstest {
+ using Pixel = RGB16;
+
+ explicit Kmstest(const Params&) noexcept {
+ }
+
+ RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept
+ {
+ using detail::rgb8;
+ const size_t mw = 20;
+ const size_t xm1 = mw;
+ const size_t xm2 = W - mw - 1;
+ const size_t ym1 = mw;
+ const size_t ym2 = H - mw - 1;
+
+ if (x == xm1 || x == xm2 || y == ym1 || y == ym2)
+ return rgb8(255, 255, 255);
+ if (x < xm1 && y < ym1)
+ return rgb8(255, 255, 255);
+ if ((x == 0 || x == W - 1) && (y < ym1 || y > ym2))
+ return rgb8(255, 255, 255);
+ if ((y == 0 || y == H - 1) && (x < xm1 || x > xm2))
+ return rgb8(255, 255, 255);
+ if (x < xm1 && (y > ym1 && y < ym2))
+ return rgb8(0, 0, 255);
+ if (y < ym1 && (x > xm1 && x < xm2))
+ return rgb8(0, 0, 255);
+ if (x > xm2 && (y > ym1 && y < ym2))
+ return rgb8(255, 0, 0);
+ if (y > ym2 && (x > xm1 && x < xm2))
+ return rgb8(255, 0, 0);
+ if (x > xm1 && x < xm2 && y > ym1 && y < ym2) {
+ if (x == y || W - x == H - y)
+ return rgb8(255, 255, 255);
+ if (W - x - 1 == y || x == H - y - 1)
+ return rgb8(255, 255, 255);
+ const int t = int((x - xm1 - 1) * 8 / (xm2 - xm1 - 1));
+ const unsigned c = unsigned((y - ym1 - 1) % 256);
+ unsigned r = 0, g = 0, b = 0;
+ switch (t) {
+ case 0: r = c; break;
+ case 1: g = c; break;
+ case 2: b = c; break;
+ case 3: g = b = c; break;
+ case 4: r = b = c; break;
+ case 5: r = g = c; break;
+ case 6: r = g = b = c; break;
+ case 7: break;
+ }
+ return rgb8(uint8_t(r), uint8_t(g), uint8_t(b));
+ }
+ return rgb8(0, 0, 0);
+ }
+};
+
+// SMPTE RP 219-1:2014 color bar pattern. Emits YUV directly with
+// pixel values defined by the spec in BT.709 / Limited range. Pass
+// `rec=BT709, range=Limited` for spec-correct output; other ColorSpec
+// settings produce visibly-wrong colors when the sink crosses to RGB
+// (the matrix the caller picked is applied to BT.709-encoded values).
+// Callers are trusted — pixpat does not override the spec for them.
+struct Smpte {
+ using Pixel = YUV16;
+
+ explicit Smpte(const Params&) noexcept {
+ }
+
+ YUV16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept
+ {
+ using detail::yuv12;
+ constexpr YUV16 gray40 = yuv12(1658, 2048, 2048);
+ constexpr YUV16 white75 = yuv12(2884, 2048, 2048);
+ constexpr YUV16 yellow75 = yuv12(2694, 704, 2171);
+ constexpr YUV16 cyan75 = yuv12(2325, 2356, 704);
+ constexpr YUV16 green75 = yuv12(2136, 1012, 827);
+ constexpr YUV16 magenta75 = yuv12(1004, 3084, 3269);
+ constexpr YUV16 red75 = yuv12( 815, 1740, 3392);
+ constexpr YUV16 blue75 = yuv12( 446, 3392, 1925);
+ constexpr YUV16 cyan100 = yuv12(3015, 2459, 256);
+ constexpr YUV16 blue100 = yuv12( 509, 3840, 1884);
+ constexpr YUV16 yellow100 = yuv12(3507, 256, 2212);
+ constexpr YUV16 black = yuv12( 256, 2048, 2048);
+ constexpr YUV16 white100 = yuv12(3760, 2048, 2048);
+ constexpr YUV16 red100 = yuv12(1001, 1637, 3840);
+ constexpr YUV16 gray15 = yuv12( 782, 2048, 2048);
+
+ constexpr YUV16 black_m2 = yuv12( 186, 2048, 2048);
+ constexpr YUV16 black_p2 = yuv12( 326, 2048, 2048);
+ constexpr YUV16 black_p4 = yuv12( 396, 2048, 2048);
+
+ constexpr size_t M = 1024;
+ const size_t xs = x * M;
+ const size_t a = W * M;
+ const size_t c = (a * 3 / 4) / 7;
+ const size_t d = a / 8;
+
+ const size_t pattern1_height = (H * 7) / 12;
+ const size_t pattern2_height = pattern1_height + (H / 12);
+ const size_t pattern3_height = pattern2_height + (H / 12);
+
+ if (y < pattern1_height) {
+ if (xs < d || xs >= (a - d))
+ return gray40;
+ const size_t bar = (xs - d) / c;
+ switch (bar) {
+ case 0: return white75;
+ case 1: return yellow75;
+ case 2: return cyan75;
+ case 3: return green75;
+ case 4: return magenta75;
+ case 5: return red75;
+ default: return blue75;
+ }
+ }
+
+ if (y < pattern2_height) {
+ if (xs < d) return cyan100;
+ if (xs >= (a - d)) return blue100;
+ return white75;
+ }
+
+ if (y < pattern3_height) {
+ if (xs < d) return yellow100;
+ if (xs >= (a - d)) return red100;
+ const size_t ramp_w = a - 2 * d;
+ const size_t ramp_x = xs - d;
+ const uint16_t y_val = uint16_t(256 + (3760 - 256) * ramp_x / ramp_w);
+ return yuv12(y_val, 2048, 2048);
+ }
+
+ // pattern4 (PLUGE)
+ const size_t c0 = d;
+ const size_t c1 = c0 + c * 3 / 2;
+ const size_t c2 = c1 + 2 * c;
+ const size_t c3 = c2 + c * 5 / 6;
+
+ if (xs < c0) return gray15;
+ if (xs < c1) return black;
+ if (xs < c2) return white100;
+ if (xs < c3) return black;
+ if (xs >= a - d) return gray15;
+ if (xs >= a - d - c) return black;
+
+ const size_t step = (xs - c3) / (c / 3);
+ switch (step) {
+ case 0: return black_m2;
+ case 1: return black;
+ case 2: return black_p2;
+ case 3: return black;
+ default: return black_p4;
+ }
+ }
+};
+
+// Solid fill from a hex color string. Reads `color=<hex>` from
+// params; the value is parsed by Params::get_hex_color (8/16-bpc,
+// alpha-first if present, optional `0x` prefix). Missing or
+// malformed `color` leaves ready()=false and the dispatcher fails
+// the call.
+struct Plain {
+ using Pixel = RGB16;
+
+ explicit Plain(const Params& p) noexcept
+ {
+ if (auto c = p.get_hex_color("color")) {
+ color_ = *c;
+ ready_ = true;
+ }
+ }
+
+ bool ready() const noexcept {
+ return ready_;
+ }
+
+ RGB16 sample(size_t, size_t, size_t, size_t) const noexcept
+ {
+ return color_;
+ }
+
+private:
+ RGB16 color_{};
+ bool ready_{ false };
+};
+
+namespace detail
+{
+// Linear ramp 0..kNormMax across [0, span-1]. span<=1 returns kNormMax.
+constexpr uint16_t ramp16(size_t pos, size_t span) noexcept
+{
+ if (span <= 1)
+ return kNormMax;
+ return uint16_t((uint64_t(pos) * kNormMax) / (span - 1));
+}
+} // namespace detail
+
+// Black/white checkerboard. Reads optional `cell=<N>` (positive
+// integer; default 8) for cell size in pixels.
+struct Checker {
+ using Pixel = RGB16;
+
+ explicit Checker(const Params& p) noexcept
+ {
+ if (p.get("cell")) {
+ auto n = p.get_int("cell");
+ if (!n || *n <= 0) {
+ ready_ = false;
+ return;
+ }
+ cell_ = size_t(*n);
+ }
+ }
+
+ bool ready() const noexcept {
+ return ready_;
+ }
+
+ RGB16 sample(size_t x, size_t y, size_t, size_t) const noexcept
+ {
+ const bool dark = (((x / cell_) ^ (y / cell_)) & 1u) != 0;
+ return dark ? RGB16{ 0, 0, 0, kNormMax }
+ : RGB16{ kNormMax, kNormMax, kNormMax, kNormMax };
+ }
+
+private:
+ size_t cell_{ 8 };
+ bool ready_{ true };
+};
+
+namespace detail
+{
+// Pick one of (R, G, B, gray) given a stripe index in [0, 4) and a
+// scalar ramp value. Used by hramp/vramp.
+constexpr RGB16 rgb_gray_stripe(size_t stripe, uint16_t v) noexcept
+{
+ switch (stripe) {
+ case 0: return RGB16{ v, 0, 0, kNormMax };
+ case 1: return RGB16{ 0, v, 0, kNormMax };
+ case 2: return RGB16{ 0, 0, v, kNormMax };
+ default: return RGB16{ v, v, v, kNormMax };
+ }
+}
+} // namespace detail
+
+// Four horizontal stripes — R, G, B, gray — each a 0..max ramp
+// along x. Per-channel and luma quantization in one pattern.
+struct Hramp {
+ using Pixel = RGB16;
+
+ explicit Hramp(const Params&) noexcept {
+ }
+
+ RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept
+ {
+ const size_t stripe = (H == 0) ? 0 : (y * 4) / H;
+ return detail::rgb_gray_stripe(stripe, detail::ramp16(x, W));
+ }
+};
+
+// Four vertical columns — R, G, B, gray — each a 0..max ramp
+// along y. Same coverage as hramp, rotated 90°.
+struct Vramp {
+ using Pixel = RGB16;
+
+ explicit Vramp(const Params&) noexcept {
+ }
+
+ RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept
+ {
+ const size_t col = (W == 0) ? 0 : (x * 4) / W;
+ return detail::rgb_gray_stripe(col, detail::ramp16(y, H));
+ }
+};
+
+// Diagonal RGB ramp: R sweeps with x, G with y, B with x+y.
+struct Dramp {
+ using Pixel = RGB16;
+
+ explicit Dramp(const Params&) noexcept {
+ }
+
+ RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept
+ {
+ const uint16_t r = detail::ramp16(x, W);
+ const uint16_t g = detail::ramp16(y, H);
+ const size_t span = (W + H >= 2) ? (W + H - 1) : 1;
+ const uint16_t b = detail::ramp16(x + y, span);
+ return RGB16{ r, g, b, kNormMax };
+ }
+};
+
+namespace detail
+{
+// Seven-region color sequence used by hbar/vbar:
+// white, red, white, green, white, blue, white. The white separators
+// between R/G/B make per-channel offsets at the band boundaries
+// visible.
+constexpr RGB16 bar_color7(size_t band) noexcept
+{
+ switch (band) {
+ case 1: return rgb8(255, 0, 0);
+ case 3: return rgb8( 0, 255, 0);
+ case 5: return rgb8( 0, 0, 255);
+ default: return rgb8(255, 255, 255);
+ }
+}
+} // namespace detail
+
+// Vertical bar (full image height, narrow along x) over a black
+// background. `pos` is the left edge in pixels (signed; negative
+// values clip at the left edge); `width` is the bar thickness in
+// pixels (default 32). The bar is split into 7 equal-height regions
+// colored white/red/white/green/white/blue/white.
+struct VBarRGB {
+ using Pixel = RGB16;
+
+ explicit VBarRGB(const Params& p) noexcept
+ {
+ auto pp = p.get_int("pos");
+ if (!pp) {
+ ready_ = false;
+ return;
+ }
+ pos_ = *pp;
+ if (p.get("width")) {
+ auto w = p.get_int("width");
+ if (!w || *w <= 0) {
+ ready_ = false;
+ return;
+ }
+ width_ = size_t(*w);
+ }
+ }
+
+ bool ready() const noexcept {
+ return ready_;
+ }
+
+ RGB16 sample(size_t x, size_t y, size_t, size_t H) const noexcept
+ {
+ const long long sx = static_cast<long long>(x);
+ const long long lo = pos_;
+ const long long hi = lo + static_cast<long long>(width_);
+ if (sx < lo || sx >= hi)
+ return detail::rgb8(0, 0, 0);
+ const size_t band = (H == 0) ? 0 : (y * 7) / H;
+ return detail::bar_color7(band);
+ }
+
+private:
+ int pos_{};
+ size_t width_{ 32 };
+ bool ready_{ true };
+};
+
+// Horizontal bar: vbar rotated 90°. `pos` is the top edge in pixels;
+// `width` is the bar thickness in pixels (default 32). The bar spans
+// the full image width and is split into 7 equal-width regions
+// colored white/red/white/green/white/blue/white.
+struct HBarRGB {
+ using Pixel = RGB16;
+
+ explicit HBarRGB(const Params& p) noexcept
+ {
+ auto pp = p.get_int("pos");
+ if (!pp) {
+ ready_ = false;
+ return;
+ }
+ pos_ = *pp;
+ if (p.get("width")) {
+ auto w = p.get_int("width");
+ if (!w || *w <= 0) {
+ ready_ = false;
+ return;
+ }
+ width_ = size_t(*w);
+ }
+ }
+
+ bool ready() const noexcept {
+ return ready_;
+ }
+
+ RGB16 sample(size_t x, size_t y, size_t W, size_t) const noexcept
+ {
+ const long long sy = static_cast<long long>(y);
+ const long long lo = pos_;
+ const long long hi = lo + static_cast<long long>(width_);
+ if (sy < lo || sy >= hi)
+ return detail::rgb8(0, 0, 0);
+ const size_t band = (W == 0) ? 0 : (x * 7) / W;
+ return detail::bar_color7(band);
+ }
+
+private:
+ int pos_{};
+ size_t width_{ 32 };
+ bool ready_{ true };
+};
+
+// Same shape as VBarRGB but emits YUV16 directly. The five unique colors
+// (black bg + white/red/green/blue bar regions) are precomputed from
+// `spec` at construction so the cross-kind pass is a no-op when the
+// sink is YUV. Use the RGB-native `VBarRGB` for RGB sinks instead — it
+// avoids the YUV→RGB pass that this variant would incur there.
+struct VBarYUV {
+ using Pixel = YUV16;
+
+ explicit VBarYUV(const Params& p, ColorSpec spec) noexcept
+ {
+ auto pp = p.get_int("pos");
+ if (!pp) {
+ ready_ = false;
+ return;
+ }
+ pos_ = *pp;
+ if (p.get("width")) {
+ auto w = p.get_int("width");
+ if (!w || *w <= 0) {
+ ready_ = false;
+ return;
+ }
+ width_ = size_t(*w);
+ }
+ const ColorCoeffs c = coeffs_for(spec);
+ using X = ColorXfm<RGB16, YUV16>;
+ bg_ = X::apply(detail::rgb8( 0, 0, 0), c);
+ bands_[0] = X::apply(detail::rgb8(255, 255, 255), c);
+ bands_[1] = X::apply(detail::rgb8(255, 0, 0), c);
+ bands_[2] = bands_[0];
+ bands_[3] = X::apply(detail::rgb8( 0, 255, 0), c);
+ bands_[4] = bands_[0];
+ bands_[5] = X::apply(detail::rgb8( 0, 0, 255), c);
+ bands_[6] = bands_[0];
+ }
+
+ bool ready() const noexcept {
+ return ready_;
+ }
+
+ YUV16 sample(size_t x, size_t y, size_t, size_t H) const noexcept
+ {
+ const long long sx = static_cast<long long>(x);
+ const long long lo = pos_;
+ const long long hi = lo + static_cast<long long>(width_);
+ if (sx < lo || sx >= hi)
+ return bg_;
+ const size_t band = (H == 0) ? 0 : (y * 7) / H;
+ return bands_[band];
+ }
+
+private:
+ YUV16 bg_{};
+ YUV16 bands_[7]{};
+ int pos_{};
+ size_t width_{ 32 };
+ bool ready_{ true };
+};
+
+// YUV-native counterpart to HBarRGB. See VBarYUV.
+struct HBarYUV {
+ using Pixel = YUV16;
+
+ explicit HBarYUV(const Params& p, ColorSpec spec) noexcept
+ {
+ auto pp = p.get_int("pos");
+ if (!pp) {
+ ready_ = false;
+ return;
+ }
+ pos_ = *pp;
+ if (p.get("width")) {
+ auto w = p.get_int("width");
+ if (!w || *w <= 0) {
+ ready_ = false;
+ return;
+ }
+ width_ = size_t(*w);
+ }
+ const ColorCoeffs c = coeffs_for(spec);
+ using X = ColorXfm<RGB16, YUV16>;
+ bg_ = X::apply(detail::rgb8( 0, 0, 0), c);
+ bands_[0] = X::apply(detail::rgb8(255, 255, 255), c);
+ bands_[1] = X::apply(detail::rgb8(255, 0, 0), c);
+ bands_[2] = bands_[0];
+ bands_[3] = X::apply(detail::rgb8( 0, 255, 0), c);
+ bands_[4] = bands_[0];
+ bands_[5] = X::apply(detail::rgb8( 0, 0, 255), c);
+ bands_[6] = bands_[0];
+ }
+
+ bool ready() const noexcept {
+ return ready_;
+ }
+
+ YUV16 sample(size_t x, size_t y, size_t W, size_t) const noexcept
+ {
+ const long long sy = static_cast<long long>(y);
+ const long long lo = pos_;
+ const long long hi = lo + static_cast<long long>(width_);
+ if (sy < lo || sy >= hi)
+ return bg_;
+ const size_t band = (W == 0) ? 0 : (x * 7) / W;
+ return bands_[band];
+ }
+
+private:
+ YUV16 bg_{};
+ YUV16 bands_[7]{};
+ int pos_{};
+ size_t width_{ 32 };
+ bool ready_{ true };
+};
+
+// Centered radial cosine zone plate: 0.5 + 0.5 * cos(k * (cx² + cy²))
+// with cx, cy measured from the image center and k chosen so the
+// local frequency hits Nyquist at the longer edge — i.e. the pattern
+// uses every spatial frequency the grid can resolve.
+struct Zoneplate {
+ using Pixel = RGB16;
+
+ explicit Zoneplate(const Params&) noexcept {
+ }
+
+ RGB16 sample(size_t x, size_t y, size_t W, size_t H) const noexcept
+ {
+ const double max_dim = double(W > H ? W : H);
+ // Local frequency d(k r²)/dr = 2 k r. At r = max_dim/2 the
+ // frequency reaches π/pixel (Nyquist), giving k = π / max_dim.
+ const double k = 3.14159265358979323846 / (max_dim > 0 ? max_dim : 1.0);
+ const double cx = double(x) - 0.5 * double(W);
+ const double cy = double(y) - 0.5 * double(H);
+ const double phase = k * (cx * cx + cy * cy);
+ const double v = 0.5 + 0.5 * std::cos(phase);
+ const double scaled = v * 65535.0;
+ const uint16_t g = (scaled < 0.0) ? uint16_t(0)
+ : (scaled > 65535.0) ? kNormMax
+ : uint16_t(scaled + 0.5);
+ return RGB16{ g, g, g, kNormMax };
+ }
+};
+
+} // namespace pixpat::patterns
diff --git a/subprojects/pixpat/pixpat-native/src/pattern_catalog.h b/subprojects/pixpat/pixpat-native/src/pattern_catalog.h
new file mode 100644
index 0000000..6576b2b
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/pattern_catalog.h
@@ -0,0 +1,64 @@
+#pragma once
+
+// Catalog of every named pattern the C++ side knows. Mirrors the
+// shape of format_catalog.h. The X-macro is a list of
+// (Label, RgbType, YuvType, "name") rows:
+//
+// X(Label, RgbType, YuvType, "name")
+//
+// `Label` is the C++ identifier doubling as the PatternId enum value
+// and the s_pattern_caps[] index. `RgbType` and `YuvType` resolve to
+// classes in `pixpat::patterns::` (defined in pattern.h) that satisfy
+// the pattern interface (sample(), Pixel) — one per color kind. Use
+// `void` if the pattern has no variant in that kind. At least one
+// must be non-void. When both are present, dispatch_draw_pattern
+// picks the variant matching the sink's color kind so the cross-kind
+// pass is a no-op; when only one is present, the pipeline runs the
+// cross-kind pass for the opposite-kind sinks. `name` is the
+// lowercase identifier exposed via the C ABI.
+//
+// Adding a pattern = a row here AND its class(es) in pattern.h. The
+// codegen (pixpat-native/codegen/gen_pixpat.py) parses this X-macro
+// to learn the pattern set; pixpat_pattern.cpp re-expands it to build
+// the dispatch arms and the default-pattern fallback.
+
+#include <cstddef>
+#include <cstdint>
+
+namespace pixpat
+{
+
+#define PIXPAT_PATTERN_LIST(X) \
+ X(Kmstest, Kmstest, void, "kmstest") \
+ X(Smpte, void, Smpte, "smpte") \
+ X(Plain, Plain, void, "plain") \
+ X(Checker, Checker, void, "checker") \
+ X(Hramp, Hramp, void, "hramp") \
+ X(Vramp, Vramp, void, "vramp") \
+ X(HBar, HBarRGB, HBarYUV, "hbar") \
+ X(VBar, VBarRGB, VBarYUV, "vbar") \
+ X(Dramp, Dramp, void, "dramp") \
+ X(Zoneplate, Zoneplate, void, "zoneplate")
+
+enum class PatternId : uint8_t {
+#define X(label, rgb, yuv, name) label,
+ PIXPAT_PATTERN_LIST(X)
+#undef X
+ Unknown,
+};
+
+struct PatternEntry {
+ const char* name;
+ PatternId id;
+};
+
+inline constexpr PatternEntry s_pattern_table[] = {
+#define X(label, rgb, yuv, name) { name, PatternId::label },
+ PIXPAT_PATTERN_LIST(X)
+#undef X
+};
+
+inline constexpr size_t s_pattern_catalog_count =
+ sizeof(s_pattern_table) / sizeof(s_pattern_table[0]);
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/pipeline.h b/subprojects/pixpat/pixpat-native/src/pipeline.h
new file mode 100644
index 0000000..09e13bc
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/pipeline.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <cstddef>
+
+#include "color.h"
+#include "layout.h"
+
+// Inlined source → color → sink composition. The intermediate Pixel
+// values stay in registers across stages; there is no normalized RGB16
+// or YUV16 buffer between source and sink. Block size is dictated by
+// the sink: 1x1 for non-subsampled formats, h_sub × v_sub for chroma-
+// subsampled ones.
+
+namespace pixpat
+{
+
+template <typename Source, typename Sink>
+struct Converter {
+ using Xfm = ColorXfm<typename Source::Pixel, typename Sink::Pixel>;
+ static constexpr size_t bh = Sink::block_h;
+ static constexpr size_t bw = Sink::block_w;
+
+ static void run(const Buffer<Source::Layout::num_planes>& src,
+ Buffer<Sink::Layout::num_planes>& dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec) noexcept
+ {
+ const ColorCoeffs c = coeffs_for(spec);
+ for (size_t by = by_start; by < by_end; by += bh) {
+ for (size_t bx = 0; bx < W; bx += bw) {
+ typename Sink::Pixel block[bh][bw];
+ for (size_t dy = 0; dy < bh; ++dy)
+ for (size_t dx = 0; dx < bw; ++dx)
+ block[dy][dx] = Xfm::apply(
+ Source::read(src, bx + dx, by + dy,
+ W, H), c);
+ Sink::write_block(dst, bx, by, block);
+ }
+ }
+ }
+};
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/pixpat.cpp b/subprojects/pixpat/pixpat-native/src/pixpat.cpp
new file mode 100644
index 0000000..ac21fac
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/pixpat.cpp
@@ -0,0 +1,355 @@
+// pixpat: extern "C" entry points + runtime format dispatch.
+//
+// The format catalog (X-macro PIXPAT_FORMAT_LIST + FormatId enum +
+// s_format_table) is hand-written in format_catalog.h. The generator
+// (pixpat-native/codegen/gen_pixpat.py) reads the same X-macro and
+// the user TOML and emits the per-config bits:
+//
+// pixpat_config.h — PIXPAT_FEATURE_PATTERN / _CONVERT
+// pixpat_caps.inc — s_format_caps[] (per-format readable / writable /
+// hot_src / hot_dst, indexed by FormatId) and
+// s_pattern_caps[] (per-pattern enabled flag).
+//
+// The convert and pattern dispatch (dispatch_convert in
+// pixpat_convert.cpp, dispatch_draw_pattern in pixpat_pattern.cpp) is
+// hand-written and consumes s_format_caps / s_pattern_caps via
+// `if constexpr` on the per-row constexpr fields.
+//
+// s_format_info is built here, once, by re-expanding the catalog
+// X-macro through unpack_for / pack_for / snk_block_h_for /
+// snk_block_w_for. Those constexpr helpers use `if constexpr` on the
+// per-format readable / writable flags from s_format_caps to either
+// take the address of unpack_to_norm / pack_from_norm or fall back to
+// nullptr (or 0). Because they're function templates, the discarded
+// branch is never instantiated, so disabled-direction templates
+// produce no code.
+//
+// Feature gating is meson-side: pixpat_pattern.cpp / pixpat_convert.cpp
+// are added to the source list only when their feature is enabled. This
+// file's entry points always exist; they call the bridge functions
+// dispatch_draw_pattern / dispatch_convert under `if constexpr
+// (kFeatureXxx)`. The discarded if-constexpr branch produces no symbol
+// reference, so when the matching TU is absent the link still succeeds
+// and the entry point returns -1 instead.
+
+#include <pixpat/pixpat.h>
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+
+#include "pixpat_config.h"
+
+#include "color.h"
+#include "error.h"
+#include "format_catalog.h"
+#include "formats.h"
+#include "io.h"
+#include "layout.h"
+#include "params.h"
+#include "pattern.h"
+#include "pixpat_internal.h"
+#include "threading.h"
+
+namespace pixpat
+{
+
+inline constexpr bool kFeaturePattern = PIXPAT_FEATURE_PATTERN;
+inline constexpr bool kFeatureConvert = PIXPAT_FEATURE_CONVERT;
+
+static FormatId lookup_format(const char* name) noexcept
+{
+ if (!name)
+ return FormatId::Unknown;
+ for (const auto& e : s_format_table)
+ if (std::strcmp(e.name, name) == 0)
+ return e.id;
+ return FormatId::Unknown;
+}
+
+// Per-source: fill `bh` rows of normalized pixels by calling Src::read.
+// Address is taken in s_format_info[] for every readable format. When
+// no format is readable (convert disabled) no specialization is
+// instantiated, so this template emits no code.
+template <typename Src>
+static void unpack_to_norm(uint8_t* norm, const pixpat_buffer* src,
+ size_t by, size_t bh, size_t W) noexcept
+{
+ using P = typename Src::Pixel;
+ auto sb = make_buffer<typename Src::Layout>(src);
+ auto* dst = reinterpret_cast<P*>(norm);
+ const size_t H = src->height;
+ for (size_t dy = 0; dy < bh; ++dy)
+ for (size_t x = 0; x < W; ++x)
+ dst[dy * W + x] = Src::read(sb, x, by + dy, W, H);
+}
+
+// Per-sink: re-block `Snk::block_h × W` of normalized pixels and call
+// Sink::write_block. Snk's block_h dictates how many normalized rows
+// the caller has to have prepared. Used by the normalized pivot for
+// both convert (cold path) and pattern.
+template <typename Snk>
+static void pack_from_norm(const pixpat_buffer* dst,
+ const uint8_t* norm,
+ size_t by, size_t W) noexcept
+{
+ using P = typename Snk::Pixel;
+ constexpr size_t bh = Snk::block_h;
+ constexpr size_t bw = Snk::block_w;
+ auto db = make_buffer<typename Snk::Layout>(dst);
+ auto* src = reinterpret_cast<const P*>(norm);
+ for (size_t bx = 0; bx < W; bx += bw) {
+ P block[bh][bw];
+ for (size_t dy = 0; dy < bh; ++dy)
+ for (size_t dx = 0; dx < bw; ++dx)
+ block[dy][dx] = src[dy * W + bx + dx];
+ Snk::write_block(db, bx, by, block);
+ }
+}
+
+// Generated: s_format_caps[] indexed by FormatId, plus s_pattern_* /
+// DefaultPattern (used only by pixpat_pattern.cpp; harmless here).
+#include "pixpat_caps.inc"
+
+static_assert(sizeof(s_format_caps) / sizeof(s_format_caps[0]) == s_format_catalog_count,
+ "s_format_caps must cover the full catalog");
+
+// `if constexpr` keeps disabled-direction function-template bodies
+// uninstantiated. Taking `&unpack_to_norm<Src>` / `&pack_from_norm<Snk>`
+// forces the function body to be emitted; without the gate every
+// catalog format would carry unpack and pack code regardless of its
+// readable / writable bit. Snk::block_h / Snk::block_w are constexpr
+// scalars — no body, no emission — so they're inlined directly in the
+// initializer below, without a helper.
+template <bool Read, typename Src>
+static constexpr UnpackFn unpack_for() noexcept
+{
+ if constexpr (Read)
+ return &unpack_to_norm<Src>;
+ else
+ return nullptr;
+}
+
+template <bool Write, typename Snk>
+static constexpr PackFn pack_for() noexcept
+{
+ if constexpr (Write)
+ return &pack_from_norm<Snk>;
+ else
+ return nullptr;
+}
+
+const FormatInfo s_format_info[] = {
+#define CAPS(name) s_format_caps[size_t(FormatId::name)]
+#define X(name) \
+ { \
+ unpack_for<CAPS(name).readable, formats::name::Source>(), \
+ pack_for<CAPS(name).writable, formats::name::Sink>(), \
+ formats::name::kind, \
+ uint8_t(formats::name::h_sub), \
+ uint8_t(formats::name::v_sub), \
+ uint8_t(formats::name::Sink::block_h), \
+ uint8_t(formats::name::Sink::block_w), \
+ },
+ PIXPAT_FORMAT_LIST(X)
+#undef X
+#undef CAPS
+};
+static_assert(sizeof(s_format_info) / sizeof(s_format_info[0]) == s_format_catalog_count,
+ "s_format_info must cover the full catalog");
+
+// validate_* / parse_spec are only reached from inside the entry points'
+// `if constexpr (kFeatureXxx)` true branches. With a feature disabled,
+// its caller's branch is discarded and the helper becomes unreferenced;
+// require_readable is convert-only. [[maybe_unused]] keeps
+// -Wunused-function (and clang's -Wunneeded-internal-declaration) quiet.
+[[maybe_unused]] static void validate_buffer(const pixpat_buffer* b)
+{
+ if (!b)
+ throw invalid_argument("null buffer");
+ if (b->width == 0 || b->height == 0)
+ throw invalid_argument("zero-sized buffer");
+}
+
+[[maybe_unused]] static FormatId validate_format(const char* name)
+{
+ auto id = lookup_format(name);
+ if (id == FormatId::Unknown)
+ throw invalid_argument("unknown format");
+ return id;
+}
+
+[[maybe_unused]] static void require_writable(FormatId id)
+{
+ if (s_format_info[size_t(id)].pack == nullptr)
+ throw invalid_argument("format not enabled as a sink in this build");
+}
+
+[[maybe_unused]] static void require_readable(FormatId id)
+{
+ if (s_format_info[size_t(id)].unpack == nullptr)
+ throw invalid_argument("format not enabled as a source in this build");
+}
+
+[[maybe_unused]] static unsigned validate_thread_count(int n)
+{
+ if (n < 0)
+ throw invalid_argument("negative num_threads");
+ return n > 0 ? static_cast<unsigned>(n) : default_thread_count();
+}
+
+// Map the C-side pixpat_rec / pixpat_range enums (defined in
+// pixpat.h with explicit values 0/1/2 for rec, 0/1 for range) onto
+// the internal pixpat::Rec / pixpat::Range. Out-of-range values fall
+// back to BT.601 / Limited — matching the zero-initialised opts
+// struct and kDefaultColorSpec.
+[[maybe_unused]] static ColorSpec parse_spec(int rec_in, int range_in) noexcept
+{
+ Rec rec;
+ switch (rec_in) {
+ case PIXPAT_REC_BT709: rec = Rec::BT709; break;
+ case PIXPAT_REC_BT2020: rec = Rec::BT2020; break;
+ default: rec = Rec::BT601; break;
+ }
+ Range range = (range_in == PIXPAT_RANGE_FULL) ? Range::Full : Range::Limited;
+ return ColorSpec{ rec, range };
+}
+
+} // namespace pixpat
+
+// Marks the C entry points as part of the public ABI: restores default
+// visibility against the build-wide -fvisibility=hidden, so they are
+// exported from libpixpat.so.
+#define PIXPAT_API __attribute__((visibility("default")))
+
+extern "C" {
+
+PIXPAT_API int pixpat_draw_pattern(const pixpat_buffer* dst,
+ const char* pattern,
+ const pixpat_pattern_opts* opts)
+{
+ if constexpr (pixpat::kFeaturePattern) {
+ try {
+ pixpat::validate_buffer(dst);
+ auto id = pixpat::validate_format(dst->format);
+ pixpat::require_writable(id);
+ const auto& di = pixpat::s_format_info[size_t(id)];
+ if (dst->width % di.snk_block_w != 0 ||
+ dst->height % di.snk_block_h != 0)
+ throw pixpat::invalid_argument(
+ "dimensions not aligned to format block");
+ const unsigned n_threads = opts
+ ? pixpat::validate_thread_count(opts->num_threads)
+ : pixpat::default_thread_count();
+ const pixpat::ColorSpec spec = opts
+ ? pixpat::parse_spec(opts->rec, opts->range)
+ : pixpat::kDefaultColorSpec;
+ const pixpat::Params params(opts ? opts->params : nullptr);
+ if (!params.ok())
+ throw pixpat::invalid_argument("malformed opts->params");
+
+ pixpat::run_stripes(dst->height, di.snk_block_h, n_threads,
+ [&](size_t y0, size_t y1) {
+ pixpat::dispatch_draw_pattern(
+ id, pattern, params, dst,
+ dst->width, dst->height, y0, y1, spec);
+ });
+ return 0;
+ } catch (const std::exception&) {
+ return -1;
+ }
+ } else {
+ (void)dst;
+ (void)pattern;
+ (void)opts;
+ return -1;
+ }
+}
+
+PIXPAT_API int pixpat_convert(const pixpat_buffer* dst,
+ const pixpat_buffer* src,
+ const pixpat_convert_opts* opts)
+{
+ if constexpr (pixpat::kFeatureConvert) {
+ try {
+ pixpat::validate_buffer(dst);
+ pixpat::validate_buffer(src);
+ if (src->width != dst->width || src->height != dst->height)
+ throw pixpat::invalid_argument("src/dst dimensions differ");
+
+ auto src_id = pixpat::validate_format(src->format);
+ auto dst_id = pixpat::validate_format(dst->format);
+ pixpat::require_readable(src_id);
+ pixpat::require_writable(dst_id);
+
+ const auto& si = pixpat::s_format_info[size_t(src_id)];
+ const auto& di = pixpat::s_format_info[size_t(dst_id)];
+ // Each constraint must hold independently — checking only
+ // max() would miss e.g. h_sub=2 vs snk_block_w=3 with W=3.
+ if (src->width % si.h_sub != 0 || src->height % si.v_sub != 0 ||
+ src->width % di.h_sub != 0 || src->height % di.v_sub != 0 ||
+ src->width % di.snk_block_w != 0 || src->height % di.snk_block_h != 0)
+ throw pixpat::invalid_argument(
+ "dimensions not aligned to format subsampling");
+ // run_stripes only needs the v dimension. Stripes must align
+ // to si.v_sub (source reads) and di.snk_block_h (sink block
+ // loop); for pixpat's catalog these are powers-of-two and
+ // max == LCM.
+ const unsigned vs = std::max({ unsigned(si.v_sub),
+ unsigned(di.v_sub),
+ unsigned(di.snk_block_h) });
+ const unsigned n_threads = opts
+ ? pixpat::validate_thread_count(opts->num_threads)
+ : pixpat::default_thread_count();
+ const pixpat::ColorSpec spec = opts
+ ? pixpat::parse_spec(opts->rec, opts->range)
+ : pixpat::kDefaultColorSpec;
+
+ pixpat::run_stripes(src->height, vs, n_threads,
+ [&](size_t y0, size_t y1) {
+ pixpat::dispatch_convert(src_id, dst_id, src, dst,
+ src->width, src->height,
+ y0, y1, spec);
+ });
+ return 0;
+ } catch (const std::exception&) {
+ return -1;
+ }
+ } else {
+ (void)dst;
+ (void)src;
+ (void)opts;
+ return -1;
+ }
+}
+
+PIXPAT_API int pixpat_format_supported(const char* format)
+{
+ auto id = pixpat::lookup_format(format);
+ if (id == pixpat::FormatId::Unknown)
+ return 0;
+ return pixpat::s_format_caps[size_t(id)].enabled() ? 1 : 0;
+}
+
+PIXPAT_API size_t pixpat_format_count(void)
+{
+ size_t n = 0;
+ for (const auto& c : pixpat::s_format_caps)
+ if (c.enabled())
+ ++n;
+ return n;
+}
+
+PIXPAT_API const char* pixpat_format_name(size_t idx)
+{
+ size_t n = 0;
+ for (size_t i = 0; i < pixpat::s_format_catalog_count; ++i) {
+ if (!pixpat::s_format_caps[i].enabled())
+ continue;
+ if (n++ == idx)
+ return pixpat::s_format_table[i].name;
+ }
+ return nullptr;
+}
+
+} // extern "C"
diff --git a/subprojects/pixpat/pixpat-native/src/pixpat_convert.cpp b/subprojects/pixpat/pixpat-native/src/pixpat_convert.cpp
new file mode 100644
index 0000000..63461d8
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/pixpat_convert.cpp
@@ -0,0 +1,201 @@
+// Convert-feature TU: built only when PIXPAT_FEATURE_CONVERT is on
+// (controlled by the meson source list). pixpat.cpp's pixpat_convert
+// entry calls into dispatch_convert() below via if-constexpr; when the
+// feature is off this file isn't compiled, the discarded if-constexpr
+// branch emits no symbol reference, and the .so simply lacks these
+// symbols.
+
+#include <cassert>
+#include <cstdint>
+#include <cstring>
+#include <vector>
+
+#include "color.h"
+#include "error.h"
+#include "format_catalog.h"
+#include "formats.h"
+#include "io.h"
+#include "layout.h"
+#include "pattern.h"
+#include "pipeline.h"
+#include "pixpat_internal.h"
+
+namespace pixpat
+{
+
+template <typename Src, typename Snk>
+static void run_convert_impl(const pixpat_buffer* src, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ using SL = typename Src::Layout;
+ using DL = typename Snk::Layout;
+ // Entry point (pixpat_convert) validates W/H against each layout's
+ // h_sub / v_sub, plus the sink's block dims.
+ assert(W % SL::h_sub == 0 && W % DL::h_sub == 0);
+ assert(H % SL::v_sub == 0 && H % DL::v_sub == 0);
+
+ auto sb = make_buffer<SL>(src);
+ auto db = make_buffer<DL>(dst);
+ Converter<Src, Snk>::run(sb, db, W, H, by_start, by_end, spec);
+}
+
+static void run_norm(FormatId src_id, FormatId dst_id,
+ const pixpat_buffer* src, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ const auto& si = s_format_info[size_t(src_id)];
+ const auto& di = s_format_info[size_t(dst_id)];
+
+ const size_t bh = di.snk_block_h;
+ // Entry point (pixpat_convert) guarantees W/H alignment to each
+ // of si.h_sub / si.v_sub and di.snk_block_w / di.snk_block_h.
+ assert(W % si.h_sub == 0 && W % di.snk_block_w == 0);
+ assert(H % si.v_sub == 0 && H % bh == 0);
+
+ // Per-thread normalized line buffer. RGB16 and YUV16 are both 8
+ // bytes, so one allocation works for both. thread_local gives each
+ // worker its own buffer when called from run_stripes.
+ thread_local std::vector<uint8_t> norm;
+ norm.resize(bh * W * sizeof(RGB16));
+
+ const ColorCoeffs c = coeffs_for(spec);
+ for (size_t by = by_start; by < by_end; by += bh) {
+ si.unpack(norm.data(), src, by, bh, W);
+ if (si.kind != di.kind) {
+ const size_t n = bh * W;
+ if (si.kind == ColorKind::RGB)
+ norm_rgb_to_yuv(norm.data(), n, c);
+ else
+ norm_yuv_to_rgb(norm.data(), n, c);
+ }
+ di.pack(dst, norm.data(), by, W);
+ }
+}
+
+// Generated: FormatCaps + s_format_caps[] (per-format readable/writable
+// + hot_src/hot_dst), plus s_pattern_* / DefaultPattern.
+#include "pixpat_caps.inc"
+
+// Per-Src dispatch: pick the right Sink for `dst_id` and call
+// run_convert_impl. The X-macro emits one case per catalog format;
+// `if constexpr (...writable)` discards the body for non-writable
+// formats — those cases fall to the trailing throw.
+template <typename Src>
+static void dispatch_dst_convert(FormatId dst_id,
+ const pixpat_buffer* src, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ switch (dst_id) {
+#define CAPS(name) s_format_caps[size_t(FormatId::name)]
+#define X(name) \
+ case FormatId::name: \
+ if constexpr (CAPS(name).writable) { \
+ run_convert_impl<Src, formats::name::Sink>( \
+ src, dst, W, H, by_start, by_end, spec); \
+ return; \
+ } \
+ break;
+ PIXPAT_FORMAT_LIST(X)
+#undef X
+#undef CAPS
+ default:
+ break;
+ }
+ throw invalid_argument("destination format not enabled in this build");
+}
+
+// Per-Snk dispatch: mirror of dispatch_dst_convert.
+template <typename Snk>
+static void dispatch_src_convert(FormatId src_id,
+ const pixpat_buffer* src, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ switch (src_id) {
+#define CAPS(name) s_format_caps[size_t(FormatId::name)]
+#define X(name) \
+ case FormatId::name: \
+ if constexpr (CAPS(name).readable) { \
+ run_convert_impl<formats::name::Source, Snk>( \
+ src, dst, W, H, by_start, by_end, spec); \
+ return; \
+ } \
+ break;
+ PIXPAT_FORMAT_LIST(X)
+#undef X
+#undef CAPS
+ default:
+ break;
+ }
+ throw invalid_argument("source format not enabled in this build");
+}
+
+// Hot-pivot probes. The wrapper has to be a template so that the
+// discarded `if constexpr` branch is not instantiated — otherwise
+// dispatch_dst_convert<formats::X::Source> would be instantiated for
+// every catalog format, not just hot pivots.
+template <bool HotSrc, FormatId Id, typename Source>
+static bool try_hot_src(FormatId src_id, FormatId dst_id,
+ const pixpat_buffer* src, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ if constexpr (HotSrc) {
+ if (src_id == Id) {
+ dispatch_dst_convert<Source>(
+ dst_id, src, dst, W, H, by_start, by_end, spec);
+ return true;
+ }
+ }
+ return false;
+}
+
+template <bool HotDst, FormatId Id, typename Sink>
+static bool try_hot_dst(FormatId src_id, FormatId dst_id,
+ const pixpat_buffer* src, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ if constexpr (HotDst) {
+ if (dst_id == Id) {
+ dispatch_src_convert<Sink>(
+ src_id, src, dst, W, H, by_start, by_end, spec);
+ return true;
+ }
+ }
+ return false;
+}
+
+void dispatch_convert(FormatId src_id, FormatId dst_id,
+ const pixpat_buffer* src, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+#define CAPS(name) s_format_caps[size_t(FormatId::name)]
+#define X(name) \
+ if (try_hot_src<CAPS(name).hot_src, FormatId::name, \
+ formats::name::Source>( \
+ src_id, dst_id, src, dst, W, H, by_start, by_end, spec)) \
+ return; \
+ if (try_hot_dst<CAPS(name).hot_dst, FormatId::name, \
+ formats::name::Sink>( \
+ src_id, dst_id, src, dst, W, H, by_start, by_end, spec)) \
+ return;
+ PIXPAT_FORMAT_LIST(X)
+#undef X
+#undef CAPS
+
+ run_norm(src_id, dst_id, src, dst, W, H, by_start, by_end, spec);
+}
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/pixpat_internal.h b/subprojects/pixpat/pixpat-native/src/pixpat_internal.h
new file mode 100644
index 0000000..50d3405
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/pixpat_internal.h
@@ -0,0 +1,89 @@
+#pragma once
+
+// Internal interface shared between the always-built pixpat.cpp and the
+// optional pixpat_pattern.cpp / pixpat_convert.cpp TUs. The feature
+// gate is meson-side: pixpat_pattern.cpp is in the source list iff
+// PIXPAT_FEATURE_PATTERN, and likewise for convert. The bridge
+// declarations below are unconditional; pixpat.cpp's entry points call
+// them inside `if constexpr (kFeatureXxx)`, and the discarded branch
+// emits no symbol reference, so absent definitions don't cause link
+// failures.
+
+#include <cstddef>
+#include <cstdint>
+
+#include <pixpat/pixpat.h>
+
+#include "color.h"
+#include "format_catalog.h"
+#include "layout.h"
+#include "pattern_catalog.h"
+
+namespace pixpat
+{
+
+template <typename Layout>
+inline Buffer<Layout::num_planes> make_buffer(const pixpat_buffer* b) noexcept
+{
+ Buffer<Layout::num_planes> out{};
+ for (size_t i = 0; i < Layout::num_planes; ++i) {
+ out.data[i] = static_cast<uint8_t*>(b->planes[i]);
+ out.stride[i] = b->strides[i];
+ }
+ return out;
+}
+
+using UnpackFn = void (*)(uint8_t*, const pixpat_buffer*, size_t, size_t, size_t);
+using PackFn = void (*)(const pixpat_buffer*, const uint8_t*, size_t, size_t);
+
+struct FormatInfo {
+ UnpackFn unpack;
+ PackFn pack;
+ ColorKind kind;
+ uint8_t h_sub;
+ uint8_t v_sub;
+ uint8_t snk_block_h;
+ uint8_t snk_block_w;
+};
+
+extern const FormatInfo s_format_info[];
+
+// Per-format build capabilities. Defined once per build by the
+// generator into s_format_caps[] (in pixpat_caps.inc); the schema is
+// here so that file is pure data.
+struct FormatCaps {
+ bool readable;
+ bool writable;
+ bool hot_src;
+ bool hot_dst;
+
+ constexpr bool enabled() const noexcept
+ {
+ return readable || writable;
+ }
+};
+
+// Per-pattern build capabilities. Generator emits s_pattern_caps[]
+// indexed by PatternId, plus a separate s_default_pattern_id singleton
+// (the fallback when pattern_name doesn't match any enabled arm).
+// Used only when PIXPAT_FEATURE_PATTERN — pixpat_pattern.cpp consumes
+// both.
+struct PatternCaps {
+ bool enabled;
+};
+
+class Params;
+
+// Bridge into pixpat_pattern.cpp (defined there iff PIXPAT_FEATURE_PATTERN).
+void dispatch_draw_pattern(FormatId id, const char* pattern_name,
+ const Params& params,
+ const pixpat_buffer* dst, size_t W, size_t H,
+ size_t by_start, size_t by_end, ColorSpec spec);
+
+// Bridge into pixpat_convert.cpp (defined there iff PIXPAT_FEATURE_CONVERT).
+void dispatch_convert(FormatId src_id, FormatId dst_id,
+ const pixpat_buffer* src, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end, ColorSpec spec);
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/pixpat_pattern.cpp b/subprojects/pixpat/pixpat-native/src/pixpat_pattern.cpp
new file mode 100644
index 0000000..e8ac780
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/pixpat_pattern.cpp
@@ -0,0 +1,168 @@
+// Pattern-feature TU: built only when PIXPAT_FEATURE_PATTERN is on
+// (controlled by the meson source list). pixpat.cpp's pixpat_draw_pattern
+// entry calls into dispatch_draw_pattern() below via if-constexpr; when
+// the feature is off this file isn't compiled, the discarded if-constexpr
+// branch emits no symbol reference, and the .so simply lacks these
+// symbols.
+
+#include <cassert>
+#include <cstdint>
+#include <string_view>
+#include <type_traits>
+#include <vector>
+
+#include "color.h"
+#include "error.h"
+#include "params.h"
+#include "pattern.h"
+#include "pattern_catalog.h"
+#include "pipeline.h"
+#include "pixpat_internal.h"
+
+namespace pixpat
+{
+
+// Generated: s_pattern_* enable flags + DefaultPattern alias. Included
+// inside namespace pixpat so the unqualified FormatId / s_format_catalog_count
+// references resolve.
+#include "pixpat_caps.inc"
+
+// Cold pattern path: fill a per-thread normalized line buffer with
+// Pattern samples in the pattern's native color kind, run a cross-
+// color-kind pass over the buffer if the sink wants the other kind,
+// then hand the buffer to the destination's per-format pack via
+// s_format_info. Same shape as run_norm in pixpat_convert.cpp.
+template <typename Pattern>
+static void run_pattern_norm(const Pattern& pat,
+ FormatId dst_id, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ using P = typename Pattern::Pixel;
+ constexpr bool pat_is_rgb = std::is_same_v<P, RGB16>;
+
+ const auto& di = s_format_info[size_t(dst_id)];
+ const size_t bh = di.snk_block_h;
+ // Entry point (pixpat_draw_pattern) validates W%bw / H%bh.
+ assert(W % di.snk_block_w == 0 && H % bh == 0);
+
+ thread_local std::vector<uint8_t> norm;
+ norm.resize(bh * W * sizeof(RGB16)); // RGB16 / YUV16 same size
+
+ const ColorCoeffs c = coeffs_for(spec);
+ const bool need_xfm = (pat_is_rgb && di.kind == ColorKind::YUV) ||
+ (!pat_is_rgb && di.kind == ColorKind::RGB);
+
+ for (size_t by = by_start; by < by_end; by += bh) {
+ auto* px = reinterpret_cast<P*>(norm.data());
+ for (size_t dy = 0; dy < bh; ++dy)
+ for (size_t x = 0; x < W; ++x)
+ px[dy * W + x] = pat.sample(x, by + dy, W, H);
+ if (need_xfm) {
+ const size_t n = bh * W;
+ if constexpr (pat_is_rgb)
+ norm_rgb_to_yuv(norm.data(), n, c);
+ else
+ norm_yuv_to_rgb(norm.data(), n, c);
+ }
+ di.pack(dst, norm.data(), by, W);
+ }
+}
+
+// Construct, ready-check, and run a pattern. Patterns whose colors
+// depend on the call's ColorSpec (e.g. native-YUV bar variants) opt
+// in by exposing a (Params, ColorSpec) constructor; the rest take
+// Params only and stay unchanged.
+template <typename Pattern>
+static void run_one_pattern(const Params& params,
+ FormatId id, const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ auto pat = [&] {
+ if constexpr (std::is_constructible_v<
+ Pattern, const Params&, ColorSpec>)
+ return Pattern(params, spec);
+ else
+ return Pattern(params);
+ }();
+ if constexpr (requires { pat.ready(); }) {
+ if (!pat.ready())
+ throw invalid_argument("pattern parameters not accepted");
+ }
+ run_pattern_norm(pat, id, dst, W, H, by_start, by_end, spec);
+}
+
+// Per-pattern dispatch arm. Templated on the catalog row's RGB and
+// YUV variants (either may be `void` if the pattern has no variant
+// in that kind). When both are present, the sink kind picks the
+// matching variant so the cross-kind pass is a no-op; when only one
+// is present, the pipeline runs the cross-kind pass for opposite-
+// kind sinks.
+//
+// Wrapping in a templated helper is what keeps the binary size down:
+// `if constexpr (Enabled = false)` discards the run_pattern_norm
+// reference, and because try_pattern is itself a template, the
+// discarded branch is *not instantiated* — so disabled patterns
+// emit no code, and the `void` arms of partial patterns never
+// instantiate `Pattern::Pixel` or run_pattern_norm<void>.
+template <bool Enabled, typename Rgb, typename Yuv>
+static bool try_pattern(std::string_view name, std::string_view want,
+ const Params& params,
+ FormatId id, ColorKind sink_kind,
+ const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ if constexpr (Enabled) {
+ if (name == want) {
+ constexpr bool has_rgb = !std::is_void_v<Rgb>;
+ constexpr bool has_yuv = !std::is_void_v<Yuv>;
+ static_assert(has_rgb || has_yuv,
+ "pattern needs at least one variant");
+ if constexpr (has_rgb && has_yuv) {
+ if (sink_kind == ColorKind::YUV)
+ run_one_pattern<Yuv>(params, id, dst, W, H,
+ by_start, by_end, spec);
+ else
+ run_one_pattern<Rgb>(params, id, dst, W, H,
+ by_start, by_end, spec);
+ } else if constexpr (has_rgb) {
+ run_one_pattern<Rgb>(params, id, dst, W, H,
+ by_start, by_end, spec);
+ } else {
+ run_one_pattern<Yuv>(params, id, dst, W, H,
+ by_start, by_end, spec);
+ }
+ return true;
+ }
+ }
+ return false;
+}
+
+void dispatch_draw_pattern(FormatId id, const char* pattern_name,
+ const Params& params,
+ const pixpat_buffer* dst,
+ size_t W, size_t H,
+ size_t by_start, size_t by_end,
+ ColorSpec spec)
+{
+ using namespace patterns;
+ // NULL pattern_name selects the default ("kmstest"); see pixpat.h.
+ const std::string_view name = pattern_name ? pattern_name : "kmstest";
+ const ColorKind kind = s_format_info[size_t(id)].kind;
+
+#define X(label, rgb, yuv, str) \
+ if (try_pattern<s_pattern_caps[size_t(PatternId::label)].enabled, rgb, yuv>( \
+ name, str, params, id, kind, dst, W, H, by_start, by_end, spec)) \
+ return;
+ PIXPAT_PATTERN_LIST(X)
+#undef X
+
+ throw invalid_argument("unknown or disabled pattern name");
+}
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/src/threading.h b/subprojects/pixpat/pixpat-native/src/threading.h
new file mode 100644
index 0000000..5e7fc01
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/src/threading.h
@@ -0,0 +1,95 @@
+#pragma once
+
+#include <unistd.h>
+
+#include <cassert>
+#include <cstddef>
+#include <exception>
+#include <thread>
+#include <vector>
+
+namespace pixpat
+{
+
+inline unsigned default_thread_count()
+{
+ long n = sysconf(_SC_NPROCESSORS_ONLN);
+ if (n < 1)
+ return 1;
+ // Cap to keep per-stripe work meaningful and avoid heavy
+ // oversubscription on large NUMA hosts.
+ if (n > 16)
+ n = 16;
+ return static_cast<unsigned>(n);
+}
+
+/*
+ * Run `fn(start_y, end_y)` over `[0, height)` partitioned into stripes
+ * aligned to `v_sub`. Half-open ranges, matching the `for (by = 0;
+ * by < H; by += bh)` block-loop style.
+ *
+ * `fn` must be callable as `void(size_t start_y, size_t end_y)` and is
+ * invoked concurrently from multiple threads — it must be safe to call
+ * with disjoint Y-ranges in parallel. Exceptions thrown from a worker
+ * are captured and the first (by stripe index) is rethrown after all
+ * workers join.
+ *
+ * When `n_threads <= 1`, `fn` is called inline on the calling thread —
+ * no `std::thread` is spawned, no allocation occurs.
+ */
+template<typename F>
+void run_stripes(size_t height, unsigned v_sub, unsigned n_threads, F&& fn)
+{
+ if (height == 0 || v_sub == 0)
+ return;
+
+ // Callers (pixpat_convert / pixpat_draw_pattern) validate divisibility
+ // at the entry point.
+ assert(height % v_sub == 0);
+
+ const size_t max_useful = height / v_sub;
+ if (n_threads == 0)
+ n_threads = 1;
+ if (static_cast<size_t>(n_threads) > max_useful)
+ n_threads = static_cast<unsigned>(max_useful);
+
+ if (n_threads <= 1) {
+ fn(size_t{ 0 }, height);
+ return;
+ }
+
+ // Stripe height rounded up to v_sub; last stripe absorbs the
+ // remainder.
+ size_t part_height = (height + n_threads - 1) / n_threads;
+ part_height = (part_height + v_sub - 1) / v_sub * v_sub;
+
+ std::vector<std::exception_ptr> errors(n_threads);
+ std::vector<std::thread> workers;
+ workers.reserve(n_threads);
+
+ for (unsigned i = 0; i < n_threads; i++) {
+ size_t start = i * part_height;
+ if (start >= height)
+ break;
+ size_t end = start + part_height;
+ if (i == n_threads - 1 || end > height)
+ end = height;
+
+ workers.emplace_back([&, i, start, end] {
+ try {
+ fn(start, end);
+ } catch (...) {
+ errors[i] = std::current_exception();
+ }
+ });
+ }
+
+ for (auto& t : workers)
+ t.join();
+
+ for (auto& e : errors)
+ if (e)
+ std::rethrow_exception(e);
+}
+
+} // namespace pixpat
diff --git a/subprojects/pixpat/pixpat-native/tests/meson.build b/subprojects/pixpat/pixpat-native/tests/meson.build
new file mode 100644
index 0000000..cbdfda2
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/tests/meson.build
@@ -0,0 +1,11 @@
+add_languages('c', native : false)
+
+test('pixpat-smoke-cpp',
+ executable('test_pixpat_cpp',
+ 'test_pixpat.cpp',
+ dependencies : libpixpat_dep))
+
+test('pixpat-smoke-c',
+ executable('test_pixpat_c',
+ 'test_pixpat_c.c',
+ dependencies : libpixpat_dep))
diff --git a/subprojects/pixpat/pixpat-native/tests/test_pixpat.cpp b/subprojects/pixpat/pixpat-native/tests/test_pixpat.cpp
new file mode 100644
index 0000000..8319968
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/tests/test_pixpat.cpp
@@ -0,0 +1,107 @@
+/*
+ * Native C++ smoke test for libpixpat.
+ *
+ * Scope is narrow: prove that the public header, the shared library,
+ * and the C ABI surface are wired up correctly when consumed from C++.
+ * Behavioral coverage (formats, patterns, error paths, threading,
+ * conversion matrix) lives in the Python test suite, which exercises
+ * the same C ABI with much less boilerplate.
+ */
+
+#include <pixpat/pixpat.h>
+
+#include <cstdint>
+#include <cstdio>
+#include <vector>
+
+// CHECK always evaluates its expression so the test still runs in
+// release builds where assert() is compiled out.
+#define CHECK(expr) do { \
+ if (!(expr)) { \
+ std::fprintf(stderr, "%s:%d: CHECK failed: %s\n", \
+ __FILE__, __LINE__, #expr); \
+ return 1; \
+ } \
+} while (0)
+
+int main()
+{
+ // Format introspection.
+ CHECK(pixpat_format_count() > 0);
+ CHECK(pixpat_format_name(0) != nullptr);
+ CHECK(pixpat_format_supported("XRGB8888") == 1);
+ CHECK(pixpat_format_supported("NOT_A_REAL_FORMAT") == 0);
+
+ // Draw a pattern into an RGB buffer.
+ const uint32_t w = 64, h = 32;
+ std::vector<uint8_t> rgb(w * h * 4, 0);
+ pixpat_buffer fb{};
+ fb.format = "XRGB8888";
+ fb.width = w;
+ fb.height = h;
+ fb.num_planes = 1;
+ fb.planes[0] = rgb.data();
+ fb.strides[0] = w * 4;
+
+ pixpat_pattern_opts pat{};
+ pat.rec = PIXPAT_REC_BT709;
+ pat.range = PIXPAT_RANGE_LIMITED;
+ CHECK(pixpat_draw_pattern(&fb, "smpte", &pat) == 0);
+ // Cover the NULL-opts path.
+ CHECK(pixpat_draw_pattern(&fb, nullptr, nullptr) == 0);
+
+ // Plain pattern: hex color via opts->params. BGR888-style byte
+ // order means the first pixel of XRGB8888 stores B at byte 0,
+ // G at byte 1, R at byte 2.
+ pixpat_pattern_opts plain_opts{};
+ plain_opts.params = "color=ff0000";
+ CHECK(pixpat_draw_pattern(&fb, "plain", &plain_opts) == 0);
+ CHECK(rgb[0] == 0x00 && rgb[1] == 0x00 && rgb[2] == 0xFF);
+ // Missing/malformed params for `plain` must fail.
+ plain_opts.params = nullptr;
+ CHECK(pixpat_draw_pattern(&fb, "plain", &plain_opts) == -1);
+ plain_opts.params = "color=zzzzzz";
+ CHECK(pixpat_draw_pattern(&fb, "plain", &plain_opts) == -1);
+ // Top-level params parse failure also fails the call.
+ plain_opts.params = "foo,bar";
+ CHECK(pixpat_draw_pattern(&fb, "plain", &plain_opts) == -1);
+
+ // Cover the rest of the pattern catalog. None of these accept
+ // failure at the C-API entry point with valid inputs.
+ pixpat_pattern_opts po{};
+ CHECK(pixpat_draw_pattern(&fb, "checker", nullptr) == 0); // default cell
+ po.params = "cell=1";
+ CHECK(pixpat_draw_pattern(&fb, "checker", &po) == 0);
+ po.params = "cell=0";
+ CHECK(pixpat_draw_pattern(&fb, "checker", &po) == -1); // non-positive
+ po.params = "cell=oops";
+ CHECK(pixpat_draw_pattern(&fb, "checker", &po) == -1); // non-numeric
+ CHECK(pixpat_draw_pattern(&fb, "hramp", nullptr) == 0);
+ CHECK(pixpat_draw_pattern(&fb, "vramp", nullptr) == 0);
+ CHECK(pixpat_draw_pattern(&fb, "dramp", nullptr) == 0);
+ CHECK(pixpat_draw_pattern(&fb, "zoneplate", nullptr) == 0);
+
+ // Convert into the normalized wide RGB format.
+ std::vector<uint8_t> wide(w * h * 8, 0);
+ pixpat_buffer dst{};
+ dst.format = "ABGR16161616";
+ dst.width = w;
+ dst.height = h;
+ dst.num_planes = 1;
+ dst.planes[0] = wide.data();
+ dst.strides[0] = w * 8;
+ CHECK(pixpat_convert(&dst, &fb, nullptr) == 0);
+
+ // Error path: unknown format must return -1, not crash.
+ uint8_t dummy[4]{};
+ pixpat_buffer bad{};
+ bad.format = "NOT_A_REAL_FORMAT";
+ bad.width = 1;
+ bad.height = 1;
+ bad.num_planes = 1;
+ bad.planes[0] = dummy;
+ bad.strides[0] = 4;
+ CHECK(pixpat_draw_pattern(&bad, "smpte", &pat) == -1);
+
+ return 0;
+}
diff --git a/subprojects/pixpat/pixpat-native/tests/test_pixpat_c.c b/subprojects/pixpat/pixpat-native/tests/test_pixpat_c.c
new file mode 100644
index 0000000..e80780a
--- /dev/null
+++ b/subprojects/pixpat/pixpat-native/tests/test_pixpat_c.c
@@ -0,0 +1,68 @@
+/*
+ * Native C smoke test for libpixpat.
+ *
+ * Sole purpose: prove that <pixpat/pixpat.h> compiles as C and that the
+ * library links and runs from a C consumer. Behavioral coverage lives
+ * in the Python suite; this file exists to catch C-only regressions in
+ * the public header (stray C++-isms, missing extern "C", etc.) that a
+ * C++ test cannot detect.
+ */
+
+#include <pixpat/pixpat.h>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+/* CHECK always evaluates its expression so the test still runs in
+ * release builds where assert() is compiled out. */
+#define CHECK(expr) do { \
+ if (!(expr)) { \
+ fprintf(stderr, "%s:%d: CHECK failed: %s\n", \
+ __FILE__, __LINE__, #expr); \
+ return 1; \
+ } \
+} while (0)
+
+int main(void)
+{
+ CHECK(pixpat_format_count() > 0);
+ CHECK(pixpat_format_supported("XRGB8888") == 1);
+
+ const uint32_t w = 32, h = 16;
+ uint8_t *rgb = (uint8_t *)calloc((size_t)w * h * 4, 1);
+ uint8_t *wide = (uint8_t *)calloc((size_t)w * h * 8, 1);
+ CHECK(rgb && wide);
+
+ pixpat_buffer fb;
+ memset(&fb, 0, sizeof(fb));
+ fb.format = "XRGB8888";
+ fb.width = w;
+ fb.height = h;
+ fb.num_planes = 1;
+ fb.planes[0] = rgb;
+ fb.strides[0] = w * 4;
+
+ pixpat_pattern_opts pat;
+ memset(&pat, 0, sizeof(pat));
+ pat.rec = PIXPAT_REC_BT709;
+ pat.range = PIXPAT_RANGE_LIMITED;
+ CHECK(pixpat_draw_pattern(&fb, "smpte", &pat) == 0);
+ /* Cover the NULL-opts path. */
+ CHECK(pixpat_draw_pattern(&fb, NULL, NULL) == 0);
+
+ pixpat_buffer dst;
+ memset(&dst, 0, sizeof(dst));
+ dst.format = "ABGR16161616";
+ dst.width = w;
+ dst.height = h;
+ dst.num_planes = 1;
+ dst.planes[0] = wide;
+ dst.strides[0] = w * 8;
+ CHECK(pixpat_convert(&dst, &fb, NULL) == 0);
+
+ free(rgb);
+ free(wide);
+ return 0;
+}
diff --git a/subprojects/pixpat/pixpat-python/pixpat/__init__.py b/subprojects/pixpat/pixpat-python/pixpat/__init__.py
new file mode 100644
index 0000000..6531662
--- /dev/null
+++ b/subprojects/pixpat/pixpat-python/pixpat/__init__.py
@@ -0,0 +1,489 @@
+"""Python bindings for libpixpat.
+
+Loads the bundled shared library via ctypes — no CPython extension. Works on
+any CPython >= 3.9 for the wheel's target architecture.
+
+The library draws test patterns and converts pixel data directly into
+caller-owned pixel buffers, in a wide range of pixel formats
+(planar/semi-planar/packed YUV, RGB, raw, …). The caller is responsible for
+allocating each plane with the correct size and stride for the chosen format.
+
+Pixel data is described by :class:`Buffer`, a passive struct mirroring the
+C ``pixpat_buffer``: format name, width, height, one writable buffer per
+plane, and one row stride per plane. Use :func:`draw_pattern` to fill a
+buffer with a test pattern, and :func:`convert` to convert pixel data
+between formats.
+
+Format names follow the convention used by `kms++` and `pixutils`
+(e.g. ``"XRGB8888"``, ``"NV12"``, ``"YUYV"``) — not the DRM/V4L2
+four-character codes (``"XR24"``, etc.).
+
+Example:
+ >>> import pixpat
+ >>> width, height = 1920, 1080
+ >>> stride = width * 4 # XRGB8888: 4 bytes per pixel
+ >>> data = bytearray(stride * height)
+ >>> buf = pixpat.Buffer(planes=[data], fmt="XRGB8888",
+ ... width=width, height=height, strides=[stride])
+ >>> pixpat.draw_pattern(buf, "smpte")
+
+Use :func:`supported_formats` to enumerate the format names accepted by
+``Buffer.fmt``, and :func:`is_supported` to test a single format.
+
+Using numpy buffers
+-------------------
+
+pixpat does not import numpy and does not depend on it, but any
+C-contiguous ``numpy.ndarray`` works as a plane via Python's buffer
+protocol. The caller is responsible for matching the array's dtype and
+shape to the pixel format and for passing the correct row stride:
+
+ >>> import numpy as np
+ >>> arr = np.zeros((height, width, 4), dtype=np.uint8)
+ >>> stride = arr.strides[0] # bytes per row
+ >>> buf = pixpat.Buffer(planes=[arr], fmt="XRGB8888",
+ ... width=width, height=height, strides=[stride])
+ >>> pixpat.draw_pattern(buf, "smpte")
+
+For multi-plane formats like ``"NV12"`` pass one ndarray per plane (e.g.
+``(h, w)`` uint8 for Y and ``(h//2, w)`` uint8 for the interleaved UV
+plane).
+
+If you already hold all planes in a single contiguous buffer — the layout
+OpenCV uses for NV12, an ``(h * 3 // 2, w)`` uint8 array with Y on top
+and the interleaved UV plane below — slice it into per-plane views and
+pass those::
+
+ >>> nv12 = np.zeros((h * 3 // 2, w), dtype=np.uint8)
+ >>> y, uv = nv12[:h], nv12[h:].reshape(h // 2, w)
+ >>> pixpat.Buffer([y, uv], "NV12", w, h,
+ ... [y.strides[0], uv.strides[0]])
+
+The source side of :func:`convert` accepts read-only buffers (``bytes``,
+``arr.view()`` with ``writeable=False``, mmap'd files, …). The
+destination side must always be writable.
+"""
+
+import ctypes
+from dataclasses import dataclass, field
+from enum import IntEnum
+from typing import Mapping, Optional, Sequence, Union
+
+from ._native import (
+ _Buffer,
+ _ConvertOpts,
+ _PatternOpts,
+ _PinnedBuffers,
+ _fill_buffer,
+ _lib,
+)
+
+_VALID_PATTERNS = frozenset(
+ {
+ 'kmstest',
+ 'smpte',
+ 'plain',
+ 'checker',
+ 'hramp',
+ 'vramp',
+ 'hbar',
+ 'vbar',
+ 'dramp',
+ 'zoneplate',
+ }
+)
+
+
+class Rec(IntEnum):
+ """YCbCr color encoding standard used for YUV output formats.
+
+ Selects the matrix used to convert from internal RGB to YUV. Has no
+ effect when drawing into an RGB or raw format.
+
+ Attributes:
+ BT601: ITU-R BT.601 (standard definition).
+ BT709: ITU-R BT.709 (HD).
+ BT2020: ITU-R BT.2020 (UHD / HDR, non-constant luminance).
+ """
+
+ BT601 = 0
+ BT709 = 1
+ BT2020 = 2
+
+
+class Range(IntEnum):
+ """Quantization range used for YUV output formats.
+
+ Attributes:
+ LIMITED: "TV" / studio range — Y in [16, 235], C in [16, 240]
+ (scaled to bit depth). What most video pipelines expect.
+ FULL: "PC" / full range — every component uses the full code range
+ (e.g. [0, 255] for 8-bit).
+ """
+
+ LIMITED = 0
+ FULL = 1
+
+
+class PixpatError(RuntimeError):
+ """Raised when the underlying ``pixpat_*`` call fails.
+
+ Most commonly indicates an unknown format name or buffer dimensions /
+ plane layout that the format does not allow (e.g. odd width for a
+ horizontally-subsampled YUV format).
+ """
+
+
+@dataclass
+class Buffer:
+ """Plane data + format + dimensions, mirroring the C ``pixpat_buffer``.
+
+ A passive struct: no allocation, no format knowledge, no
+ numpy/cv2 awareness. The caller decides plane shapes and strides
+ based on its own format knowledge (e.g. via ``pixutils``).
+
+ Attributes:
+ planes: One buffer-protocol object per plane (e.g. ``bytearray``,
+ ``array.array``, ``numpy.ndarray``, ``mmap.mmap``,
+ ``memoryview``). Up to 4 planes are supported. Each plane
+ must hold at least ``strides[i] * plane_height`` bytes,
+ where ``plane_height`` is ``height`` for the main plane and
+ ``height // vertical_subsampling`` for chroma planes (e.g.
+ ``height // 2`` for the UV plane of ``NV12``). For
+ destination buffers each plane must be writable; for the
+ source side of :func:`convert` read-only objects (such as
+ ``bytes``) are accepted.
+ fmt: Pixel-format name; see :func:`supported_formats`.
+ width: Image width in pixels. Some formats constrain this — for
+ chroma-subsampled YUV formats it must be a multiple of the
+ horizontal subsampling factor (e.g. 2 for ``NV12``), and
+ packed formats add a further multiple from their
+ pixel-group size (e.g. 6 for ``P030``, 4 for ``SBGGR10P``).
+ height: Image height in pixels. For vertically-subsampled
+ formats (e.g. ``NV12``, ``YUV420``) it must be a multiple
+ of the vertical subsampling factor.
+ strides: Per-plane row stride in bytes. Must have the same
+ length as ``planes``. Strides larger than the minimum row
+ size are allowed.
+ """
+
+ planes: Sequence
+ fmt: str
+ width: int
+ height: int
+ strides: Sequence[int] = field(default_factory=list)
+
+
+def supported_formats() -> list[str]:
+ """Return the list of pixel-format names accepted by :class:`Buffer`.
+
+ Format names follow the convention used by `kms++` and `pixutils`
+ (e.g. ``"XRGB8888"``, ``"NV12"``, ``"YUYV"``) — not the DRM/V4L2
+ four-character codes.
+ """
+ n = _lib.pixpat_format_count()
+ return [_lib.pixpat_format_name(i).decode('ascii') for i in range(n)]
+
+
+def is_supported(fmt: str) -> bool:
+ """Return whether ``fmt`` is a known pixel-format name.
+
+ Equivalent to ``fmt in supported_formats()`` but cheaper — it does not
+ materialize the full list.
+ """
+ return bool(_lib.pixpat_format_supported(fmt.encode('ascii')))
+
+
+def _serialize_pattern_params(
+ params: Optional[Union[str, Mapping[str, object]]],
+) -> Optional[bytes]:
+ """Encode `params` for the C ABI.
+
+ Accepts a ready-made string (passed through verbatim) or a mapping of
+ string keys to stringifiable values, which is serialized to the
+ ``"key=val,key=val"`` form pixpat parses on the C side. Returns None
+ if there is nothing to pass.
+ """
+ if params is None:
+ return None
+ if isinstance(params, str):
+ return params.encode('ascii')
+ if isinstance(params, Mapping):
+ items = []
+ for k, v in params.items():
+ if not isinstance(k, str):
+ raise TypeError(f'pattern params keys must be str, got {type(k).__name__}')
+ sv = str(v)
+ if ',' in k or '=' in k:
+ raise ValueError(f'pattern params key contains , or =: {k!r}')
+ if ',' in sv or '=' in sv:
+ raise ValueError(f'pattern params value contains , or =: {sv!r}')
+ items.append(f'{k}={sv}')
+ return ','.join(items).encode('ascii')
+ raise TypeError(f'pattern params must be None, str, or a Mapping, got {type(params).__name__}')
+
+
+def draw_pattern(
+ dst: Buffer,
+ pattern: Optional[str] = None,
+ *,
+ rec: Rec = Rec.BT601,
+ color_range: Range = Range.LIMITED,
+ num_threads: int = 0,
+ params: Optional[Union[str, Mapping[str, object]]] = None,
+) -> None:
+ """Draw a test pattern into ``dst``.
+
+ Args:
+ dst: Destination buffer; all planes must be writable.
+ pattern: Name of the pattern to draw. ``None`` selects the default
+ (equivalent to ``"kmstest"``). Recognized values:
+
+ * ``"kmstest"`` — color gradients with ramps, in the style of
+ the original ``kmstest`` tool. The default.
+ * ``"smpte"`` — SMPTE RP 219-1 color bars (with PLUGE).
+ * ``"plain"`` — solid color fill from ``params["color"]``.
+ See `params` below.
+ * ``"checker"`` — black/white checkerboard. Optional
+ ``params["cell"]`` (decimal positive integer, default 8)
+ sets the cell size in pixels. ``"cell": "1"`` is a
+ 1-pixel chroma-subsampling stress test.
+ * ``"hramp"`` — four horizontal stripes (R, G, B, gray),
+ each a 0..max ramp along x. Combined per-channel and
+ luma quantization check.
+ * ``"vramp"`` — same as ``"hramp"`` rotated 90°: four
+ vertical columns ramping along y.
+ * ``"hbar"`` — horizontal bar (full image width, narrow
+ along y) over a black background. Required
+ ``params["pos"]`` (signed integer, top edge in pixels);
+ optional ``params["width"]`` (positive integer, default
+ 32). The bar is split into seven equal-width regions
+ colored white/red/white/green/white/blue/white.
+ * ``"vbar"`` — same as ``"hbar"`` rotated 90°: vertical
+ bar with ``pos`` measured along x.
+ * ``"dramp"`` — diagonal RGB ramp (R on x, G on y,
+ B on x+y).
+ * ``"zoneplate"`` — centered radial cosine pattern,
+ frequency ramping from DC at the center to Nyquist at
+ the longer edge. Useful for spotting scaling/aliasing.
+
+ ``"smpte"`` is defined by the spec in BT.709 / Limited.
+ Pass ``rec=Rec.BT709, color_range=Range.LIMITED`` for
+ spec-correct output; other settings produce visibly-wrong
+ colors when drawing into RGB sinks (the caller's matrix is
+ applied to BT.709-encoded values). Callers are trusted —
+ pixpat does not silently override the spec.
+ rec: YCbCr matrix for YUV formats. Ignored for RGB / raw formats.
+ Defaults to :attr:`Rec.BT601`.
+ color_range: Quantization range for YUV formats. Ignored for
+ RGB / raw formats. Defaults to :attr:`Range.LIMITED`.
+ num_threads: Worker-thread count. ``0`` selects a sensible
+ default (one per online CPU, capped to a sane maximum);
+ ``1`` runs single-threaded with no thread-spawn overhead;
+ ``N > 1`` uses exactly ``N`` workers. Defaults to ``0``.
+ Output is bit-identical regardless of the chosen count.
+ params: Optional pattern-specific parameters. Either a mapping
+ (``{"color": "ff0000"}``) — serialized to ``"color=ff0000"``
+ for the C ABI — or a raw ``"key=val,key=val"`` string.
+ Unknown keys are silently ignored; patterns that don't read
+ params (``kmstest``, ``smpte``) ignore this entirely.
+ Per-pattern keys:
+
+ * ``"plain"`` reads ``"color"`` as a hex RGB(A) string with
+ an optional ``"0x"`` prefix. The hex-digit count selects
+ the layout: 6 → 8-bit ``RRGGBB``, 8 → 8-bit ``AARRGGBB``
+ (alpha first), 12 → 16-bit ``RRRRGGGGBBBB``, 16 → 16-bit
+ ``AAAARRRRGGGGBBBB``. Missing or malformed ``"color"``
+ raises :class:`PixpatError`.
+ * ``"checker"`` reads optional ``"cell"`` as a positive
+ decimal integer; default 8. A non-positive or non-numeric
+ value raises :class:`PixpatError`.
+ * ``"hbar"`` / ``"vbar"`` read required ``"pos"`` (signed
+ decimal integer, top/left edge of the bar in pixels;
+ negative values clip at the edge) and optional ``"width"``
+ (positive decimal integer, bar thickness; default 32).
+ Missing/non-numeric ``pos`` or non-positive ``width``
+ raises :class:`PixpatError`.
+
+ Raises:
+ ValueError: ``dst.planes`` exceeds the maximum plane count,
+ ``dst.strides`` length does not match ``dst.planes``,
+ ``pattern`` is not one of the recognized names, or a
+ ``params`` mapping value contains a forbidden ``,`` or ``=``.
+ TypeError: A plane is read-only (e.g. ``bytes``, a read-only
+ ``memoryview``), or ``params`` has an unsupported type.
+ PixpatError: The underlying C call failed — typically an unknown
+ ``fmt``, dimensions / strides incompatible with the format,
+ or pattern parameters that the pattern rejected.
+
+ Example:
+ >>> w, h = 640, 480
+ >>> stride = w * 4
+ >>> data = bytearray(stride * h)
+ >>> dst = Buffer([data], "XRGB8888", w, h, [stride])
+ >>> draw_pattern(dst, "smpte")
+ >>> draw_pattern(dst, "plain", params={"color": "ff0000"})
+ """
+ if pattern is None:
+ pattern = 'kmstest'
+ elif pattern not in _VALID_PATTERNS:
+ raise ValueError(
+ f'unknown pattern {pattern!r}; expected one of {sorted(_VALID_PATTERNS)} or None'
+ )
+ if num_threads < 0:
+ raise ValueError(f'num_threads must be >= 0, got {num_threads}')
+
+ params_bytes = _serialize_pattern_params(params)
+
+ c_buf = _Buffer()
+ opts = _PatternOpts()
+ opts.rec = int(rec)
+ opts.range = int(color_range)
+ opts.num_threads = num_threads
+ opts.params = params_bytes
+
+ with _PinnedBuffers() as pins:
+ _fill_buffer(
+ c_buf,
+ pins,
+ dst.planes,
+ dst.fmt,
+ dst.width,
+ dst.height,
+ dst.strides,
+ writable=True,
+ )
+ rc = _lib.pixpat_draw_pattern(
+ ctypes.byref(c_buf), pattern.encode('ascii'), ctypes.byref(opts)
+ )
+
+ if rc != 0:
+ raise PixpatError(f'pixpat_draw_pattern failed (rc={rc}); check format name and dimensions')
+
+
+def convert(
+ dst: Buffer,
+ src: Buffer,
+ *,
+ rec: Rec = Rec.BT601,
+ color_range: Range = Range.LIMITED,
+ num_threads: int = 0,
+) -> None:
+ """Convert pixel data from ``src`` into ``dst``.
+
+ Both buffers must describe an image of the same width and height;
+ only the pixel format may differ. Any format accepted by
+ :func:`supported_formats` works as both source and destination in
+ the default build (custom build profiles can mark individual formats
+ read-only or write-only). Conversion is routed internally through a
+ 16-bit normalized RGB or YUV intermediate, so format-to-format
+ conversions in either direction (e.g. ``NV12`` -> ``YUV420``,
+ ``XRGB8888`` -> ``NV12``, ``SRGGB10`` -> ``BGR888``) are a single
+ call. Bayer sources are decoded with a 3x3 bilinear demosaic.
+
+ ``src.planes`` may hold read-only buffers (e.g. ``bytes``, mmap'd
+ files, numpy arrays with ``writeable=False``); ``dst.planes`` must
+ be writable.
+
+ Args:
+ dst: Destination buffer.
+ src: Source buffer.
+ rec: YCbCr matrix used when the conversion crosses the RGB/YUV
+ boundary. Ignored when ``src.fmt`` and ``dst.fmt`` share
+ the same color kind. Defaults to :attr:`Rec.BT601`.
+ color_range: Quantization range used when the conversion crosses
+ the RGB/YUV boundary. Ignored when ``src.fmt`` and
+ ``dst.fmt`` share the same color kind. Defaults to
+ :attr:`Range.LIMITED`.
+ num_threads: Worker-thread count. ``0`` selects a sensible
+ default (one per online CPU, capped to a sane maximum);
+ ``1`` runs single-threaded with no thread-spawn overhead;
+ ``N > 1`` uses exactly ``N`` workers. Defaults to ``0``.
+ Output is bit-identical regardless of the chosen count.
+
+ Raises:
+ ValueError: ``dst`` and ``src`` have mismatched dimensions, a
+ plane sequence exceeds the maximum plane count, or a
+ strides length does not match its planes length.
+ TypeError: A ``dst`` plane is read-only.
+ PixpatError: The underlying C call failed — typically an
+ unknown format name, a format disabled in the current build
+ (or disabled in the requested direction), or dimensions /
+ strides incompatible with one of the formats.
+
+ Example:
+ Cross-color-kind, multi-plane on both sides — paint an NV12
+ source via :func:`draw_pattern`, then convert it to planar
+ YUV420:
+
+ >>> w, h = 64, 32
+ >>> y_src = bytearray(w * h)
+ >>> uv_src = bytearray(w * h // 2)
+ >>> draw_pattern(Buffer([y_src, uv_src], "NV12", w, h, [w, w]),
+ ... "smpte")
+ >>> y_dst = bytearray(w * h)
+ >>> u_dst = bytearray(w * h // 4)
+ >>> v_dst = bytearray(w * h // 4)
+ >>> convert(
+ ... Buffer([y_dst, u_dst, v_dst], "YUV420", w, h,
+ ... [w, w // 2, w // 2]),
+ ... Buffer([y_src, uv_src], "NV12", w, h, [w, w]),
+ ... )
+ """
+ if dst.width != src.width or dst.height != src.height:
+ raise ValueError(
+ f'dst dimensions {dst.width}x{dst.height} do not match '
+ f'src dimensions {src.width}x{src.height}'
+ )
+ if num_threads < 0:
+ raise ValueError(f'num_threads must be >= 0, got {num_threads}')
+
+ c_dst = _Buffer()
+ c_src = _Buffer()
+ opts = _ConvertOpts()
+ opts.rec = int(rec)
+ opts.range = int(color_range)
+ opts.num_threads = num_threads
+
+ with _PinnedBuffers() as pins:
+ _fill_buffer(
+ c_dst,
+ pins,
+ dst.planes,
+ dst.fmt,
+ dst.width,
+ dst.height,
+ dst.strides,
+ writable=True,
+ role='dst',
+ )
+ _fill_buffer(
+ c_src,
+ pins,
+ src.planes,
+ src.fmt,
+ src.width,
+ src.height,
+ src.strides,
+ writable=False,
+ role='src',
+ )
+ rc = _lib.pixpat_convert(ctypes.byref(c_dst), ctypes.byref(c_src), ctypes.byref(opts))
+
+ if rc != 0:
+ raise PixpatError(
+ f'pixpat_convert failed (rc={rc}); check format names, dimensions, '
+ f'and that src.fmt is a supported source format'
+ )
+
+
+__all__ = [
+ 'Buffer',
+ 'Rec',
+ 'Range',
+ 'PixpatError',
+ 'supported_formats',
+ 'is_supported',
+ 'draw_pattern',
+ 'convert',
+]
diff --git a/subprojects/pixpat/pixpat-python/pixpat/_lib/.gitkeep b/subprojects/pixpat/pixpat-python/pixpat/_lib/.gitkeep
new file mode 100644
index 0000000..7319712
--- /dev/null
+++ b/subprojects/pixpat/pixpat-python/pixpat/_lib/.gitkeep
@@ -0,0 +1,3 @@
+# Placeholder so the _lib/ directory exists in source control.
+# `pip install .` and `pip install -e .` populate libpixpat.so.0 here
+# (copy and symlink respectively); see setup.py.
diff --git a/subprojects/pixpat/pixpat-python/pixpat/_native.py b/subprojects/pixpat/pixpat-python/pixpat/_native.py
new file mode 100644
index 0000000..46fe452
--- /dev/null
+++ b/subprojects/pixpat/pixpat-python/pixpat/_native.py
@@ -0,0 +1,203 @@
+"""ctypes plumbing for libpixpat.
+
+Private module — public API lives in :mod:`pixpat`. Loads the bundled
+shared library, declares the C structs and function signatures, and
+provides :func:`_fill_buffer` to translate Python plane sequences into
+the C ``pixpat_buffer`` layout while pinning the underlying memory via
+the buffer protocol.
+
+The buffer-protocol path uses ``PyObject_GetBuffer`` / ``PyBuffer_Release``
+directly (rather than ctypes ``from_buffer``) so that read-only inputs
+work for the ``src`` side of :func:`pixpat.convert`. ``from_buffer``
+unconditionally requires a writable buffer.
+"""
+
+import ctypes
+import os
+import pathlib
+from typing import Sequence
+
+_PIXPAT_MAX_PLANES = 4
+
+_lib_override = os.environ.get('PIXPAT_LIB')
+if _lib_override:
+ if not pathlib.Path(_lib_override).exists():
+ raise ImportError(f'pixpat: PIXPAT_LIB={_lib_override} does not exist')
+ _lib = ctypes.CDLL(_lib_override)
+else:
+ _lib_dir = pathlib.Path(__file__).parent / '_lib'
+ _so_candidates = sorted(_lib_dir.glob('libpixpat.so*'))
+ if not _so_candidates:
+ raise ImportError(f'pixpat: no libpixpat.so* found in {_lib_dir}')
+ _lib = ctypes.CDLL(str(_so_candidates[0]))
+
+
+class _Buffer(ctypes.Structure):
+ _fields_ = [
+ ('format', ctypes.c_char_p),
+ ('width', ctypes.c_uint32),
+ ('height', ctypes.c_uint32),
+ ('num_planes', ctypes.c_uint32),
+ ('planes', ctypes.c_void_p * _PIXPAT_MAX_PLANES),
+ ('strides', ctypes.c_uint32 * _PIXPAT_MAX_PLANES),
+ ]
+
+
+class _PatternOpts(ctypes.Structure):
+ _fields_ = [
+ ('rec', ctypes.c_int),
+ ('range', ctypes.c_int),
+ ('num_threads', ctypes.c_int),
+ ('params', ctypes.c_char_p),
+ ]
+
+
+class _ConvertOpts(ctypes.Structure):
+ _fields_ = [
+ ('rec', ctypes.c_int),
+ ('range', ctypes.c_int),
+ ('num_threads', ctypes.c_int),
+ ]
+
+
+_lib.pixpat_draw_pattern.argtypes = [
+ ctypes.POINTER(_Buffer),
+ ctypes.c_char_p,
+ ctypes.POINTER(_PatternOpts),
+]
+_lib.pixpat_draw_pattern.restype = ctypes.c_int
+
+_lib.pixpat_convert.argtypes = [
+ ctypes.POINTER(_Buffer),
+ ctypes.POINTER(_Buffer),
+ ctypes.POINTER(_ConvertOpts),
+]
+_lib.pixpat_convert.restype = ctypes.c_int
+
+_lib.pixpat_format_supported.argtypes = [ctypes.c_char_p]
+_lib.pixpat_format_supported.restype = ctypes.c_int
+
+_lib.pixpat_format_count.argtypes = []
+_lib.pixpat_format_count.restype = ctypes.c_size_t
+
+_lib.pixpat_format_name.argtypes = [ctypes.c_size_t]
+_lib.pixpat_format_name.restype = ctypes.c_char_p
+
+
+class _Py_buffer(ctypes.Structure):
+ _fields_ = [
+ ('buf', ctypes.c_void_p),
+ ('obj', ctypes.py_object),
+ ('len', ctypes.c_ssize_t),
+ ('itemsize', ctypes.c_ssize_t),
+ ('readonly', ctypes.c_int),
+ ('ndim', ctypes.c_int),
+ ('format', ctypes.c_char_p),
+ ('shape', ctypes.POINTER(ctypes.c_ssize_t)),
+ ('strides', ctypes.POINTER(ctypes.c_ssize_t)),
+ ('suboffsets', ctypes.POINTER(ctypes.c_ssize_t)),
+ ('internal', ctypes.c_void_p),
+ ]
+
+
+_PyObject_GetBuffer = ctypes.pythonapi.PyObject_GetBuffer
+_PyObject_GetBuffer.argtypes = [
+ ctypes.py_object,
+ ctypes.POINTER(_Py_buffer),
+ ctypes.c_int,
+]
+_PyObject_GetBuffer.restype = ctypes.c_int
+
+_PyBuffer_Release = ctypes.pythonapi.PyBuffer_Release
+_PyBuffer_Release.argtypes = [ctypes.POINTER(_Py_buffer)]
+_PyBuffer_Release.restype = None
+
+_PyBUF_SIMPLE = 0
+_PyBUF_WRITABLE = 0x0001
+
+
+class _PinnedBuffers:
+ """Holds Py_buffer views for the lifetime of a pixpat call.
+
+ Releases each view in ``__exit__`` (or when garbage-collected) so
+ the underlying objects' buffer-export count drops back to zero.
+ """
+
+ def __init__(self) -> None:
+ self._views: list[_Py_buffer] = []
+
+ def acquire(self, obj, *, writable: bool) -> int:
+ view = _Py_buffer()
+ flags = _PyBUF_WRITABLE if writable else _PyBUF_SIMPLE
+ # ctypes.pythonapi propagates the set exception (BufferError /
+ # TypeError) automatically on failure, so no rc check is needed.
+ _PyObject_GetBuffer(obj, ctypes.byref(view), flags)
+ self._views.append(view)
+ return view.buf or 0
+
+ def release(self) -> None:
+ while self._views:
+ view = self._views.pop()
+ _PyBuffer_Release(ctypes.byref(view))
+
+ def __enter__(self) -> '_PinnedBuffers':
+ return self
+
+ def __exit__(self, exc_type, exc, tb) -> None:
+ self.release()
+
+ def __del__(self) -> None:
+ self.release()
+
+
+def _fill_buffer(
+ buf: _Buffer,
+ pins: _PinnedBuffers,
+ planes: Sequence,
+ fmt: str,
+ width: int,
+ height: int,
+ strides: Sequence[int],
+ *,
+ writable: bool,
+ role: str = '',
+) -> None:
+ """Populate ``buf`` from a Python plane sequence, pinning each plane.
+
+ ``writable`` selects whether each plane must be writable: True for
+ destination buffers (the C library writes into them), False for
+ source buffers (the C library only reads).
+ """
+ label = f'{role} ' if role else ''
+ if len(planes) > _PIXPAT_MAX_PLANES:
+ raise ValueError(f'too many {label}planes: {len(planes)} (max {_PIXPAT_MAX_PLANES})')
+ if len(strides) != len(planes):
+ raise ValueError(f'{label}strides has {len(strides)} entries, expected {len(planes)}')
+
+ plane_ptrs = (ctypes.c_void_p * _PIXPAT_MAX_PLANES)()
+ stride_arr = (ctypes.c_uint32 * _PIXPAT_MAX_PLANES)()
+ for i, plane in enumerate(planes):
+ try:
+ addr = pins.acquire(plane, writable=writable)
+ except BufferError as e:
+ kind = 'writable' if writable else 'buffer-protocol'
+ raise TypeError(f'{label}plane {i} does not support the {kind} interface: {e}') from e
+ plane_ptrs[i] = addr
+ stride_arr[i] = strides[i]
+
+ buf.format = fmt.encode('ascii')
+ buf.width = width
+ buf.height = height
+ buf.num_planes = len(planes)
+ buf.planes = plane_ptrs
+ buf.strides = stride_arr
+
+
+__all__ = [
+ '_Buffer',
+ '_PatternOpts',
+ '_ConvertOpts',
+ '_PinnedBuffers',
+ '_lib',
+ '_fill_buffer',
+]
diff --git a/subprojects/pixpat/pixpat-python/scripts/build_wheel.sh b/subprojects/pixpat/pixpat-python/scripts/build_wheel.sh
new file mode 100755
index 0000000..1eb2586
--- /dev/null
+++ b/subprojects/pixpat/pixpat-python/scripts/build_wheel.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+# Build a pixpat wheel for the given target architecture.
+#
+# Usage: pixpat-python/scripts/build_wheel.sh <x86_64|aarch64>
+#
+# Meson is invoked from setup.py during the wheel build; this script just
+# selects the target arch and hands off to `python -m build`. The meson
+# build dir lands at pixpat-python/build-<arch>/native/, so cross-compiling
+# both arches in turn keeps each arch's compile incremental.
+#
+# Prerequisites:
+# - meson, ninja in PATH (e.g. `pip install meson ninja`)
+# - For aarch64: gcc-aarch64-linux-gnu, g++-aarch64-linux-gnu
+# (sudo apt install gcc-aarch64-linux-gnu g++-aarch64-linux-gnu)
+# - Python with `build` package: pip install build
+
+set -euo pipefail
+
+ARCH="${1:-}"
+case "$ARCH" in
+ x86_64|aarch64) ;;
+ *)
+ echo "usage: $0 <x86_64|aarch64>" >&2
+ exit 1
+ ;;
+esac
+
+REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+cd "$REPO_ROOT"
+
+# --no-isolation: reuse the host environment so meson's per-arch build dir
+# (pixpat-python/build-<arch>/native/) survives across invocations and stays
+# incremental. Build isolation copies the source to a temp dir, so the meson
+# build dir would be thrown away each run.
+PIXPAT_TARGET_ARCH="$ARCH" python -m build --wheel --no-isolation
+
+echo
+echo "Wheel(s) in dist/:"
+ls -1 dist/*.whl
diff --git a/subprojects/pixpat/pixpat-python/scripts/perf_test.py b/subprojects/pixpat/pixpat-python/scripts/perf_test.py
new file mode 100755
index 0000000..100c16e
--- /dev/null
+++ b/subprojects/pixpat/pixpat-python/scripts/perf_test.py
@@ -0,0 +1,434 @@
+#!/usr/bin/env python3
+"""Micro-benchmark pixpat across draw_pattern and convert call paths.
+
+Each row times one ``<source> -> <destination>`` operation:
+
+* a pattern draw, where ``source`` is a pattern name (``smpte``,
+ ``kmstest`` etc.) and the row times ``pixpat.draw_pattern``;
+* a format conversion, where both sides are format names and the row
+ times ``pixpat.convert``.
+
+Buffer allocation is driven by ``pixutils.formats.PixelFormats`` —
+``framesize`` / ``planesize`` / ``stride`` give us per-plane geometry
+without per-format hand-coding.
+
+Filtering is uniform across both kinds because the case name always
+has the form ``"<lhs> -> <rhs>"``: ``--ionly kmstest`` keeps that
+pattern's cases, ``--ionly NV12`` keeps NV12-source convert cases,
+``--oonly BGR888`` keeps any case (pattern or convert) writing
+BGR888, and so on.
+
+Usage:
+ python perf_test.py [--width 1920] [--height 1080] [--iters 200]
+ [--warmup 10] [--pp-threads 1]
+ [--rec BT709] [--range LIMITED]
+ [--only-pattern | --only-convert]
+ [--only STR] [--ionly STR] [--oonly STR]
+ [--cases STR] [--tsv]
+"""
+
+from __future__ import annotations
+
+import argparse
+import gc
+import math
+import sys
+import time
+from dataclasses import dataclass
+from typing import Callable, Optional
+
+import numpy as np
+
+import pixpat
+from pixutils.formats import PixelFormat, PixelFormats
+
+
+PATTERNS = ['kmstest', 'smpte', 'plain']
+
+PATTERN_SINK_FMTS = [
+ 'XRGB8888',
+ 'BGR888',
+ 'RGB565',
+ 'RGBA1010102',
+ 'ABGR16161616',
+ 'Y8',
+ 'YUYV',
+ 'UYVY',
+ 'NV12',
+ 'NV16',
+ 'YUV420',
+ 'SRGGB8',
+ 'SRGGB10P',
+]
+
+CONVERT_PAIRS: list[tuple[str, str]] = [
+ # RGB shuffles
+ ('RGB888', 'BGR888'),
+ ('BGR888', 'BGRA8888'),
+ ('BGRA8888', 'BGR888'),
+ # Grayscale
+ ('BGR888', 'Y8'),
+ ('Y8', 'BGR888'),
+ # Packed YUV decode
+ ('YUYV', 'BGR888'),
+ ('UYVY', 'BGR888'),
+ # Semiplanar YUV decode
+ ('NV12', 'BGR888'),
+ ('NV21', 'BGR888'),
+ # Planar YUV decode
+ ('YUV420', 'BGR888'),
+ ('YVU420', 'BGR888'),
+ # Planar YUV encode
+ ('BGR888', 'YUV420'),
+ ('BGR888', 'YVU420'),
+ # NV12 encode/decode against XRGB8888
+ ('XRGB8888', 'NV12'),
+ ('BGR888', 'NV12'),
+ ('NV12', 'XRGB8888'),
+ # Cold path: neither side is BGR888
+ ('XRGB8888', 'RGBA8888'),
+ ('RGBA8888', 'XRGB8888'),
+ ('NV12', 'YUV420'),
+ ('YUV420', 'NV12'),
+ ('NV12', 'YUYV'),
+ ('NV12', 'RGB565'),
+ ('Y8', 'NV12'),
+ # Wider coverage
+ ('BGR888', 'YUYV'),
+ ('BGR888', 'NV16'),
+ ('NV16', 'BGR888'),
+ ('BGR888', 'YUV422'),
+ ('YUV422', 'BGR888'),
+ ('BGR888', 'YUV444'),
+ ('YUV444', 'BGR888'),
+ # 16-bit packed RGB
+ ('BGR888', 'RGB565'),
+ ('RGB565', 'BGR888'),
+ # 10-bit packed RGB
+ ('BGR888', 'RGBA1010102'),
+ ('RGBA1010102', 'BGR888'),
+ # 64-bit packed RGB (pixpat's normalized wide form)
+ ('BGR888', 'ABGR16161616'),
+ ('ABGR16161616', 'BGR888'),
+ # Multi-pixel-per-word semiplanar YUV
+ ('BGR888', 'P030'),
+ ('P030', 'BGR888'),
+ ('BGR888', 'P230'),
+ # Multi-pixel-per-word planar YUV
+ ('BGR888', 'T430'),
+ ('T430', 'BGR888'),
+ # Multi-pixel-per-word grayscale
+ ('BGR888', 'XYYY2101010'),
+ # Single-pixel-per-word YUV
+ ('BGR888', 'XVUY2101010'),
+ ('BGR888', 'AVUY16161616'),
+ # Bayer
+ ('BGR888', 'SRGGB8'),
+ ('SRGGB8', 'BGR888'),
+ ('BGR888', 'SRGGB10P'),
+ ('SRGGB10P', 'BGR888'),
+ ('BGR888', 'SRGGB12P'),
+ ('SRGGB12P', 'BGR888'),
+ # Single-channel RGB
+ ('BGR888', 'R8'),
+ ('R8', 'BGR888'),
+ # MIPI CSI-2 packed grayscale
+ ('BGR888', 'Y10P'),
+ ('Y10P', 'BGR888'),
+ # 4:2:2 packed YUV in 64-bit words
+ ('BGR888', 'Y210'),
+ ('Y210', 'BGR888'),
+]
+
+
+_PF_BY_NAME = {v.name: v for v in PixelFormats.__dict__.values() if isinstance(v, PixelFormat)}
+
+
+def _required_align(pf: PixelFormat) -> tuple[int, int]:
+ # pf.pixel_align doesn't always capture the per-plane pixels_per_block /
+ # vsub requirements (e.g. T430 lists (1,1) but each plane is 3 px wide),
+ # so combine them to get the effective alignment we need to skip cleanly.
+ w_align = pf.pixel_align[0]
+ h_align = pf.pixel_align[1]
+ for p in pf.planes:
+ w_align = math.lcm(w_align, p.pixels_per_block * p.hsub)
+ h_align = math.lcm(h_align, p.vsub)
+ return w_align, h_align
+
+
+def _alloc_buffer(fmt_name: str, w: int, h: int) -> Optional[tuple[pixpat.Buffer, np.ndarray]]:
+ """Build a pixpat.Buffer + its 1-D backing array, or None when the
+ resolution doesn't fit the format's alignment."""
+ pf = _PF_BY_NAME[fmt_name]
+ w_align, h_align = _required_align(pf)
+ if w % w_align or h % h_align:
+ return None
+
+ backing = np.zeros(pf.framesize(w, h), dtype=np.uint8)
+ planes: list[np.ndarray] = []
+ strides: list[int] = []
+ offset = 0
+ for i in range(len(pf.planes)):
+ s = pf.stride(w, i)
+ psize = pf.planesize(s, h, i)
+ view = backing[offset : offset + psize].reshape(psize // s, s)
+ planes.append(view)
+ strides.append(s)
+ offset += psize
+ return pixpat.Buffer(planes, fmt_name, w, h, strides), backing
+
+
+@dataclass
+class Case:
+ name: str
+ kind: str # 'pattern' or 'convert'
+ dst: pixpat.Buffer
+ src: Optional[pixpat.Buffer] = None
+ src_arr: Optional[np.ndarray] = None # 1-D backing for random fill
+
+
+def _build_cases(w: int, h: int, kinds: set[str]) -> list[Case]:
+ cases: list[Case] = []
+
+ if 'pattern' in kinds:
+ for fmt in PATTERN_SINK_FMTS:
+ alloc = _alloc_buffer(fmt, w, h)
+ if alloc is None:
+ continue
+ buf, _ = alloc
+ for pat in PATTERNS:
+ cases.append(Case(name=f'{pat} -> {fmt}', kind='pattern', dst=buf))
+
+ if 'convert' in kinds:
+ for src_fmt, dst_fmt in CONVERT_PAIRS:
+ src_alloc = _alloc_buffer(src_fmt, w, h)
+ dst_alloc = _alloc_buffer(dst_fmt, w, h)
+ if src_alloc is None or dst_alloc is None:
+ continue
+ src_buf, src_backing = src_alloc
+ dst_buf, _ = dst_alloc
+ cases.append(
+ Case(
+ name=f'{src_fmt} -> {dst_fmt}',
+ kind='convert',
+ dst=dst_buf,
+ src=src_buf,
+ src_arr=src_backing,
+ )
+ )
+
+ return cases
+
+
+def _parse_filter(s: Optional[str]) -> Optional[set[str]]:
+ if s is None:
+ return None
+ return {x.strip().upper() for x in s.split(',') if x.strip()}
+
+
+def _norm_case(name: str) -> str:
+ return ' '.join(name.upper().split())
+
+
+def _filter_cases(
+ cases: list[Case],
+ only: Optional[str],
+ ionly: Optional[str],
+ oonly: Optional[str],
+ cases_filter: Optional[str],
+) -> list[Case]:
+ only_set = _parse_filter(only)
+ ionly_set = _parse_filter(ionly)
+ oonly_set = _parse_filter(oonly)
+ cases_set: Optional[set[str]] = None
+ if cases_filter is not None:
+ cases_set = {_norm_case(x) for x in cases_filter.split(',') if x.strip()}
+ if only_set is None and ionly_set is None and oonly_set is None and cases_set is None:
+ return cases
+
+ out: list[Case] = []
+ for c in cases:
+ lhs, _, rhs = c.name.partition(' -> ')
+ lhs, rhs = lhs.upper(), rhs.upper()
+ if cases_set is not None and _norm_case(c.name) not in cases_set:
+ continue
+ if only_set is not None and lhs not in only_set and rhs not in only_set:
+ continue
+ if ionly_set is not None and lhs not in ionly_set:
+ continue
+ if oonly_set is not None and rhs not in oonly_set:
+ continue
+ out.append(c)
+ return out
+
+
+def _bind(
+ case: Case,
+ num_threads: int,
+ rec: 'pixpat.Rec',
+ color_range: 'pixpat.Range',
+) -> Callable[[], object]:
+ if case.kind == 'pattern':
+ fn = pixpat.draw_pattern
+ dst = case.dst
+ pat = case.name.partition(' -> ')[0]
+ # `plain` needs an explicit color; the rest ignore params.
+ params = {'color': 'ff0000'} if pat == 'plain' else None
+ return lambda: fn(
+ dst,
+ pat,
+ rec=rec,
+ color_range=color_range,
+ num_threads=num_threads,
+ params=params,
+ )
+ fn = pixpat.convert
+ assert case.src is not None
+ src, dst = case.src, case.dst
+ return lambda: fn(dst, src, rec=rec, color_range=color_range, num_threads=num_threads)
+
+
+def _time_n(fn: Callable[[], object], iters: int) -> float:
+ """Return min seconds over ``iters`` calls."""
+ best = float('inf')
+ for _ in range(iters):
+ t0 = time.perf_counter_ns()
+ fn()
+ dt = time.perf_counter_ns() - t0
+ if dt < best:
+ best = dt
+ return best * 1e-9
+
+
+def main() -> int:
+ p = argparse.ArgumentParser(
+ description='Micro-benchmark pixpat across draw_pattern and convert.'
+ )
+ p.add_argument('--width', type=int, default=1920)
+ p.add_argument('--height', type=int, default=1080)
+ p.add_argument('--iters', type=int, default=200)
+ p.add_argument('--warmup', type=int, default=10)
+ p.add_argument(
+ '--pp-threads',
+ type=int,
+ default=1,
+ help='pixpat thread count; 0 = sensible default',
+ )
+ p.add_argument(
+ '--rec',
+ default='BT709',
+ choices=[r.name for r in pixpat.Rec],
+ help='YCbCr matrix for YUV cases (default BT709)',
+ )
+ p.add_argument(
+ '--range',
+ dest='color_range',
+ default='LIMITED',
+ choices=[r.name for r in pixpat.Range],
+ help='Quantization range for YUV cases (default LIMITED)',
+ )
+ kind_group = p.add_mutually_exclusive_group()
+ kind_group.add_argument(
+ '--only-pattern',
+ action='store_true',
+ help='restrict to pattern-draw cases',
+ )
+ kind_group.add_argument(
+ '--only-convert',
+ action='store_true',
+ help='restrict to convert cases',
+ )
+ p.add_argument(
+ '--only',
+ default=None,
+ help='comma-separated names; keep cases whose lhs OR rhs of " -> " matches',
+ )
+ p.add_argument(
+ '--ionly',
+ default=None,
+ help='comma-separated names; keep cases whose lhs (pattern or src fmt) matches',
+ )
+ p.add_argument(
+ '--oonly',
+ default=None,
+ help='comma-separated names; keep cases whose rhs (dst fmt) matches',
+ )
+ p.add_argument(
+ '--cases',
+ default=None,
+ help='comma-separated full case names (e.g. "smpte -> BGR888,RGB888 -> BGR888")',
+ )
+ p.add_argument(
+ '--tsv',
+ action='store_true',
+ help='emit tab-separated rows on stdout; meta and warnings go to stderr',
+ )
+ args = p.parse_args()
+
+ w, h = args.width, args.height
+ mp = (w * h) / 1e6
+
+ rec = pixpat.Rec[args.rec]
+ color_range = pixpat.Range[args.color_range]
+
+ kinds: set[str] = {'pattern', 'convert'}
+ if args.only_pattern:
+ kinds = {'pattern'}
+ elif args.only_convert:
+ kinds = {'convert'}
+
+ cases = _build_cases(w, h, kinds)
+ cases = _filter_cases(cases, args.only, args.ionly, args.oonly, args.cases)
+ if not cases:
+ print('no cases matched filter')
+ return 1
+
+ info: Callable[[str], None] = (lambda m: print(m, file=sys.stderr)) if args.tsv else print
+
+ info(
+ f'Resolution: {w}x{h} ({mp:.2f} MP/frame), '
+ f'iters={args.iters}, warmup={args.warmup}, '
+ f'pp threads={args.pp_threads}, '
+ f'rec={rec.name}, range={color_range.name}'
+ )
+
+ name_w = max((len(c.name) for c in cases), default=20) + 2
+
+ if args.tsv:
+ print('\t'.join(['case', 'pp_mps', 'pp_fps']))
+ else:
+ print()
+ header = f'{"case":<{name_w}} {"pp MP/s":>9} {"pp fps":>8}'
+ print(header)
+ print('-' * len(header))
+
+ rng = np.random.default_rng(0)
+
+ for case in cases:
+ if case.kind == 'convert' and case.src_arr is not None:
+ case.src_arr[...] = rng.integers(0, 256, size=case.src_arr.shape, dtype=np.uint8)
+
+ run = _bind(case, args.pp_threads, rec=rec, color_range=color_range)
+ gc.disable()
+ try:
+ for _ in range(args.warmup):
+ run()
+ t = _time_n(run, args.iters)
+ except (pixpat.PixpatError, ValueError, TypeError) as e:
+ info(f'{case.name:<{name_w}} skipped: {type(e).__name__}: {e}')
+ continue
+ finally:
+ gc.enable()
+
+ fps = 1.0 / t
+ mps = fps * mp
+ if args.tsv:
+ print(f'{case.name}\t{mps:.0f}\t{fps:.0f}')
+ else:
+ print(f'{case.name:<{name_w}} {mps:>9.0f} {fps:>8.0f}')
+
+ return 0
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
diff --git a/subprojects/pixpat/pixpat-python/tests/test_basic.py b/subprojects/pixpat/pixpat-python/tests/test_basic.py
new file mode 100644
index 0000000..2ed8bb7
--- /dev/null
+++ b/subprojects/pixpat/pixpat-python/tests/test_basic.py
@@ -0,0 +1,539 @@
+"""Smoke tests for the pixpat Python bindings."""
+
+import pytest
+
+import pixpat
+
+
+def test_supported_formats_nonempty():
+ formats = pixpat.supported_formats()
+ assert isinstance(formats, list)
+ assert len(formats) > 0
+ assert all(isinstance(f, str) for f in formats)
+ assert 'XRGB8888' in formats
+ assert 'NV12' in formats
+
+
+def test_is_supported():
+ assert pixpat.is_supported('XRGB8888')
+ assert pixpat.is_supported('NV12')
+ assert not pixpat.is_supported('NOT_A_REAL_FORMAT')
+
+
+def test_draw_pattern_xrgb8888():
+ w, h = 64, 32
+ stride = w * 4
+ data = bytearray(stride * h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'smpte')
+ assert any(b != 0 for b in data)
+
+
+def test_draw_pattern_unknown_pattern_raises():
+ data = bytearray(64 * 32 * 4)
+ buf = pixpat.Buffer([data], 'XRGB8888', 64, 32, [64 * 4])
+ with pytest.raises(ValueError):
+ pixpat.draw_pattern(buf, 'bogus')
+
+
+def test_draw_pattern_unknown_format_raises():
+ data = bytearray(64 * 32 * 4)
+ buf = pixpat.Buffer([data], 'NOT_A_REAL_FORMAT', 64, 32, [64 * 4])
+ with pytest.raises(pixpat.PixpatError):
+ pixpat.draw_pattern(buf)
+
+
+def test_draw_pattern_readonly_buffer_raises():
+ data = bytes(64 * 32 * 4)
+ buf = pixpat.Buffer([data], 'XRGB8888', 64, 32, [64 * 4])
+ with pytest.raises((TypeError, BufferError)):
+ pixpat.draw_pattern(buf)
+
+
+def _alloc_xrgb(w, h):
+ stride = w * 4
+ return bytearray(stride * h), stride
+
+
+def _first_pixel_bgr(data):
+ # BGR888-style byte order: pixpat XRGB8888 stores B at byte 0,
+ # G at byte 1, R at byte 2, X at byte 3. See
+ # README "Format names and byte order".
+ return data[0], data[1], data[2]
+
+
+def test_draw_pattern_plain_red_str_params():
+ w, h = 32, 16
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'plain', params='color=ff0000')
+ b, g, r = _first_pixel_bgr(data)
+ assert (b, g, r) == (0, 0, 0xFF)
+
+
+def test_draw_pattern_plain_red_dict_params():
+ w, h = 32, 16
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'plain', params={'color': 'ff0000'})
+ b, g, r = _first_pixel_bgr(data)
+ assert (b, g, r) == (0, 0, 0xFF)
+
+
+def test_draw_pattern_plain_alpha_passes_through_to_argb():
+ w, h = 16, 8
+ stride = w * 4
+ data = bytearray(stride * h)
+ buf = pixpat.Buffer([data], 'ARGB8888', w, h, [stride])
+ # 8-char form is alpha-first: AARRGGBB.
+ pixpat.draw_pattern(buf, 'plain', params={'color': '80112233'})
+ # ARGB8888 byte order (LSB-first): B, G, R, A
+ assert (data[0], data[1], data[2], data[3]) == (0x33, 0x22, 0x11, 0x80)
+
+
+def test_draw_pattern_plain_accepts_0x_prefix():
+ w, h = 16, 8
+ stride = w * 4
+ data = bytearray(stride * h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'plain', params={'color': '0xff0000'})
+ assert (data[0], data[1], data[2]) == (0, 0, 0xFF)
+
+
+def test_draw_pattern_plain_16bpc_rgb():
+ """16-bpc form: 12 hex chars, RRRRGGGGBBBB. ABGR16161616 names
+ A-B-G-R MSB-first in a 64-bit word; stored little-endian, the
+ bytes are R lo, R hi, G lo, G hi, B lo, B hi, A lo, A hi."""
+ w, h = 4, 2
+ stride = w * 8
+ data = bytearray(stride * h)
+ buf = pixpat.Buffer([data], 'ABGR16161616', w, h, [stride])
+ pixpat.draw_pattern(buf, 'plain', params={'color': '0xfedc00000000'})
+ # R=0xFEDC, G=0x0000, B=0x0000, A=0xFFFF (default opaque).
+ pix = bytes(data[:8])
+ assert pix == b'\xdc\xfe\x00\x00\x00\x00\xff\xff'
+
+
+def test_draw_pattern_plain_16bpc_argb():
+ w, h = 4, 2
+ stride = w * 8
+ data = bytearray(stride * h)
+ buf = pixpat.Buffer([data], 'ABGR16161616', w, h, [stride])
+ # 16-char form is alpha-first: AAAARRRRGGGGBBBB.
+ pixpat.draw_pattern(buf, 'plain', params={'color': '0x80007f000000ffff'})
+ pix = bytes(data[:8])
+ # R=0x7F00, G=0x0000, B=0xFFFF, A=0x8000.
+ assert pix == b'\x00\x7f\x00\x00\xff\xff\x00\x80'
+
+
+def test_draw_pattern_plain_missing_color_raises():
+ w, h = 32, 16
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ with pytest.raises(pixpat.PixpatError):
+ pixpat.draw_pattern(buf, 'plain')
+ with pytest.raises(pixpat.PixpatError):
+ pixpat.draw_pattern(buf, 'plain', params='')
+
+
+def test_draw_pattern_plain_malformed_color_raises():
+ w, h = 32, 16
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ for bad in ('color=zzzzzz', 'color=abc', 'color=', 'color=1234567'):
+ with pytest.raises(pixpat.PixpatError):
+ pixpat.draw_pattern(buf, 'plain', params=bad)
+
+
+def test_draw_pattern_malformed_params_string_raises():
+ """Top-level params parse failure (not pattern-specific)."""
+ w, h = 32, 16
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ for bad in ('foo,bar', '=ff0000', ',color=ff0000'):
+ with pytest.raises(pixpat.PixpatError):
+ pixpat.draw_pattern(buf, 'plain', params=bad)
+
+
+def test_draw_pattern_dict_params_with_separators_rejected_by_python():
+ w, h = 32, 16
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ with pytest.raises(ValueError):
+ pixpat.draw_pattern(buf, 'plain', params={'color': 'ff,00,00'})
+ with pytest.raises(ValueError):
+ pixpat.draw_pattern(buf, 'plain', params={'col=or': 'ff0000'})
+
+
+def test_draw_pattern_unknown_params_keys_ignored():
+ """Patterns silently ignore keys they don't read; unknown keys
+ alongside the recognised one still let the call succeed."""
+ w, h = 32, 16
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'plain', params={'color': 'ff0000', 'unknown_key': '42'})
+ b, g, r = _first_pixel_bgr(data)
+ assert (b, g, r) == (0, 0, 0xFF)
+
+
+def test_draw_pattern_params_ignored_by_kmstest():
+ """Patterns that don't read params accept arbitrary params strings."""
+ w, h = 32, 16
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'kmstest', params={'whatever': 'foo'})
+ assert any(b != 0 for b in data)
+
+
+def _xrgb_pixel_at(data, stride, x, y):
+ off = y * stride + x * 4
+ return data[off], data[off + 1], data[off + 2] # B, G, R
+
+
+@pytest.mark.parametrize(
+ 'pattern,extra',
+ [
+ ('hramp', {}),
+ ('vramp', {}),
+ ('dramp', {}),
+ ('zoneplate', {}),
+ ('checker', {}),
+ ('checker', {'params': 'cell=1'}),
+ ('checker', {'params': {'cell': '32'}}),
+ ],
+)
+def test_draw_pattern_phase3_smoke(pattern, extra):
+ w, h = 64, 32
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, pattern, **extra)
+ assert any(b != 0 for b in data)
+
+
+def test_draw_pattern_checker_default_cell_first_pixel_white():
+ w, h = 64, 32
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'checker')
+ # (0,0) is in the first cell -> white.
+ assert _xrgb_pixel_at(data, stride, 0, 0) == (0xFF, 0xFF, 0xFF)
+ # Default cell=8: (8,0) is the next cell horizontally -> black.
+ assert _xrgb_pixel_at(data, stride, 8, 0) == (0, 0, 0)
+
+
+def test_draw_pattern_checker_cell1_alternates():
+ w, h = 16, 8
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'checker', params={'cell': '1'})
+ p00 = _xrgb_pixel_at(data, stride, 0, 0)
+ p10 = _xrgb_pixel_at(data, stride, 1, 0)
+ p01 = _xrgb_pixel_at(data, stride, 0, 1)
+ p11 = _xrgb_pixel_at(data, stride, 1, 1)
+ assert p00 != p10
+ assert p00 != p01
+ assert p00 == p11 # diagonal repeats
+
+
+@pytest.mark.parametrize('cell', ['0', '-1', 'oops', '1.5'])
+def test_draw_pattern_checker_invalid_cell_raises(cell):
+ w, h = 16, 8
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ with pytest.raises(pixpat.PixpatError):
+ pixpat.draw_pattern(buf, 'checker', params={'cell': cell})
+
+
+def test_draw_pattern_hramp_stripes():
+ """hramp: 4 horizontal stripes (R, G, B, gray), each ramping along x.
+ The right edge (x=W-1) hits 0xFF in the active channel(s)."""
+ w, h = 32, 16 # h=16 → stripes at rows [0,3], [4,7], [8,11], [12,15]
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'hramp')
+ assert _xrgb_pixel_at(data, stride, w - 1, 0) == (0, 0, 0xFF) # R
+ assert _xrgb_pixel_at(data, stride, w - 1, 4) == (0, 0xFF, 0) # G
+ assert _xrgb_pixel_at(data, stride, w - 1, 8) == (0xFF, 0, 0) # B
+ assert _xrgb_pixel_at(data, stride, w - 1, h - 1) == (0xFF, 0xFF, 0xFF) # gray
+ # Left edge is the 0 end of every ramp.
+ for y in (0, 4, 8, 12):
+ assert _xrgb_pixel_at(data, stride, 0, y) == (0, 0, 0)
+
+
+def test_draw_pattern_vramp_columns():
+ """vramp: 4 vertical columns (R, G, B, gray), each ramping along y."""
+ w, h = 16, 32
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'vramp')
+ # Bottom row hits max in the active channel(s).
+ assert _xrgb_pixel_at(data, stride, 0, h - 1) == (0, 0, 0xFF) # R
+ assert _xrgb_pixel_at(data, stride, 4, h - 1) == (0, 0xFF, 0) # G
+ assert _xrgb_pixel_at(data, stride, 8, h - 1) == (0xFF, 0, 0) # B
+ assert _xrgb_pixel_at(data, stride, 12, h - 1) == (0xFF, 0xFF, 0xFF) # gray
+ # Top row is the 0 end.
+ for x in (0, 4, 8, 12):
+ assert _xrgb_pixel_at(data, stride, x, 0) == (0, 0, 0)
+ # Within a column, luma is monotonic.
+ prev = -1
+ for y in range(h):
+ b, g, r = _xrgb_pixel_at(data, stride, 12, y) # gray column
+ assert b == g == r
+ assert b >= prev
+ prev = b
+
+
+def test_draw_pattern_zoneplate_center_white():
+ """Even sizes don't sample exactly at the center; pick odd dimensions
+ so (W//2, H//2) is the center pixel where cos(0)=1 → white."""
+ w, h = 65, 33
+ data, stride = _alloc_xrgb(w, h)
+ buf = pixpat.Buffer([data], 'XRGB8888', w, h, [stride])
+ pixpat.draw_pattern(buf, 'zoneplate')
+ b, g, r = _xrgb_pixel_at(data, stride, w // 2, h // 2)
+ # Center pixel: phase=0, gray ≈ 1.0.
+ assert b == g == r and b >= 0xFE
+
+
+def test_convert_roundtrip_xrgb8888():
+ w, h = 64, 32
+ src = bytearray(w * h * 4)
+ mid = bytearray(w * h * 8)
+ dst = bytearray(w * h * 4)
+ pixpat.draw_pattern(pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]), 'smpte')
+ pixpat.convert(
+ pixpat.Buffer([mid], 'ABGR16161616', w, h, [w * 8]),
+ pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]),
+ )
+ pixpat.convert(
+ pixpat.Buffer([dst], 'XRGB8888', w, h, [w * 4]),
+ pixpat.Buffer([mid], 'ABGR16161616', w, h, [w * 8]),
+ )
+ assert src == dst
+
+
+def test_convert_readonly_bytes_src():
+ """Source planes may be read-only (bytes); the C library only reads src."""
+ w, h = 64, 32
+ src_writable = bytearray(w * h * 4)
+ pixpat.draw_pattern(pixpat.Buffer([src_writable], 'XRGB8888', w, h, [w * 4]), 'smpte')
+ src_readonly = bytes(src_writable)
+ dst = bytearray(w * h * 8)
+ pixpat.convert(
+ pixpat.Buffer([dst], 'ABGR16161616', w, h, [w * 8]),
+ pixpat.Buffer([src_readonly], 'XRGB8888', w, h, [w * 4]),
+ )
+ assert any(b != 0 for b in dst)
+
+
+def test_convert_dimension_mismatch_raises():
+ w, h = 64, 32
+ src = bytearray(w * h * 4)
+ dst = bytearray((w * 2) * h * 8)
+ with pytest.raises(ValueError, match='dimensions'):
+ pixpat.convert(
+ pixpat.Buffer([dst], 'ABGR16161616', w * 2, h, [w * 2 * 8]),
+ pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]),
+ )
+
+
+def test_draw_pattern_nv12_semi_planar():
+ """Semi-planar NV12: separate Y and interleaved-UV planes."""
+ w, h = 64, 32
+ y = bytearray(w * h)
+ uv = bytearray(w * (h // 2))
+ pixpat.draw_pattern(pixpat.Buffer([y, uv], 'NV12', w, h, [w, w]), 'smpte')
+ assert any(b != 0 for b in y)
+ assert any(b != 0 for b in uv)
+
+
+def test_draw_pattern_yuv420_three_planes():
+ """Fully planar YUV420: Y, U and V all at separate plane pointers."""
+ w, h = 64, 32
+ yp = bytearray(w * h)
+ up = bytearray((w // 2) * (h // 2))
+ vp = bytearray((w // 2) * (h // 2))
+ pixpat.draw_pattern(
+ pixpat.Buffer([yp, up, vp], 'YUV420', w, h, [w, w // 2, w // 2]),
+ 'smpte',
+ )
+ assert any(b != 0 for b in yp)
+ assert any(b != 0 for b in up)
+ assert any(b != 0 for b in vp)
+
+
+def test_convert_xrgb_to_nv12_writes_both_planes():
+ """Convert into a multi-plane destination must populate every plane."""
+ w, h = 64, 32
+ src = bytearray(w * h * 4)
+ pixpat.draw_pattern(pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]), 'smpte')
+ y = bytearray(w * h)
+ uv = bytearray(w * (h // 2))
+ pixpat.convert(
+ pixpat.Buffer([y, uv], 'NV12', w, h, [w, w]),
+ pixpat.Buffer([src], 'XRGB8888', w, h, [w * 4]),
+ )
+ assert any(b != 0 for b in y)
+ assert any(b != 0 for b in uv)
+
+
+def test_convert_yuv420_src_three_planes():
+ """Planar YUV is a supported convert source — the C library reads
+ all three plane pointers, so make sure the binding wires them up."""
+ w, h = 64, 32
+ yp = bytearray(w * h)
+ up = bytearray((w // 2) * (h // 2))
+ vp = bytearray((w // 2) * (h // 2))
+ pixpat.draw_pattern(
+ pixpat.Buffer([yp, up, vp], 'YUV420', w, h, [w, w // 2, w // 2]),
+ 'smpte',
+ )
+ dst = bytearray(w * h * 4)
+ pixpat.convert(
+ pixpat.Buffer([dst], 'XRGB8888', w, h, [w * 4]),
+ pixpat.Buffer([yp, up, vp], 'YUV420', w, h, [w, w // 2, w // 2]),
+ )
+ assert any(b != 0 for b in dst)
+
+
+def test_convert_bayer_src():
+ """Bayer formats are supported as a convert source (decoded by
+ nearest-neighbor demosaic per the public header docstring)."""
+ w, h = 64, 32
+ src = bytearray(w * h)
+ pixpat.draw_pattern(pixpat.Buffer([src], 'SRGGB8', w, h, [w]), 'smpte')
+ dst = bytearray(w * h * 4)
+ pixpat.convert(
+ pixpat.Buffer([dst], 'XRGB8888', w, h, [w * 4]),
+ pixpat.Buffer([src], 'SRGGB8', w, h, [w]),
+ )
+ assert any(b != 0 for b in dst)
+
+
+def test_convert_roundtrip_yuyv():
+ """YUYV (packed 4:2:2) — chroma is averaged on write and replicated on
+ read, so the second leg must not change the buffer."""
+ w, h = 64, 32
+ src = bytearray(w * h * 2)
+ mid = bytearray(w * h * 8)
+ dst = bytearray(w * h * 2)
+ pixpat.draw_pattern(pixpat.Buffer([src], 'YUYV', w, h, [w * 2]), 'smpte')
+ pixpat.convert(
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ pixpat.Buffer([src], 'YUYV', w, h, [w * 2]),
+ )
+ pixpat.convert(
+ pixpat.Buffer([dst], 'YUYV', w, h, [w * 2]),
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ )
+ assert src == dst
+
+
+def test_convert_roundtrip_y8():
+ """Y8 grayscale — chroma is discarded on write and synthesized on read,
+ Y values themselves should roundtrip exactly."""
+ w, h = 64, 32
+ src = bytearray(w * h)
+ mid = bytearray(w * h * 8)
+ dst = bytearray(w * h)
+ pixpat.draw_pattern(pixpat.Buffer([src], 'Y8', w, h, [w]), 'smpte')
+ pixpat.convert(
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ pixpat.Buffer([src], 'Y8', w, h, [w]),
+ )
+ pixpat.convert(
+ pixpat.Buffer([dst], 'Y8', w, h, [w]),
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ )
+ assert src == dst
+
+
+def test_convert_roundtrip_nv12():
+ """NV12 semi-planar 4:2:0 — chroma averaged into a 2x2 block on write,
+ replicated on read; second leg is a no-op."""
+ w, h = 64, 32
+ y_src = bytearray(w * h)
+ uv_src = bytearray(w * (h // 2))
+ mid = bytearray(w * h * 8)
+ y_dst = bytearray(w * h)
+ uv_dst = bytearray(w * (h // 2))
+ pixpat.draw_pattern(pixpat.Buffer([y_src, uv_src], 'NV12', w, h, [w, w]), 'smpte')
+ pixpat.convert(
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ pixpat.Buffer([y_src, uv_src], 'NV12', w, h, [w, w]),
+ )
+ pixpat.convert(
+ pixpat.Buffer([y_dst, uv_dst], 'NV12', w, h, [w, w]),
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ )
+ assert y_src == y_dst
+ assert uv_src == uv_dst
+
+
+def test_convert_roundtrip_xv15():
+ """P030 semi-planar 4:2:0, 10 bits packed 3-per-32-bit. Width and height
+ chosen so the Y plane fits 3 samples per word and chroma is on a 2x2 grid."""
+ w, h = 96, 32 # multiples of 3 (Y group) and 2 (v_sub)
+ y_words = (w // 3) * h
+ uv_words = (w // 3 // 2) * (h // 2)
+ y_src = bytearray(y_words * 4)
+ uv_src = bytearray(uv_words * 8)
+ mid = bytearray(w * h * 8)
+ y_dst = bytearray(y_words * 4)
+ uv_dst = bytearray(uv_words * 8)
+ pixpat.draw_pattern(
+ pixpat.Buffer([y_src, uv_src], 'P030', w, h, [(w // 3) * 4, (w // 3 // 2) * 8]),
+ 'smpte',
+ )
+ pixpat.convert(
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ pixpat.Buffer([y_src, uv_src], 'P030', w, h, [(w // 3) * 4, (w // 3 // 2) * 8]),
+ )
+ pixpat.convert(
+ pixpat.Buffer([y_dst, uv_dst], 'P030', w, h, [(w // 3) * 4, (w // 3 // 2) * 8]),
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ )
+ assert y_src == y_dst
+ assert uv_src == uv_dst
+
+
+def test_convert_roundtrip_x403():
+ """T430 fully-planar 4:4:4 with 3-samples-per-32-bit packing on each
+ plane; no chroma subsampling so first leg is already lossless."""
+ w, h = 96, 32 # width must be a multiple of 3
+ plane_words = (w // 3) * h
+ y_src = bytearray(plane_words * 4)
+ cb_src = bytearray(plane_words * 4)
+ cr_src = bytearray(plane_words * 4)
+ mid = bytearray(w * h * 8)
+ y_dst = bytearray(plane_words * 4)
+ cb_dst = bytearray(plane_words * 4)
+ cr_dst = bytearray(plane_words * 4)
+ strides = [(w // 3) * 4] * 3
+ pixpat.draw_pattern(
+ pixpat.Buffer([y_src, cb_src, cr_src], 'T430', w, h, strides),
+ 'smpte',
+ )
+ pixpat.convert(
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ pixpat.Buffer([y_src, cb_src, cr_src], 'T430', w, h, strides),
+ )
+ pixpat.convert(
+ pixpat.Buffer([y_dst, cb_dst, cr_dst], 'T430', w, h, strides),
+ pixpat.Buffer([mid], 'AVUY16161616', w, h, [w * 8]),
+ )
+ assert y_src == y_dst
+ assert cb_src == cb_dst
+ assert cr_src == cr_dst
+
+
+def test_strides_length_mismatch_multi_plane_raises():
+ """Strides count must match planes count — caught in Python before C."""
+ w, h = 64, 32
+ y = bytearray(w * h)
+ uv = bytearray(w * (h // 2))
+ with pytest.raises(ValueError, match='strides'):
+ pixpat.draw_pattern(
+ pixpat.Buffer([y, uv], 'NV12', w, h, [w]), # 1 stride, 2 planes
+ 'smpte',
+ )
diff --git a/subprojects/pixpat/pixpat-python/tests/test_numpy.py b/subprojects/pixpat/pixpat-python/tests/test_numpy.py
new file mode 100644
index 0000000..6dbd7bb
--- /dev/null
+++ b/subprojects/pixpat/pixpat-python/tests/test_numpy.py
@@ -0,0 +1,110 @@
+"""Empirical check that pixpat works correctly with numpy.
+
+pixpat does not depend on numpy. This file proves that ndarrays work
+as ``Buffer`` planes through the buffer protocol, in the shapes a
+caller would typically use. Skipped when numpy is missing.
+"""
+
+import pixpat
+import pytest
+
+np = pytest.importorskip('numpy')
+
+
+def test_xrgb8888_into_uint8_3d_ndarray():
+ w, h = 64, 32
+ arr = np.zeros((h, w, 4), dtype=np.uint8)
+ pixpat.draw_pattern(pixpat.Buffer([arr], 'XRGB8888', w, h, [arr.strides[0]]), 'smpte')
+ assert arr.any()
+
+
+def test_abgr16161616_into_uint16_3d_ndarray():
+ w, h = 64, 32
+ arr = np.zeros((h, w, 4), dtype=np.uint16)
+ pixpat.draw_pattern(pixpat.Buffer([arr], 'ABGR16161616', w, h, [arr.strides[0]]), 'smpte')
+ assert arr.any()
+ assert arr.dtype == np.uint16
+
+
+def test_nv12_into_two_ndarrays():
+ w, h = 64, 32
+ y = np.zeros((h, w), dtype=np.uint8)
+ uv = np.zeros((h // 2, w), dtype=np.uint8) # interleaved U,V at half-height
+ pixpat.draw_pattern(
+ pixpat.Buffer(
+ planes=[y, uv],
+ fmt='NV12',
+ width=w,
+ height=h,
+ strides=[y.strides[0], uv.strides[0]],
+ ),
+ 'smpte',
+ )
+ assert y.any()
+ assert uv.any()
+
+
+def test_convert_with_readonly_ndarray_src():
+ """A numpy view with writeable=False must work as the convert source."""
+ w, h = 64, 32
+ src = np.zeros((h, w, 4), dtype=np.uint8)
+ pixpat.draw_pattern(pixpat.Buffer([src], 'XRGB8888', w, h, [src.strides[0]]), 'smpte')
+ src.flags.writeable = False
+
+ dst = np.zeros((h, w, 4), dtype=np.uint16)
+ pixpat.convert(
+ pixpat.Buffer([dst], 'ABGR16161616', w, h, [dst.strides[0]]),
+ pixpat.Buffer([src], 'XRGB8888', w, h, [src.strides[0]]),
+ )
+ assert dst.any()
+
+
+def test_roundtrip_ndarrays_byte_for_byte():
+ """End-to-end XRGB8888 -> ABGR16161616 -> XRGB8888 with ndarrays only."""
+ w, h = 64, 32
+ src = np.zeros((h, w, 4), dtype=np.uint8)
+ mid = np.zeros((h, w, 4), dtype=np.uint16)
+ dst = np.zeros((h, w, 4), dtype=np.uint8)
+
+ pixpat.draw_pattern(pixpat.Buffer([src], 'XRGB8888', w, h, [src.strides[0]]), 'smpte')
+ pixpat.convert(
+ pixpat.Buffer([mid], 'ABGR16161616', w, h, [mid.strides[0]]),
+ pixpat.Buffer([src], 'XRGB8888', w, h, [src.strides[0]]),
+ )
+ pixpat.convert(
+ pixpat.Buffer([dst], 'XRGB8888', w, h, [dst.strides[0]]),
+ pixpat.Buffer([mid], 'ABGR16161616', w, h, [mid.strides[0]]),
+ )
+ assert np.array_equal(src, dst)
+
+
+def test_writable_view_via_view_method():
+ """`arr.view()` shares memory and is writable by default — drawing into
+ it must update the original."""
+ w, h = 64, 32
+ arr = np.zeros((h, w, 4), dtype=np.uint8)
+ view = arr.view()
+ pixpat.draw_pattern(pixpat.Buffer([view], 'XRGB8888', w, h, [view.strides[0]]), 'smpte')
+ assert arr.any() # the original sees the writes
+
+
+def test_noncontiguous_source_smoke():
+ """Discovery test: what happens if the caller hands us a non-C-contiguous
+ array? pixpat reads ``stride`` bytes per row, so any row-contiguous
+ layout where ``arr.strides[0]`` is the row stride should work; a
+ transposed array (where rows aren't even contiguous) is misuse.
+
+ This test passes a *valid* row-contiguous slice — the top half of a
+ bigger image, taken via slicing — and expects success. It exists to
+ document the contract: 'rows must be contiguous in memory; pass
+ arr.strides[0] as the stride'.
+ """
+ big = np.zeros((64, 64, 4), dtype=np.uint8)
+ top_half = big[:32] # shape (32, 64, 4); rows still C-contiguous
+ assert top_half.strides[0] == 64 * 4
+ pixpat.draw_pattern(
+ pixpat.Buffer([top_half], 'XRGB8888', 64, 32, [top_half.strides[0]]),
+ 'smpte',
+ )
+ assert big[:32].any()
+ assert not big[32:].any() # the other half stayed untouched
diff --git a/subprojects/pixpat/pixpat-python/tests/test_threading.py b/subprojects/pixpat/pixpat-python/tests/test_threading.py
new file mode 100644
index 0000000..99d4620
--- /dev/null
+++ b/subprojects/pixpat/pixpat-python/tests/test_threading.py
@@ -0,0 +1,187 @@
+"""Multi-threaded == single-threaded parity tests.
+
+The chosen ``num_threads`` is supposed to be transparent: the output
+bytes must match the single-threaded output exactly. Anything else is
+a threading bug.
+
+These tests parametrize across one format per writer template so that
+every code path in the dispatcher is covered.
+"""
+
+import pytest
+
+import pixpat
+
+
+# Width must be divisible by 3 (T430/XYYY2101010 pack 3-per-32-bit), 4
+# (Bayer 10P packs 4 pixels per group), and 2 (h_sub).
+# Height must be divisible by 2 (v_sub for 4:2:0 formats).
+#
+# Two sizes: a small one for fast format-coverage parametrization, and
+# a larger one (~HD-width) where worker-stripe alignment can interact
+# with the writers' chroma logic in non-obvious ways.
+SMALL = (192, 64)
+LARGE = (1920, 64)
+
+
+def _alloc(fmt, w, h):
+ """Return (planes, strides) sized for `fmt` at (w, h)."""
+ if fmt in ('NV12', 'NV21'): # semi-planar 4:2:0
+ return [bytearray(w * h), bytearray(w * (h // 2))], [w, w]
+ if fmt in ('NV16', 'NV61'): # semi-planar 4:2:2
+ return [bytearray(w * h), bytearray(w * h)], [w, w]
+ if fmt == 'P030': # semi-planar 4:2:0, 10-bit packed 3-per-32
+ ys, uvs = (w // 3) * 4, (w // 3 // 2) * 8
+ return [bytearray(ys * h), bytearray(uvs * (h // 2))], [ys, uvs]
+ if fmt == 'P230': # semi-planar 4:2:2, 10-bit packed 3-per-32
+ ys, uvs = (w // 3) * 4, (w // 3 // 2) * 8
+ return [bytearray(ys * h), bytearray(uvs * h)], [ys, uvs]
+ if fmt in ('YUV420', 'YVU420'): # planar 4:2:0
+ return (
+ [bytearray(w * h), bytearray((w // 2) * (h // 2)), bytearray((w // 2) * (h // 2))],
+ [w, w // 2, w // 2],
+ )
+ if fmt in ('YUV422', 'YVU422'): # planar 4:2:2
+ return (
+ [bytearray(w * h), bytearray((w // 2) * h), bytearray((w // 2) * h)],
+ [w, w // 2, w // 2],
+ )
+ if fmt in ('YUV444', 'YVU444'): # planar 4:4:4
+ return [bytearray(w * h)] * 3, [w] * 3
+ if fmt == 'T430': # planar packed 4:4:4, 10-bit
+ s = (w // 3) * 4
+ return [bytearray(s * h)] * 3, [s] * 3
+ if fmt in ('Y8', 'R8', 'RGB332'):
+ return [bytearray(w * h)], [w]
+ if fmt == 'XYYY2101010':
+ s = (w // 3) * 4
+ return [bytearray(s * h)], [s]
+ if fmt in ('YUYV', 'YVYU', 'UYVY', 'VYUY'):
+ return [bytearray(w * h * 2)], [w * 2]
+ if fmt in ('Y210', 'Y212', 'Y216'):
+ # 4:2:2, two pixels per 64-bit word.
+ return [bytearray(w * h * 4)], [w * 4]
+ if fmt == 'VUY888':
+ # 24-bit packed YUV, 3 bytes per pixel (storage uint32_t).
+ return [bytearray(w * h * 3)], [w * 3]
+ if fmt in ('XVUY2101010', 'XVUY8888'):
+ return [bytearray(w * h * 4)], [w * 4]
+ if fmt in ('AVUY16161616', 'ABGR16161616'):
+ return [bytearray(w * h * 8)], [w * 8]
+ # MIPI CSI-2 byte packing (Bayer SXXXX10P/12P and grayscale Y10P/Y12P).
+ if fmt.endswith('10P'):
+ s = (w // 4) * 5
+ return [bytearray(s * h)], [s]
+ if fmt.endswith('12P'):
+ s = (w // 2) * 3
+ return [bytearray(s * h)], [s]
+ # Plain Bayer
+ if fmt.startswith('S') and fmt[-1] == '8':
+ return [bytearray(w * h)], [w]
+ if fmt.startswith('S'):
+ return [bytearray(w * h * 2)], [w * 2]
+ # 16-bit RGB
+ if fmt.endswith('565') or fmt.endswith('1555') or fmt.endswith('4444'):
+ return [bytearray(w * h * 2)], [w * 2]
+ # 32-bit RGB (8888 / 2101010 / 1010102 / 888 in 32-bit storage)
+ return [bytearray(w * h * 4)], [w * 4]
+
+
+# One format per writer template in pixpat.cpp.
+DRAW_FORMATS = [
+ 'XRGB8888', # ARGB_Writer
+ 'RGB565', # ARGB_Writer (16-bit)
+ 'ABGR16161616', # ARGB_Writer (normalized wide)
+ 'XVUY2101010', # YUV_Writer
+ 'AVUY16161616', # YUV_Writer (normalized wide)
+ 'YUYV', # YUVPackedWriter
+ 'NV12', # YUVSemiPlanarWriter v_sub=2
+ 'NV16', # YUVSemiPlanarWriter v_sub=1
+ 'P030', # YUVSemiPlanarWriter v_sub=2, 10-bit packed
+ 'P230', # YUVSemiPlanarWriter v_sub=1, 10-bit packed
+ 'YUV420', # YUVPlanarWriter v_sub=2
+ 'YUV422', # YUVPlanarWriter v_sub=1
+ 'YUV444', # YUVPlanarWriter no subsampling
+ 'T430', # YUVPlanarPackedWriter
+ 'Y8', # Y_Writer (1 sample per byte)
+ 'XYYY2101010', # Y_Writer (3 samples per word)
+ 'Y10P', # GrayPacked_Writer (CSI-2 byte packing, Y-only)
+ 'Y210', # YUVPackedWriter with X-padding entries (4:2:2 in 64-bit word)
+ 'R8', # MonoRGB_Writer (single R channel)
+ 'SRGGB8', # Bayer_Writer
+ 'SRGGB10', # Bayer_Writer
+ 'SRGGB10P', # BayerPacked_Writer
+ 'SRGGB12P', # BayerPacked_Writer
+]
+
+# Convert sources: same minus Bayer (Bayer formats have no read path).
+CONVERT_FORMATS = [f for f in DRAW_FORMATS if not f.startswith('S')]
+
+
+@pytest.mark.parametrize('fmt', DRAW_FORMATS)
+@pytest.mark.parametrize(
+ 'pattern',
+ ['smpte', 'kmstest', 'checker', 'hramp', 'vramp', 'dramp', 'zoneplate'],
+)
+@pytest.mark.parametrize('size', [SMALL, LARGE], ids=['small', 'large'])
+def test_draw_pattern_threaded_matches_single(fmt, pattern, size):
+ """draw_pattern with num_threads=4 must produce identical bytes
+ to num_threads=1 for every supported format."""
+ w, h = size
+ s_planes, strides = _alloc(fmt, w, h)
+ pixpat.draw_pattern(pixpat.Buffer(s_planes, fmt, w, h, strides), pattern, num_threads=1)
+
+ t_planes, _ = _alloc(fmt, w, h)
+ pixpat.draw_pattern(pixpat.Buffer(t_planes, fmt, w, h, strides), pattern, num_threads=4)
+
+ for i, (s, t) in enumerate(zip(s_planes, t_planes)):
+ assert s == t, f'{fmt} ({pattern}, {w}x{h}): plane {i} differs threaded vs single'
+
+
+@pytest.mark.parametrize('fmt', ['XRGB8888', 'NV12', 'YUV420', 'BGR888'])
+@pytest.mark.parametrize('size', [SMALL, LARGE], ids=['small', 'large'])
+def test_draw_pattern_plain_threaded_matches_single(fmt, size):
+ """draw_pattern with params= must also be bit-identical across
+ thread counts. Spot-check a few representative formats."""
+ w, h = size
+ params = {'color': '12ab34'}
+ s_planes, strides = _alloc(fmt, w, h)
+ pixpat.draw_pattern(
+ pixpat.Buffer(s_planes, fmt, w, h, strides), 'plain', num_threads=1, params=params
+ )
+
+ t_planes, _ = _alloc(fmt, w, h)
+ pixpat.draw_pattern(
+ pixpat.Buffer(t_planes, fmt, w, h, strides), 'plain', num_threads=4, params=params
+ )
+
+ for i, (s, t) in enumerate(zip(s_planes, t_planes)):
+ assert s == t, f'{fmt} (plain, {w}x{h}): plane {i} differs threaded vs single'
+
+
+@pytest.mark.parametrize('src_fmt', CONVERT_FORMATS)
+@pytest.mark.parametrize('dst_fmt', ['ABGR16161616', 'AVUY16161616', 'XRGB8888', 'NV12', 'YUV420'])
+@pytest.mark.parametrize('size', [SMALL, LARGE], ids=['small', 'large'])
+def test_convert_threaded_matches_single(src_fmt, dst_fmt, size):
+ """convert with num_threads=4 must produce identical bytes
+ to num_threads=1."""
+ w, h = size
+ src_planes, src_strides = _alloc(src_fmt, w, h)
+ pixpat.draw_pattern(pixpat.Buffer(src_planes, src_fmt, w, h, src_strides), 'smpte')
+
+ s_dst_planes, dst_strides = _alloc(dst_fmt, w, h)
+ pixpat.convert(
+ pixpat.Buffer(s_dst_planes, dst_fmt, w, h, dst_strides),
+ pixpat.Buffer(src_planes, src_fmt, w, h, src_strides),
+ num_threads=1,
+ )
+
+ t_dst_planes, _ = _alloc(dst_fmt, w, h)
+ pixpat.convert(
+ pixpat.Buffer(t_dst_planes, dst_fmt, w, h, dst_strides),
+ pixpat.Buffer(src_planes, src_fmt, w, h, src_strides),
+ num_threads=4,
+ )
+
+ for i, (s, t) in enumerate(zip(s_dst_planes, t_dst_planes)):
+ assert s == t, f'{src_fmt}->{dst_fmt} ({w}x{h}): plane {i} differs threaded vs single'
diff --git a/subprojects/pixpat/pyproject.toml b/subprojects/pixpat/pyproject.toml
new file mode 100644
index 0000000..2de83a4
--- /dev/null
+++ b/subprojects/pixpat/pyproject.toml
@@ -0,0 +1,26 @@
+[build-system]
+requires = ["setuptools>=70.1", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "pixpat"
+# Keep version in sync with meson.build (single source of truth would be nice;
+# revisit if it ever drifts in practice).
+version = "1.0.0"
+description = "Pixel format conversion and test pattern generation"
+requires-python = ">=3.9"
+authors = [{name = "Tomi Valkeinen", email = "tomi.valkeinen@ideasonboard.com"}]
+
+[tool.setuptools]
+package-dir = {"" = "pixpat-python"}
+packages = ["pixpat"]
+
+[tool.ruff]
+line-length = 100
+
+[tool.ruff.lint.flake8-quotes]
+inline-quotes = 'single'
+multiline-quotes = 'single'
+
+[tool.ruff.format]
+quote-style = 'single'
diff --git a/subprojects/pixpat/scripts/build_profiles.sh b/subprojects/pixpat/scripts/build_profiles.sh
new file mode 100755
index 0000000..0ae6d63
--- /dev/null
+++ b/subprojects/pixpat/scripts/build_profiles.sh
@@ -0,0 +1,135 @@
+#!/usr/bin/env bash
+# Build every codegen profile under gcc+clang release and report .so sizes.
+# Always wipes existing build dirs to ensure a clean configure.
+# Profiles are discovered from pixpat-native/profiles/*.toml.
+
+set -euo pipefail
+
+# Meson setup flags applied to every (compiler × profile) build dir.
+# Add new -D flags or buildtype overrides here.
+MESON_SETUP_ARGS=(
+ --buildtype=release
+)
+
+# name → toml path.
+PROFILES=()
+profile_dir="pixpat-native/profiles"
+for toml in "$profile_dir"/*.toml; do
+ [[ -e "$toml" ]] || continue
+ name="$(basename "$toml" .toml)"
+ PROFILES+=("$name:$toml")
+done
+
+COMPILERS=(
+ "gcc:gcc:g++"
+ "clang:clang:clang++"
+)
+
+build_one() {
+ local cc_name=$1 cc=$2 cxx=$3 name=$4 toml=$5 log=$6
+ local dir="build-$cc_name-$name"
+
+ local setup_args=("${MESON_SETUP_ARGS[@]}")
+ [[ -d "$dir/meson-info" ]] && setup_args+=(--wipe)
+ setup_args+=("-Dconfig=$toml")
+
+ # Capture meson output so we can show it on failure; on success discard
+ # it and let ninja write its (warnings-only with --quiet) output instead.
+ if ! CC=$cc CXX=$cxx meson setup "$dir" "${setup_args[@]}" >"$log" 2>&1; then
+ return 1
+ fi
+ : >"$log"
+ ninja -C "$dir" --quiet >"$log" 2>&1
+}
+
+pids=()
+labels=()
+logs=()
+for comp in "${COMPILERS[@]}"; do
+ IFS=: read -r cc_name cc cxx <<<"$comp"
+ for entry in "${PROFILES[@]}"; do
+ name="${entry%%:*}"
+ toml="${entry#*:}"
+ dir="build-$cc_name-$name"
+ log=$(mktemp)
+ echo "==> launching $dir"
+ build_one "$cc_name" "$cc" "$cxx" "$name" "$toml" "$log" &
+ pids+=($!)
+ labels+=("$dir")
+ logs+=("$log")
+ done
+done
+
+failed=0
+for i in "${!pids[@]}"; do
+ if wait "${pids[$i]}"; then
+ if [[ -s "${logs[$i]}" ]]; then
+ echo "==> ${labels[$i]} ok (warnings):"
+ cat "${logs[$i]}"
+ else
+ echo "==> ${labels[$i]} ok"
+ fi
+ else
+ echo "==> ${labels[$i]} FAILED:"
+ cat "${logs[$i]}"
+ failed=1
+ fi
+ rm -f "${logs[$i]}"
+done
+(( failed == 0 )) || exit 1
+
+CASES=(
+ "RGB888 -> BGR888"
+ "NV12 -> BGR888"
+ "BGR888 -> NV12"
+ "NV12 -> YUV420"
+ "smpte -> BGR888"
+ "smpte -> NV12"
+ "kmstest -> BGR888"
+)
+LABELS=("RGB->BGR" "NV12->BGR" "BGR->NV12" "NV12->YUV" "smpte/BGR" "smpte/NV12" "kmstest/BGR")
+
+PERF_TEST="pixpat-python/scripts/perf_test.py"
+
+join_cases() {
+ local out=""
+ local c
+ for c in "$@"; do
+ out+="${out:+,}$c"
+ done
+ printf '%s' "$out"
+}
+cases_arg=$(join_cases "${CASES[@]}")
+
+echo
+echo "=== libpixpat.so.0.0.0 sizes + pixpat MP/s (release, --iters 5 --warmup 2) ==="
+printf "%-28s %10s" "build-dir" "bytes"
+for lbl in "${LABELS[@]}"; do
+ printf " %10s" "$lbl"
+done
+echo
+
+for comp in "${COMPILERS[@]}"; do
+ cc_name="${comp%%:*}"
+ for entry in "${PROFILES[@]}"; do
+ name="${entry%%:*}"
+ dir="build-$cc_name-$name"
+ so="$dir/libpixpat.so.0.0.0"
+ if [[ ! -f "$so" ]]; then
+ printf "%-28s %10s\n" "$dir" "MISSING"
+ continue
+ fi
+
+ bytes=$(stat -c%s "$so")
+ tsv=$(PIXPAT_LIB="$so" python3 "$PERF_TEST" \
+ --tsv --iters 5 --warmup 2 \
+ --cases "$cases_arg" 2>/dev/null || true)
+
+ printf "%-28s %10d" "$dir" "$bytes"
+ for c in "${CASES[@]}"; do
+ val=$(awk -F'\t' -v want="$c" '$1 == want { print $2; exit }' <<<"$tsv")
+ printf " %10s" "${val:--}"
+ done
+ echo
+ done
+done
diff --git a/subprojects/pixpat/scripts/format-all.sh b/subprojects/pixpat/scripts/format-all.sh
new file mode 100755
index 0000000..156d807
--- /dev/null
+++ b/subprojects/pixpat/scripts/format-all.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+uncrustify -c uncrustify.cfg -l CPP --no-backup $(git ls-files '*.cpp' '*.h')
diff --git a/subprojects/pixpat/setup.py b/subprojects/pixpat/setup.py
new file mode 100644
index 0000000..0a24682
--- /dev/null
+++ b/subprojects/pixpat/setup.py
@@ -0,0 +1,152 @@
+"""setup.py overrides several setuptools commands to integrate the meson-built
+libpixpat.so into the Python wheel.
+
+Layout (where things land for each workflow):
+
+ meson setup build / meson compile -C build — repo-root build/, the
+ C++ developer's meson dir. Python tooling never touches this.
+
+ pip install -e . — DevEditableWheel
+ symlinks build/libpixpat.so into pixpat-python/pixpat/_lib/.
+ Requires the user to have built `build/` themselves.
+
+ pip install . — MesonBuildPy invokes
+ meson into pixpat-python/build/native/, copies the .so straight into
+ setuptools' build_lib (pixpat-python/build/lib/pixpat/_lib/), and
+ bdist_wheel zips that into the wheel. The source tree's _lib/ is
+ never touched on this path, so wheel builds don't depend on (or
+ pollute) it.
+
+ scripts/build_wheel.sh <arch> — sets PIXPAT_TARGET_ARCH
+ and runs `python -m build`; meson lands in pixpat-python/build-<arch>/
+ native/, setuptools alongside.
+
+bdist_wheel: setuptools' default produces a `py3-none-any` (pure-Python) wheel
+when there's no compiled extension. We bundle a `.so` as package data, so we
+MUST tag the wheel for a specific platform — otherwise pip would
+install our x86_64 wheel on an arm64 box. Target arch is read from
+PIXPAT_TARGET_ARCH and falls back to the running machine's arch — important
+because setuptools' editable_wheel delegates wheel construction to whatever
+bdist_wheel is registered, so this class is invoked during `pip install -e .`
+too.
+"""
+
+import os
+import platform
+import shutil
+import subprocess
+from pathlib import Path
+
+from setuptools import setup
+from setuptools.command.bdist_wheel import bdist_wheel
+from setuptools.command.build import build
+from setuptools.command.build_py import build_py
+from setuptools.command.editable_wheel import editable_wheel
+
+REPO_ROOT = Path(__file__).resolve().parent
+LIB_DIR = REPO_ROOT / 'pixpat-python' / 'pixpat' / '_lib'
+
+
+def _target_arch() -> str:
+ return os.environ.get('PIXPAT_TARGET_ARCH') or platform.machine()
+
+
+def _wheel_build_root() -> Path:
+ """Top-level dir where setuptools and meson stage wheel-build output.
+
+ Plain `pip install .` uses pixpat-python/build/. Cross-compile via
+ PIXPAT_TARGET_ARCH gets a per-arch sibling so meson can keep arches
+ incrementally compiled side by side.
+ """
+ arch = os.environ.get('PIXPAT_TARGET_ARCH')
+ suffix = f'-{arch}' if arch else ''
+ return REPO_ROOT / 'pixpat-python' / f'build{suffix}'
+
+
+class ImpureBdistWheel(bdist_wheel):
+ def finalize_options(self):
+ super().finalize_options()
+ self.root_is_pure = False
+
+ def get_tag(self):
+ return ('py3', 'none', f'linux_{_target_arch()}')
+
+
+class WheelBuild(build):
+ """Direct setuptools' staging into pixpat-python/build[-<arch>]/."""
+
+ def initialize_options(self):
+ super().initialize_options()
+ self.build_base = str(_wheel_build_root())
+
+
+class MesonBuildPy(build_py):
+ """Compile libpixpat.so via meson and stage it into build_lib for the wheel."""
+
+ def run(self):
+ super().run()
+ # editable_mode is set by setuptools' editable_wheel command. In that
+ # path DevEditableWheel has already symlinked the user's repo-root
+ # build/ into the source _lib/, so meson must not run.
+ if not self.editable_mode:
+ self._build_native()
+
+ def _build_native(self):
+ meson_dir = _wheel_build_root() / 'native'
+ arch = _target_arch()
+ cross_args = []
+ if arch != platform.machine():
+ cross_file = REPO_ROOT / 'pixpat-native' / 'cross' / f'{arch}-linux-gnu.txt'
+ if not cross_file.exists():
+ raise SystemExit(f'no cross-file for {arch}: {cross_file}')
+ cross_args = ['--cross-file', str(cross_file)]
+
+ if not (meson_dir / 'meson-info').exists():
+ meson_dir.parent.mkdir(parents=True, exist_ok=True)
+ subprocess.check_call(
+ ['meson', 'setup', str(meson_dir), '--buildtype=release', *cross_args],
+ cwd=REPO_ROOT,
+ )
+ subprocess.check_call(['meson', 'compile', '-C', str(meson_dir)], cwd=REPO_ROOT)
+
+ out_dir = Path(self.build_lib) / 'pixpat' / '_lib'
+ out_dir.mkdir(parents=True, exist_ok=True)
+ # follow_symlinks resolves meson's libpixpat.so -> .so.<maj> -> .so.<maj>.<min>.<patch>
+ # chain into a regular file. The wheel ships only the unversioned name, so we
+ # don't have to track meson's project version here.
+ shutil.copy2(meson_dir / 'libpixpat.so', out_dir / 'libpixpat.so')
+
+
+class DevEditableWheel(editable_wheel):
+ """Symlink the user's repo-root meson build into _lib/ before installing."""
+
+ def run(self):
+ build_so = REPO_ROOT / 'build' / 'libpixpat.so'
+
+ if not build_so.exists():
+ raise SystemExit(
+ f'editable install needs {build_so}.\n'
+ 'Configure and build first:\n'
+ ' meson setup build\n'
+ ' meson compile -C build'
+ )
+
+ LIB_DIR.mkdir(parents=True, exist_ok=True)
+ for old in LIB_DIR.glob('libpixpat.so*'):
+ old.unlink()
+ # Relative path so the symlink survives moves of the source tree.
+ (LIB_DIR / 'libpixpat.so').symlink_to(
+ Path('..') / '..' / '..' / 'build' / 'libpixpat.so'
+ )
+
+ super().run()
+
+
+setup(
+ cmdclass={
+ 'bdist_wheel': ImpureBdistWheel,
+ 'build': WheelBuild,
+ 'build_py': MesonBuildPy,
+ 'editable_wheel': DevEditableWheel,
+ }
+)
diff --git a/subprojects/pixpat/uncrustify.cfg b/subprojects/pixpat/uncrustify.cfg
new file mode 100644
index 0000000..a0a0bb4
--- /dev/null
+++ b/subprojects/pixpat/uncrustify.cfg
@@ -0,0 +1,3708 @@
+# Uncrustify-0.78.1_f
+
+#
+# General options
+#
+
+# The type of line endings.
+#
+# Default: auto
+newlines = auto # lf/crlf/cr/auto
+
+# The original size of tabs in the input.
+#
+# Default: 8
+input_tab_size = 8 # unsigned number
+
+# The size of tabs in the output (only used if align_with_tabs=true).
+#
+# Default: 8
+output_tab_size = 8 # unsigned number
+
+# The ASCII value of the string escape char, usually 92 (\) or (Pawn) 94 (^).
+#
+# Default: 92
+string_escape_char = 92 # unsigned number
+
+# Alternate string escape char (usually only used for Pawn).
+# Only works right before the quote char.
+string_escape_char2 = 0 # unsigned number
+
+# Replace tab characters found in string literals with the escape sequence \t
+# instead.
+string_replace_tab_chars = false # true/false
+
+# Allow interpreting '>=' and '>>=' as part of a template in code like
+# 'void f(list<list<B>>=val);'. If true, 'assert(x<0 && y>=3)' will be broken.
+# Improvements to template detection may make this option obsolete.
+tok_split_gte = false # true/false
+
+# Disable formatting of NL_CONT ('\\n') ended lines (e.g. multi-line macros).
+disable_processing_nl_cont = false # true/false
+
+# Specify the marker used in comments to disable processing of part of the
+# file.
+#
+# Default: *INDENT-OFF*
+disable_processing_cmt = " *INDENT-OFF*" # string
+
+# Specify the marker used in comments to (re)enable processing in a file.
+#
+# Default: *INDENT-ON*
+enable_processing_cmt = " *INDENT-ON*" # string
+
+# Enable parsing of digraphs.
+enable_digraphs = false # true/false
+
+# Option to allow both disable_processing_cmt and enable_processing_cmt
+# strings, if specified, to be interpreted as ECMAScript regular expressions.
+# If true, a regex search will be performed within comments according to the
+# specified patterns in order to disable/enable processing.
+processing_cmt_as_regex = false # true/false
+
+# Add or remove the UTF-8 BOM (recommend 'remove').
+utf8_bom = ignore # ignore/add/remove/force/not_defined
+
+# If the file contains bytes with values between 128 and 255, but is not
+# UTF-8, then output as UTF-8.
+utf8_byte = false # true/false
+
+# Force the output encoding to UTF-8.
+utf8_force = false # true/false
+
+#
+# Spacing options
+#
+
+# Add or remove space around non-assignment symbolic operators ('+', '/', '%',
+# '<<', and so forth).
+sp_arith = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around arithmetic operators '+' and '-'.
+#
+# Overrides sp_arith.
+sp_arith_additive = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around assignment operator '=', '+=', etc.
+sp_assign = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around '=' in C++11 lambda capture specifications.
+#
+# Overrides sp_assign.
+sp_cpp_lambda_assign = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the capture specification of a C++11 lambda when
+# an argument list is present, as in '[] <here> (int x){ ... }'.
+sp_cpp_lambda_square_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the capture specification of a C++11 lambda with
+# no argument list is present, as in '[] <here> { ... }'.
+sp_cpp_lambda_square_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the opening parenthesis and before the closing
+# parenthesis of a argument list of a C++11 lambda, as in
+# '[]( <here> ){ ... }'
+# with an empty list.
+sp_cpp_lambda_argument_list_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the opening parenthesis and before the closing
+# parenthesis of a argument list of a C++11 lambda, as in
+# '[]( <here> int x <here> ){ ... }'.
+sp_cpp_lambda_argument_list = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the argument list of a C++11 lambda, as in
+# '[](int x) <here> { ... }'.
+sp_cpp_lambda_paren_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a lambda body and its call operator of an
+# immediately invoked lambda, as in '[]( ... ){ ... } <here> ( ... )'.
+sp_cpp_lambda_fparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around assignment operator '=' in a prototype.
+#
+# If set to ignore, use sp_assign.
+sp_assign_default = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before assignment operator '=', '+=', etc.
+#
+# Overrides sp_assign.
+sp_before_assign = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after assignment operator '=', '+=', etc.
+#
+# Overrides sp_assign.
+sp_after_assign = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space in 'enum {'.
+#
+# Default: add
+sp_enum_brace = add # ignore/add/remove/force/not_defined
+
+# Add or remove space in 'NS_ENUM ('.
+sp_enum_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around assignment '=' in enum.
+sp_enum_assign = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before assignment '=' in enum.
+#
+# Overrides sp_enum_assign.
+sp_enum_before_assign = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after assignment '=' in enum.
+#
+# Overrides sp_enum_assign.
+sp_enum_after_assign = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around assignment ':' in enum.
+sp_enum_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around preprocessor '##' concatenation operator.
+#
+# Default: add
+sp_pp_concat = add # ignore/add/remove/force/not_defined
+
+# Add or remove space after preprocessor '#' stringify operator.
+# Also affects the '#@' charizing operator.
+sp_pp_stringify = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before preprocessor '#' stringify operator
+# as in '#define x(y) L#y'.
+sp_before_pp_stringify = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around boolean operators '&&' and '||'.
+sp_bool = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around compare operator '<', '>', '==', etc.
+sp_compare = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside '(' and ')'.
+sp_inside_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between nested parentheses, i.e. '((' vs. ') )'.
+sp_paren_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between back-to-back parentheses, i.e. ')(' vs. ') ('.
+sp_cparen_oparen = ignore # ignore/add/remove/force/not_defined
+
+# Whether to balance spaces inside nested parentheses.
+sp_balance_nested_parens = false # true/false
+
+# Add or remove space between ')' and '{'.
+sp_paren_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between nested braces, i.e. '{{' vs. '{ {'.
+sp_brace_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before pointer star '*'.
+sp_before_ptr_star = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before pointer star '*' that isn't followed by a
+# variable name. If set to ignore, sp_before_ptr_star is used instead.
+sp_before_unnamed_ptr_star = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before pointer star '*' that is followed by a qualifier.
+# If set to ignore, sp_before_unnamed_ptr_star is used instead.
+sp_before_qualifier_ptr_star = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before pointer star '*' that is followed by 'operator' keyword.
+# If set to ignore, sp_before_unnamed_ptr_star is used instead.
+sp_before_operator_ptr_star = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before pointer star '*' that is followed by
+# a class scope (as in 'int *MyClass::method()') or namespace scope
+# (as in 'int *my_ns::func()').
+# If set to ignore, sp_before_unnamed_ptr_star is used instead.
+sp_before_scope_ptr_star = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before pointer star '*' that is followed by '::',
+# as in 'int *::func()'.
+# If set to ignore, sp_before_unnamed_ptr_star is used instead.
+sp_before_global_scope_ptr_star = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a qualifier and a pointer star '*' that isn't
+# followed by a variable name, as in '(char const *)'. If set to ignore,
+# sp_before_ptr_star is used instead.
+sp_qualifier_unnamed_ptr_star = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between pointer stars '*', as in 'int ***a;'.
+sp_between_ptr_star = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between pointer star '*' and reference '&', as in 'int *& a;'.
+sp_between_ptr_ref = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after pointer star '*', if followed by a word.
+#
+# Overrides sp_type_func.
+sp_after_ptr_star = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after pointer caret '^', if followed by a word.
+sp_after_ptr_block_caret = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after pointer star '*', if followed by a qualifier.
+sp_after_ptr_star_qualifier = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after a pointer star '*', if followed by a function
+# prototype or function definition.
+#
+# Overrides sp_after_ptr_star and sp_type_func.
+sp_after_ptr_star_func = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after a pointer star '*' in the trailing return of a
+# function prototype or function definition.
+sp_after_ptr_star_trailing = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between the pointer star '*' and the name of the variable
+# in a function pointer definition.
+sp_ptr_star_func_var = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between the pointer star '*' and the name of the type
+# in a function pointer type definition.
+sp_ptr_star_func_type = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after a pointer star '*', if followed by an open
+# parenthesis, as in 'void* (*)()'.
+sp_ptr_star_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before a pointer star '*', if followed by a function
+# prototype or function definition. If set to ignore, sp_before_ptr_star is
+# used instead.
+sp_before_ptr_star_func = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a qualifier and a pointer star '*' followed by
+# the name of the function in a function prototype or definition, as in
+# 'char const *foo()`. If set to ignore, sp_before_ptr_star is used instead.
+sp_qualifier_ptr_star_func = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before a pointer star '*' in the trailing return of a
+# function prototype or function definition.
+sp_before_ptr_star_trailing = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a qualifier and a pointer star '*' in the
+# trailing return of a function prototype or function definition, as in
+# 'auto foo() -> char const *'.
+sp_qualifier_ptr_star_trailing = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before a reference sign '&'.
+sp_before_byref = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before a reference sign '&' that isn't followed by a
+# variable name. If set to ignore, sp_before_byref is used instead.
+sp_before_unnamed_byref = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after reference sign '&', if followed by a word.
+#
+# Overrides sp_type_func.
+sp_after_byref = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after a reference sign '&', if followed by a function
+# prototype or function definition.
+#
+# Overrides sp_after_byref and sp_type_func.
+sp_after_byref_func = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before a reference sign '&', if followed by a function
+# prototype or function definition.
+sp_before_byref_func = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after a reference sign '&', if followed by an open
+# parenthesis, as in 'char& (*)()'.
+sp_byref_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between type and word. In cases where total removal of
+# whitespace would be a syntax error, a value of 'remove' is treated the same
+# as 'force'.
+#
+# This also affects some other instances of space following a type that are
+# not covered by other options; for example, between the return type and
+# parenthesis of a function type template argument, between the type and
+# parenthesis of an array parameter, or between 'decltype(...)' and the
+# following word.
+#
+# Default: force
+sp_after_type = force # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'decltype(...)' and word,
+# brace or function call.
+sp_after_decltype = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove space before the parenthesis in the D constructs
+# 'template Foo(' and 'class Foo('.
+sp_before_template_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'template' and '<'.
+# If set to ignore, sp_before_angle is used.
+sp_template_angle = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before '<'.
+sp_before_angle = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside '<' and '>'.
+sp_inside_angle = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside '<>'.
+# if empty.
+sp_inside_angle_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '>' and ':'.
+sp_angle_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after '>'.
+sp_after_angle = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '>' and '(' as found in 'new List<byte>(foo);'.
+sp_angle_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '>' and '()' as found in 'new List<byte>();'.
+sp_angle_paren_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '>' and a word as in 'List<byte> m;' or
+# 'template <typename T> static ...'.
+sp_angle_word = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '>' and '>' in '>>' (template stuff).
+#
+# Default: add
+sp_angle_shift = add # ignore/add/remove/force/not_defined
+
+# (C++11) Permit removal of the space between '>>' in 'foo<bar<int> >'. Note
+# that sp_angle_shift cannot remove the space without this option.
+sp_permit_cpp11_shift = false # true/false
+
+# Add or remove space before '(' of control statements ('if', 'for', 'switch',
+# 'while', etc.).
+sp_before_sparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside '(' and ')' of control statements other than
+# 'for'.
+sp_inside_sparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after '(' of control statements other than 'for'.
+#
+# Overrides sp_inside_sparen.
+sp_inside_sparen_open = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before ')' of control statements other than 'for'.
+#
+# Overrides sp_inside_sparen.
+sp_inside_sparen_close = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside '(' and ')' of 'for' statements.
+sp_inside_for = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after '(' of 'for' statements.
+#
+# Overrides sp_inside_for.
+sp_inside_for_open = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before ')' of 'for' statements.
+#
+# Overrides sp_inside_for.
+sp_inside_for_close = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '((' or '))' of control statements.
+sp_sparen_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after ')' of control statements.
+sp_after_sparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between ')' and '{' of control statements.
+sp_sparen_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'do' and '{'.
+sp_do_brace_open = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '}' and 'while'.
+sp_brace_close_while = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'while' and '('. Overrides sp_before_sparen.
+sp_while_paren_open = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove space between 'invariant' and '('.
+sp_invariant_paren = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove space after the ')' in 'invariant (C) c'.
+sp_after_invariant_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before empty statement ';' on 'if', 'for' and 'while'.
+sp_special_semi = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before ';'.
+#
+# Default: remove
+sp_before_semi = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space before ';' in non-empty 'for' statements.
+sp_before_semi_for = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before a semicolon of an empty left part of a for
+# statement, as in 'for ( <here> ; ; )'.
+sp_before_semi_for_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between the semicolons of an empty middle part of a for
+# statement, as in 'for ( ; <here> ; )'.
+sp_between_semi_for_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after ';', except when followed by a comment.
+#
+# Default: add
+sp_after_semi = add # ignore/add/remove/force/not_defined
+
+# Add or remove space after ';' in non-empty 'for' statements.
+#
+# Default: force
+sp_after_semi_for = force # ignore/add/remove/force/not_defined
+
+# Add or remove space after the final semicolon of an empty part of a for
+# statement, as in 'for ( ; ; <here> )'.
+sp_after_semi_for_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before '[' (except '[]').
+sp_before_square = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before '[' for a variable definition.
+#
+# Default: remove
+sp_before_vardef_square = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space before '[' for asm block.
+sp_before_square_asm_block = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before '[]'.
+sp_before_squares = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before C++17 structured bindings.
+sp_cpp_before_struct_binding = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside a non-empty '[' and ']'.
+sp_inside_square = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside '[]'.
+# if empty.
+sp_inside_square_empty = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space inside a non-empty Objective-C boxed array '@[' and
+# ']'. If set to ignore, sp_inside_square is used.
+sp_inside_square_oc_array = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after ',', i.e. 'a,b' vs. 'a, b'.
+sp_after_comma = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before ',', i.e. 'a,b' vs. 'a ,b'.
+#
+# Default: remove
+sp_before_comma = remove # ignore/add/remove/force/not_defined
+
+# (C#, Vala) Add or remove space between ',' and ']' in multidimensional array type
+# like 'int[,,]'.
+sp_after_mdatype_commas = ignore # ignore/add/remove/force/not_defined
+
+# (C#, Vala) Add or remove space between '[' and ',' in multidimensional array type
+# like 'int[,,]'.
+sp_before_mdatype_commas = ignore # ignore/add/remove/force/not_defined
+
+# (C#, Vala) Add or remove space between ',' in multidimensional array type
+# like 'int[,,]'.
+sp_between_mdatype_commas = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between an open parenthesis and comma,
+# i.e. '(,' vs. '( ,'.
+#
+# Default: force
+sp_paren_comma = force # ignore/add/remove/force/not_defined
+
+# Add or remove space between a type and ':'.
+sp_type_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the variadic '...' when preceded by a
+# non-punctuator.
+# The value REMOVE will be overridden with FORCE
+sp_after_ellipsis = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before the variadic '...' when preceded by a
+# non-punctuator.
+# The value REMOVE will be overridden with FORCE
+sp_before_ellipsis = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a type and '...'.
+sp_type_ellipsis = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a '*' and '...'.
+sp_ptr_type_ellipsis = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between ')' and '...'.
+sp_paren_ellipsis = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '&&' and '...'.
+sp_byref_ellipsis = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between ')' and a qualifier such as 'const'.
+sp_paren_qualifier = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between ')' and 'noexcept'.
+sp_paren_noexcept = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after class ':'.
+sp_after_class_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before class ':'.
+sp_before_class_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after class constructor ':'.
+#
+# Default: add
+sp_after_constr_colon = add # ignore/add/remove/force/not_defined
+
+# Add or remove space before class constructor ':'.
+#
+# Default: add
+sp_before_constr_colon = add # ignore/add/remove/force/not_defined
+
+# Add or remove space before case ':'.
+#
+# Default: remove
+sp_before_case_colon = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'operator' and operator sign.
+sp_after_operator = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between the operator symbol and the open parenthesis, as
+# in 'operator ++('.
+sp_after_operator_sym = ignore # ignore/add/remove/force/not_defined
+
+# Overrides sp_after_operator_sym when the operator has no arguments, as in
+# 'operator *()'.
+sp_after_operator_sym_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after C/D cast, i.e. 'cast(int)a' vs. 'cast(int) a' or
+# '(int)a' vs. '(int) a'.
+sp_after_cast = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove spaces inside cast parentheses.
+sp_inside_paren_cast = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between the type and open parenthesis in a C++ cast,
+# i.e. 'int(exp)' vs. 'int (exp)'.
+sp_cpp_cast_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'sizeof' and '('.
+sp_sizeof_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'sizeof' and '...'.
+sp_sizeof_ellipsis = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'sizeof...' and '('.
+sp_sizeof_ellipsis_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '...' and a parameter pack.
+sp_ellipsis_parameter_pack = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a parameter pack and '...'.
+sp_parameter_pack_ellipsis = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'decltype' and '('.
+sp_decltype_paren = ignore # ignore/add/remove/force/not_defined
+
+# (Pawn) Add or remove space after the tag keyword.
+sp_after_tag = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside enum '{' and '}'.
+sp_inside_braces_enum = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside struct/union '{' and '}'.
+sp_inside_braces_struct = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space inside Objective-C boxed dictionary '{' and '}'
+sp_inside_braces_oc_dict = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after open brace in an unnamed temporary
+# direct-list-initialization
+# if statement is a brace_init_lst
+# works only if sp_brace_brace is set to ignore.
+sp_after_type_brace_init_lst_open = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before close brace in an unnamed temporary
+# direct-list-initialization
+# if statement is a brace_init_lst
+# works only if sp_brace_brace is set to ignore.
+sp_before_type_brace_init_lst_close = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside an unnamed temporary direct-list-initialization
+# if statement is a brace_init_lst
+# works only if sp_brace_brace is set to ignore
+# works only if sp_before_type_brace_init_lst_close is set to ignore.
+sp_inside_type_brace_init_lst = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside '{' and '}'.
+sp_inside_braces = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside '{}'.
+# if empty.
+sp_inside_braces_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around trailing return operator '->'.
+sp_trailing_return = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between return type and function name. A minimum of 1
+# is forced except for pointer return types.
+sp_type_func = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between type and open brace of an unnamed temporary
+# direct-list-initialization.
+sp_type_brace_init_lst = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between function name and '(' on function declaration.
+sp_func_proto_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between function name and '()' on function declaration
+# if empty.
+sp_func_proto_paren_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between function name and '(' with a typedef specifier.
+sp_func_type_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between alias name and '(' of a non-pointer function type typedef.
+sp_func_def_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between function name and '()' on function definition
+# if empty.
+sp_func_def_paren_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside empty function '()'.
+# Overrides sp_after_angle unless use_sp_after_angle_always is set to true.
+sp_inside_fparens = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside function '(' and ')'.
+sp_inside_fparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside user functor '(' and ')'.
+sp_func_call_user_inside_rparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside empty functor '()'.
+# Overrides sp_after_angle unless use_sp_after_angle_always is set to true.
+sp_inside_rparens = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside functor '(' and ')'.
+sp_inside_rparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside the first parentheses in a function type, as in
+# 'void (*x)(...)'.
+sp_inside_tparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between the ')' and '(' in a function type, as in
+# 'void (*x)(...)'.
+sp_after_tparen_close = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between ']' and '(' when part of a function call.
+sp_square_fparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between ')' and '{' of function.
+sp_fparen_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between ')' and '{' of a function call in object
+# initialization.
+#
+# Overrides sp_fparen_brace.
+sp_fparen_brace_initializer = ignore # ignore/add/remove/force/not_defined
+
+# (Java) Add or remove space between ')' and '{{' of double brace initializer.
+sp_fparen_dbrace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between function name and '(' on function calls.
+sp_func_call_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between function name and '()' on function calls without
+# parameters. If set to ignore (the default), sp_func_call_paren is used.
+sp_func_call_paren_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between the user function name and '(' on function
+# calls. You need to set a keyword to be a user function in the config file,
+# like:
+# set func_call_user tr _ i18n
+sp_func_call_user_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside user function '(' and ')'.
+sp_func_call_user_inside_fparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between nested parentheses with user functions,
+# i.e. '((' vs. '( ('.
+sp_func_call_user_paren_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a constructor/destructor and the open
+# parenthesis.
+sp_func_class_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a constructor without parameters or destructor
+# and '()'.
+sp_func_class_paren_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after 'return'.
+#
+# Default: force
+sp_return = force # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'return' and '('.
+sp_return_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'return' and '{'.
+sp_return_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '__attribute__' and '('.
+sp_attribute_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'defined' and '(' in '#if defined (FOO)'.
+sp_defined_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'throw' and '(' in 'throw (something)'.
+sp_throw_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'throw' and anything other than '(' as in
+# '@throw [...];'.
+sp_after_throw = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'catch' and '(' in 'catch (something) { }'.
+# If set to ignore, sp_before_sparen is used.
+sp_catch_paren = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space between '@catch' and '('
+# in '@catch (something) { }'. If set to ignore, sp_catch_paren is used.
+sp_oc_catch_paren = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space before Objective-C protocol list
+# as in '@protocol Protocol<here><Protocol_A>' or '@interface MyClass : NSObject<here><MyProtocol>'.
+sp_before_oc_proto_list = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space between class name and '('
+# in '@interface className(categoryName)<ProtocolName>:BaseClass'
+sp_oc_classname_paren = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove space between 'version' and '('
+# in 'version (something) { }'. If set to ignore, sp_before_sparen is used.
+sp_version_paren = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove space between 'scope' and '('
+# in 'scope (something) { }'. If set to ignore, sp_before_sparen is used.
+sp_scope_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'super' and '(' in 'super (something)'.
+#
+# Default: remove
+sp_super_paren = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'this' and '(' in 'this (something)'.
+#
+# Default: remove
+sp_this_paren = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space between a macro name and its definition.
+sp_macro = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a macro function ')' and its definition.
+sp_macro_func = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'else' and '{' if on the same line.
+sp_else_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '}' and 'else' if on the same line.
+sp_brace_else = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '}' and the name of a typedef on the same line.
+sp_brace_typedef = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before the '{' of a 'catch' statement, if the '{' and
+# 'catch' are on the same line, as in 'catch (decl) <here> {'.
+sp_catch_brace = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space before the '{' of a '@catch' statement, if the '{'
+# and '@catch' are on the same line, as in '@catch (decl) <here> {'.
+# If set to ignore, sp_catch_brace is used.
+sp_oc_catch_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '}' and 'catch' if on the same line.
+sp_brace_catch = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space between '}' and '@catch' if on the same line.
+# If set to ignore, sp_brace_catch is used.
+sp_oc_brace_catch = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'finally' and '{' if on the same line.
+sp_finally_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between '}' and 'finally' if on the same line.
+sp_brace_finally = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'try' and '{' if on the same line.
+sp_try_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between get/set and '{' if on the same line.
+sp_getset_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a variable and '{' for C++ uniform
+# initialization.
+sp_word_brace_init_lst = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between a variable and '{' for a namespace.
+#
+# Default: add
+sp_word_brace_ns = add # ignore/add/remove/force/not_defined
+
+# Add or remove space before the '::' operator.
+sp_before_dc = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the '::' operator.
+sp_after_dc = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove around the D named array initializer ':' operator.
+sp_d_array_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the '!' (not) unary operator.
+#
+# Default: remove
+sp_not = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space between two '!' (not) unary operators.
+# If set to ignore, sp_not will be used.
+sp_not_not = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the '~' (invert) unary operator.
+#
+# Default: remove
+sp_inv = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space after the '&' (address-of) unary operator. This does not
+# affect the spacing after a '&' that is part of a type.
+#
+# Default: remove
+sp_addr = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space around the '.' or '->' operators.
+#
+# Default: remove
+sp_member = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space after the '*' (dereference) unary operator. This does
+# not affect the spacing after a '*' that is part of a type.
+#
+# Default: remove
+sp_deref = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space after '+' or '-', as in 'x = -5' or 'y = +7'.
+#
+# Default: remove
+sp_sign = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space between '++' and '--' the word to which it is being
+# applied, as in '(--x)' or 'y++;'.
+#
+# Default: remove
+sp_incdec = remove # ignore/add/remove/force/not_defined
+
+# Add or remove space before a backslash-newline at the end of a line.
+#
+# Default: add
+sp_before_nl_cont = add # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space after the scope '+' or '-', as in '-(void) foo;'
+# or '+(int) bar;'.
+sp_after_oc_scope = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space after the colon in message specs,
+# i.e. '-(int) f:(int) x;' vs. '-(int) f: (int) x;'.
+sp_after_oc_colon = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space before the colon in message specs,
+# i.e. '-(int) f: (int) x;' vs. '-(int) f : (int) x;'.
+sp_before_oc_colon = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space after the colon in immutable dictionary expression
+# 'NSDictionary *test = @{@"foo" :@"bar"};'.
+sp_after_oc_dict_colon = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space before the colon in immutable dictionary expression
+# 'NSDictionary *test = @{@"foo" :@"bar"};'.
+sp_before_oc_dict_colon = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space after the colon in message specs,
+# i.e. '[object setValue:1];' vs. '[object setValue: 1];'.
+sp_after_send_oc_colon = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space before the colon in message specs,
+# i.e. '[object setValue:1];' vs. '[object setValue :1];'.
+sp_before_send_oc_colon = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space after the (type) in message specs,
+# i.e. '-(int)f: (int) x;' vs. '-(int)f: (int)x;'.
+sp_after_oc_type = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space after the first (type) in message specs,
+# i.e. '-(int) f:(int)x;' vs. '-(int)f:(int)x;'.
+sp_after_oc_return_type = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space between '@selector' and '(',
+# i.e. '@selector(msgName)' vs. '@selector (msgName)'.
+# Also applies to '@protocol()' constructs.
+sp_after_oc_at_sel = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space between '@selector(x)' and the following word,
+# i.e. '@selector(foo) a:' vs. '@selector(foo)a:'.
+sp_after_oc_at_sel_parens = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space inside '@selector' parentheses,
+# i.e. '@selector(foo)' vs. '@selector( foo )'.
+# Also applies to '@protocol()' constructs.
+sp_inside_oc_at_sel_parens = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space before a block pointer caret,
+# i.e. '^int (int arg){...}' vs. ' ^int (int arg){...}'.
+sp_before_oc_block_caret = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space after a block pointer caret,
+# i.e. '^int (int arg){...}' vs. '^ int (int arg){...}'.
+sp_after_oc_block_caret = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space between the receiver and selector in a message,
+# as in '[receiver selector ...]'.
+sp_after_oc_msg_receiver = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space after '@property'.
+sp_after_oc_property = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove space between '@synchronized' and the open parenthesis,
+# i.e. '@synchronized(foo)' vs. '@synchronized (foo)'.
+sp_after_oc_synchronized = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around the ':' in 'b ? t : f'.
+sp_cond_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before the ':' in 'b ? t : f'.
+#
+# Overrides sp_cond_colon.
+sp_cond_colon_before = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the ':' in 'b ? t : f'.
+#
+# Overrides sp_cond_colon.
+sp_cond_colon_after = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space around the '?' in 'b ? t : f'.
+sp_cond_question = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before the '?' in 'b ? t : f'.
+#
+# Overrides sp_cond_question.
+sp_cond_question_before = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the '?' in 'b ? t : f'.
+#
+# Overrides sp_cond_question.
+sp_cond_question_after = ignore # ignore/add/remove/force/not_defined
+
+# In the abbreviated ternary form '(a ?: b)', add or remove space between '?'
+# and ':'.
+#
+# Overrides all other sp_cond_* options.
+sp_cond_ternary_short = ignore # ignore/add/remove/force/not_defined
+
+# Fix the spacing between 'case' and the label. Only 'ignore' and 'force' make
+# sense here.
+sp_case_label = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove space around the D '..' operator.
+sp_range = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after ':' in a Java/C++11 range-based 'for',
+# as in 'for (Type var : <here> expr)'.
+sp_after_for_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before ':' in a Java/C++11 range-based 'for',
+# as in 'for (Type var <here> : expr)'.
+sp_before_for_colon = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove space between 'extern' and '(' as in 'extern <here> (C)'.
+sp_extern_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the opening of a C++ comment, as in '// <here> A'.
+sp_cmt_cpp_start = ignore # ignore/add/remove/force/not_defined
+
+# remove space after the '//' and the pvs command '-V1234',
+# only works with sp_cmt_cpp_start set to add or force.
+sp_cmt_cpp_pvs = false # true/false
+
+# remove space after the '//' and the command 'lint',
+# only works with sp_cmt_cpp_start set to add or force.
+sp_cmt_cpp_lint = false # true/false
+
+# Add or remove space in a C++ region marker comment, as in '// <here> BEGIN'.
+# A region marker is defined as a comment which is not preceded by other text
+# (i.e. the comment is the first non-whitespace on the line), and which starts
+# with either 'BEGIN' or 'END'.
+#
+# Overrides sp_cmt_cpp_start.
+sp_cmt_cpp_region = ignore # ignore/add/remove/force/not_defined
+
+# If true, space added with sp_cmt_cpp_start will be added after Doxygen
+# sequences like '///', '///<', '//!' and '//!<'.
+sp_cmt_cpp_doxygen = false # true/false
+
+# If true, space added with sp_cmt_cpp_start will be added after Qt translator
+# or meta-data comments like '//:', '//=', and '//~'.
+sp_cmt_cpp_qttr = false # true/false
+
+# Add or remove space between #else or #endif and a trailing comment.
+sp_endif_cmt = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after 'new', 'delete' and 'delete[]'.
+sp_after_new = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between 'new' and '(' in 'new()'.
+sp_between_new_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space between ')' and type in 'new(foo) BAR'.
+sp_after_newop_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space inside parentheses of the new operator
+# as in 'new(foo) BAR'.
+sp_inside_newop_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after the open parenthesis of the new operator,
+# as in 'new(foo) BAR'.
+#
+# Overrides sp_inside_newop_paren.
+sp_inside_newop_paren_open = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before the close parenthesis of the new operator,
+# as in 'new(foo) BAR'.
+#
+# Overrides sp_inside_newop_paren.
+sp_inside_newop_paren_close = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before a trailing comment.
+sp_before_tr_cmt = ignore # ignore/add/remove/force/not_defined
+
+# Number of spaces before a trailing comment.
+sp_num_before_tr_cmt = 0 # unsigned number
+
+# Add or remove space before an embedded comment.
+#
+# Default: force
+sp_before_emb_cmt = force # ignore/add/remove/force/not_defined
+
+# Number of spaces before an embedded comment.
+#
+# Default: 1
+sp_num_before_emb_cmt = 1 # unsigned number
+
+# Add or remove space after an embedded comment.
+#
+# Default: force
+sp_after_emb_cmt = force # ignore/add/remove/force/not_defined
+
+# Number of spaces after an embedded comment.
+#
+# Default: 1
+sp_num_after_emb_cmt = 1 # unsigned number
+
+# (Java) Add or remove space between an annotation and the open parenthesis.
+sp_annotation_paren = ignore # ignore/add/remove/force/not_defined
+
+# If true, vbrace tokens are dropped to the previous token and skipped.
+sp_skip_vbrace_tokens = false # true/false
+
+# Add or remove space after 'noexcept'.
+sp_after_noexcept = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after '_'.
+sp_vala_after_translation = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space before a bit colon ':'.
+sp_before_bit_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove space after a bit colon ':'.
+sp_after_bit_colon = ignore # ignore/add/remove/force/not_defined
+
+# If true, a <TAB> is inserted after #define.
+force_tab_after_define = false # true/false
+
+#
+# Indenting options
+#
+
+# The number of columns to indent per level. Usually 2, 3, 4, or 8.
+#
+# Default: 8
+indent_columns = 8 # unsigned number
+
+# Whether to ignore indent for the first continuation line. Subsequent
+# continuation lines will still be indented to match the first.
+indent_ignore_first_continue = false # true/false
+
+# The continuation indent. If non-zero, this overrides the indent of '(', '['
+# and '=' continuation indents. Negative values are OK; negative value is
+# absolute and not increased for each '(' or '[' level.
+#
+# For FreeBSD, this is set to 4.
+# Requires indent_ignore_first_continue=false.
+indent_continue = 0 # number
+
+# The continuation indent, only for class header line(s). If non-zero, this
+# overrides the indent of 'class' continuation indents.
+# Requires indent_ignore_first_continue=false.
+indent_continue_class_head = 0 # unsigned number
+
+# Whether to indent empty lines (i.e. lines which contain only spaces before
+# the newline character).
+indent_single_newlines = false # true/false
+
+# The continuation indent for func_*_param if they are true. If non-zero, this
+# overrides the indent.
+indent_param = 0 # unsigned number
+
+# How to use tabs when indenting code.
+#
+# 0: Spaces only
+# 1: Indent with tabs to brace level, align with spaces (default)
+# 2: Indent and align with tabs, using spaces when not on a tabstop
+#
+# Default: 1
+indent_with_tabs = 1 # unsigned number
+
+# Whether to indent comments that are not at a brace level with tabs on a
+# tabstop. Requires indent_with_tabs=2. If false, will use spaces.
+indent_cmt_with_tabs = false # true/false
+
+# Whether to indent strings broken by '\' so that they line up.
+indent_align_string = false # true/false
+
+# The number of spaces to indent multi-line XML strings.
+# Requires indent_align_string=true.
+indent_xml_string = 0 # unsigned number
+
+# Spaces to indent '{' from level.
+indent_brace = 0 # unsigned number
+
+# Whether braces are indented to the body level.
+indent_braces = false # true/false
+
+# Whether to disable indenting function braces if indent_braces=true.
+indent_braces_no_func = false # true/false
+
+# Whether to disable indenting class braces if indent_braces=true.
+indent_braces_no_class = false # true/false
+
+# Whether to disable indenting struct braces if indent_braces=true.
+indent_braces_no_struct = false # true/false
+
+# Whether to indent based on the size of the brace parent,
+# i.e. 'if' => 3 spaces, 'for' => 4 spaces, etc.
+indent_brace_parent = false # true/false
+
+# Whether to indent based on the open parenthesis instead of the open brace
+# in '({\n'.
+indent_paren_open_brace = false # true/false
+
+# (C#) Whether to indent the brace of a C# delegate by another level.
+indent_cs_delegate_brace = false # true/false
+
+# (C#) Whether to indent a C# delegate (to handle delegates with no brace) by
+# another level.
+indent_cs_delegate_body = false # true/false
+
+# Whether to indent the body of a 'namespace'.
+indent_namespace = false # true/false
+
+# Whether to indent only the first namespace, and not any nested namespaces.
+# Requires indent_namespace=true.
+indent_namespace_single_indent = false # true/false
+
+# The number of spaces to indent a namespace block.
+# If set to zero, use the value indent_columns
+indent_namespace_level = 0 # unsigned number
+
+# If the body of the namespace is longer than this number, it won't be
+# indented. Requires indent_namespace=true. 0 means no limit.
+indent_namespace_limit = 0 # unsigned number
+
+# Whether to indent only in inner namespaces (nested in other namespaces).
+# Requires indent_namespace=true.
+indent_namespace_inner_only = false # true/false
+
+# Whether the 'extern "C"' body is indented.
+indent_extern = false # true/false
+
+# Whether the 'class' body is indented.
+indent_class = true # true/false
+
+# Whether to ignore indent for the leading base class colon.
+indent_ignore_before_class_colon = false # true/false
+
+# Additional indent before the leading base class colon.
+# Negative values decrease indent down to the first column.
+# Requires indent_ignore_before_class_colon=false and a newline break before
+# the colon (see pos_class_colon and nl_class_colon)
+indent_before_class_colon = 0 # number
+
+# Whether to indent the stuff after a leading base class colon.
+indent_class_colon = false # true/false
+
+# Whether to indent based on a class colon instead of the stuff after the
+# colon. Requires indent_class_colon=true.
+indent_class_on_colon = false # true/false
+
+# Whether to ignore indent for a leading class initializer colon.
+indent_ignore_before_constr_colon = false # true/false
+
+# Whether to indent the stuff after a leading class initializer colon.
+indent_constr_colon = false # true/false
+
+# Virtual indent from the ':' for leading member initializers.
+#
+# Default: 2
+indent_ctor_init_leading = 2 # unsigned number
+
+# Virtual indent from the ':' for following member initializers.
+#
+# Default: 2
+indent_ctor_init_following = 2 # unsigned number
+
+# Additional indent for constructor initializer list.
+# Negative values decrease indent down to the first column.
+indent_ctor_init = 0 # number
+
+# Whether to indent 'if' following 'else' as a new block under the 'else'.
+# If false, 'else\nif' is treated as 'else if' for indenting purposes.
+indent_else_if = false # true/false
+
+# Amount to indent variable declarations after a open brace.
+#
+# <0: Relative
+# >=0: Absolute
+indent_var_def_blk = 0 # number
+
+# Whether to indent continued variable declarations instead of aligning.
+indent_var_def_cont = false # true/false
+
+# How to indent continued shift expressions ('<<' and '>>').
+# Set align_left_shift=false when using this.
+# 0: Align shift operators instead of indenting them (default)
+# 1: Indent by one level
+# -1: Preserve original indentation
+indent_shift = 0 # number
+
+# Whether to force indentation of function definitions to start in column 1.
+indent_func_def_force_col1 = false # true/false
+
+# Whether to indent continued function call parameters one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_call_param = false # true/false
+
+# Whether to indent continued function definition parameters one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_def_param = false # true/false
+
+# for function definitions, only if indent_func_def_param is false
+# Allows to align params when appropriate and indent them when not
+# behave as if it was true if paren position is more than this value
+# if paren position is more than the option value
+indent_func_def_param_paren_pos_threshold = 0 # unsigned number
+
+# Whether to indent continued function call prototype one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_proto_param = false # true/false
+
+# Whether to indent continued function call declaration one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_class_param = false # true/false
+
+# Whether to indent continued class variable constructors one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_func_ctor_var_param = false # true/false
+
+# Whether to indent continued template parameter list one indent level,
+# rather than aligning parameters under the open parenthesis.
+indent_template_param = false # true/false
+
+# Double the indent for indent_func_xxx_param options.
+# Use both values of the options indent_columns and indent_param.
+indent_func_param_double = false # true/false
+
+# Indentation column for standalone 'const' qualifier on a function
+# prototype.
+indent_func_const = 0 # unsigned number
+
+# Indentation column for standalone 'throw' qualifier on a function
+# prototype.
+indent_func_throw = 0 # unsigned number
+
+# How to indent within a macro followed by a brace on the same line
+# This allows reducing the indent in macros that have (for example)
+# `do { ... } while (0)` blocks bracketing them.
+#
+# true: add an indent for the brace on the same line as the macro
+# false: do not add an indent for the brace on the same line as the macro
+#
+# Default: true
+indent_macro_brace = true # true/false
+
+# The number of spaces to indent a continued '->' or '.'.
+# Usually set to 0, 1, or indent_columns.
+indent_member = 0 # unsigned number
+
+# Whether lines broken at '.' or '->' should be indented by a single indent.
+# The indent_member option will not be effective if this is set to true.
+indent_member_single = false # true/false
+
+# Spaces to indent single line ('//') comments on lines before code.
+indent_single_line_comments_before = 0 # unsigned number
+
+# Spaces to indent single line ('//') comments on lines after code.
+indent_single_line_comments_after = 0 # unsigned number
+
+# When opening a paren for a control statement (if, for, while, etc), increase
+# the indent level by this value. Negative values decrease the indent level.
+indent_sparen_extra = 0 # number
+
+# Whether to indent trailing single line ('//') comments relative to the code
+# instead of trying to keep the same absolute column.
+indent_relative_single_line_comments = false # true/false
+
+# Spaces to indent 'case' from 'switch'. Usually 0 or indent_columns.
+# It might be wise to choose the same value for the option indent_case_brace.
+indent_switch_case = 0 # unsigned number
+
+# Spaces to indent the body of a 'switch' before any 'case'.
+# Usually the same as indent_columns or indent_switch_case.
+indent_switch_body = 0 # unsigned number
+
+# Whether to ignore indent for '{' following 'case'.
+indent_ignore_case_brace = false # true/false
+
+# Spaces to indent '{' from 'case'. By default, the brace will appear under
+# the 'c' in case. Usually set to 0 or indent_columns. Negative values are OK.
+# It might be wise to choose the same value for the option indent_switch_case.
+indent_case_brace = 0 # number
+
+# indent 'break' with 'case' from 'switch'.
+indent_switch_break_with_case = false # true/false
+
+# Whether to indent preprocessor statements inside of switch statements.
+#
+# Default: true
+indent_switch_pp = true # true/false
+
+# Spaces to shift the 'case' line, without affecting any other lines.
+# Usually 0.
+indent_case_shift = 0 # unsigned number
+
+# Whether to align comments before 'case' with the 'case'.
+#
+# Default: true
+indent_case_comment = true # true/false
+
+# Whether to indent comments not found in first column.
+#
+# Default: true
+indent_comment = true # true/false
+
+# Whether to indent comments found in first column.
+indent_col1_comment = false # true/false
+
+# Whether to indent multi string literal in first column.
+indent_col1_multi_string_literal = false # true/false
+
+# Align comments on adjacent lines that are this many columns apart or less.
+#
+# Default: 3
+indent_comment_align_thresh = 3 # unsigned number
+
+# Whether to ignore indent for goto labels.
+indent_ignore_label = false # true/false
+
+# How to indent goto labels. Requires indent_ignore_label=false.
+#
+# >0: Absolute column where 1 is the leftmost column
+# <=0: Subtract from brace indent
+#
+# Default: 1
+indent_label = 1 # number
+
+# How to indent access specifiers that are followed by a
+# colon.
+#
+# >0: Absolute column where 1 is the leftmost column
+# <=0: Subtract from brace indent
+#
+# Default: 1
+indent_access_spec = 1 # number
+
+# Whether to indent the code after an access specifier by one level.
+# If true, this option forces 'indent_access_spec=0'.
+indent_access_spec_body = false # true/false
+
+# If an open parenthesis is followed by a newline, whether to indent the next
+# line so that it lines up after the open parenthesis (not recommended).
+indent_paren_nl = false # true/false
+
+# How to indent a close parenthesis after a newline.
+#
+# 0: Indent to body level (default)
+# 1: Align under the open parenthesis
+# 2: Indent to the brace level
+# -1: Preserve original indentation
+indent_paren_close = 0 # number
+
+# Whether to indent the open parenthesis of a function definition,
+# if the parenthesis is on its own line.
+indent_paren_after_func_def = false # true/false
+
+# Whether to indent the open parenthesis of a function declaration,
+# if the parenthesis is on its own line.
+indent_paren_after_func_decl = false # true/false
+
+# Whether to indent the open parenthesis of a function call,
+# if the parenthesis is on its own line.
+indent_paren_after_func_call = false # true/false
+
+# How to indent a comma when inside braces.
+# 0: Indent by one level (default)
+# 1: Align under the open brace
+# -1: Preserve original indentation
+indent_comma_brace = 0 # number
+
+# How to indent a comma when inside parentheses.
+# 0: Indent by one level (default)
+# 1: Align under the open parenthesis
+# -1: Preserve original indentation
+indent_comma_paren = 0 # number
+
+# How to indent a Boolean operator when inside parentheses.
+# 0: Indent by one level (default)
+# 1: Align under the open parenthesis
+# -1: Preserve original indentation
+indent_bool_paren = 0 # number
+
+# Whether to ignore the indentation of a Boolean operator when outside
+# parentheses.
+indent_ignore_bool = false # true/false
+
+# Whether to ignore the indentation of an arithmetic operator.
+indent_ignore_arith = false # true/false
+
+# Whether to indent a semicolon when inside a for parenthesis.
+# If true, aligns under the open for parenthesis.
+indent_semicolon_for_paren = false # true/false
+
+# Whether to ignore the indentation of a semicolon outside of a 'for'
+# statement.
+indent_ignore_semicolon = false # true/false
+
+# Whether to align the first expression to following ones
+# if indent_bool_paren=1.
+indent_first_bool_expr = false # true/false
+
+# Whether to align the first expression to following ones
+# if indent_semicolon_for_paren=true.
+indent_first_for_expr = false # true/false
+
+# If an open square is followed by a newline, whether to indent the next line
+# so that it lines up after the open square (not recommended).
+indent_square_nl = false # true/false
+
+# (ESQL/C) Whether to preserve the relative indent of 'EXEC SQL' bodies.
+indent_preserve_sql = false # true/false
+
+# Whether to ignore the indentation of an assignment operator.
+indent_ignore_assign = false # true/false
+
+# Whether to align continued statements at the '='. If false or if the '=' is
+# followed by a newline, the next line is indent one tab.
+#
+# Default: true
+indent_align_assign = true # true/false
+
+# If true, the indentation of the chunks after a '=' sequence will be set at
+# LHS token indentation column before '='.
+indent_off_after_assign = false # true/false
+
+# Whether to align continued statements at the '('. If false or the '(' is
+# followed by a newline, the next line indent is one tab.
+#
+# Default: true
+indent_align_paren = true # true/false
+
+# (OC) Whether to indent Objective-C code inside message selectors.
+indent_oc_inside_msg_sel = false # true/false
+
+# (OC) Whether to indent Objective-C blocks at brace level instead of usual
+# rules.
+indent_oc_block = false # true/false
+
+# (OC) Indent for Objective-C blocks in a message relative to the parameter
+# name.
+#
+# =0: Use indent_oc_block rules
+# >0: Use specified number of spaces to indent
+indent_oc_block_msg = 0 # unsigned number
+
+# (OC) Minimum indent for subsequent parameters
+indent_oc_msg_colon = 0 # unsigned number
+
+# (OC) Whether to prioritize aligning with initial colon (and stripping spaces
+# from lines, if necessary).
+#
+# Default: true
+indent_oc_msg_prioritize_first_colon = true # true/false
+
+# (OC) Whether to indent blocks the way that Xcode does by default
+# (from the keyword if the parameter is on its own line; otherwise, from the
+# previous indentation level). Requires indent_oc_block_msg=true.
+indent_oc_block_msg_xcode_style = false # true/false
+
+# (OC) Whether to indent blocks from where the brace is, relative to a
+# message keyword. Requires indent_oc_block_msg=true.
+indent_oc_block_msg_from_keyword = false # true/false
+
+# (OC) Whether to indent blocks from where the brace is, relative to a message
+# colon. Requires indent_oc_block_msg=true.
+indent_oc_block_msg_from_colon = false # true/false
+
+# (OC) Whether to indent blocks from where the block caret is.
+# Requires indent_oc_block_msg=true.
+indent_oc_block_msg_from_caret = false # true/false
+
+# (OC) Whether to indent blocks from where the brace caret is.
+# Requires indent_oc_block_msg=true.
+indent_oc_block_msg_from_brace = false # true/false
+
+# When indenting after virtual brace open and newline add further spaces to
+# reach this minimum indent.
+indent_min_vbrace_open = 0 # unsigned number
+
+# Whether to add further spaces after regular indent to reach next tabstop
+# when indenting after virtual brace open and newline.
+indent_vbrace_open_on_tabstop = false # true/false
+
+# How to indent after a brace followed by another token (not a newline).
+# true: indent all contained lines to match the token
+# false: indent all contained lines to match the brace
+#
+# Default: true
+indent_token_after_brace = true # true/false
+
+# Whether to indent the body of a C++11 lambda.
+indent_cpp_lambda_body = false # true/false
+
+# How to indent compound literals that are being returned.
+# true: add both the indent from return & the compound literal open brace
+# (i.e. 2 indent levels)
+# false: only indent 1 level, don't add the indent for the open brace, only
+# add the indent for the return.
+#
+# Default: true
+indent_compound_literal_return = true # true/false
+
+# (C#) Whether to indent a 'using' block if no braces are used.
+#
+# Default: true
+indent_using_block = true # true/false
+
+# How to indent the continuation of ternary operator.
+#
+# 0: Off (default)
+# 1: When the `if_false` is a continuation, indent it under the `if_true` branch
+# 2: When the `:` is a continuation, indent it under `?`
+indent_ternary_operator = 0 # unsigned number
+
+# Whether to indent the statements inside ternary operator.
+indent_inside_ternary_operator = false # true/false
+
+# If true, the indentation of the chunks after a `return` sequence will be set at return indentation column.
+indent_off_after_return = false # true/false
+
+# If true, the indentation of the chunks after a `return new` sequence will be set at return indentation column.
+indent_off_after_return_new = false # true/false
+
+# If true, the tokens after return are indented with regular single indentation. By default (false) the indentation is after the return token.
+indent_single_after_return = false # true/false
+
+# Whether to ignore indent and alignment for 'asm' blocks (i.e. assume they
+# have their own indentation).
+indent_ignore_asm_block = false # true/false
+
+# Don't indent the close parenthesis of a function definition,
+# if the parenthesis is on its own line.
+donot_indent_func_def_close_paren = false # true/false
+
+#
+# Newline adding and removing options
+#
+
+# Whether to collapse empty blocks between '{' and '}' except for functions.
+# Use nl_collapse_empty_body_functions to specify how empty function braces
+# should be formatted.
+nl_collapse_empty_body = false # true/false
+
+# Whether to collapse empty blocks between '{' and '}' for functions only.
+# If true, overrides nl_inside_empty_func.
+nl_collapse_empty_body_functions = false # true/false
+
+# Don't split one-line braced assignments, as in 'foo_t f = { 1, 2 };'.
+nl_assign_leave_one_liners = false # true/false
+
+# Don't split one-line braced statements inside a 'class xx { }' body.
+nl_class_leave_one_liners = false # true/false
+
+# Don't split one-line enums, as in 'enum foo { BAR = 15 };'
+nl_enum_leave_one_liners = false # true/false
+
+# Don't split one-line get or set functions.
+nl_getset_leave_one_liners = false # true/false
+
+# (C#) Don't split one-line property get or set functions.
+nl_cs_property_leave_one_liners = false # true/false
+
+# Don't split one-line function definitions, as in 'int foo() { return 0; }'.
+# might modify nl_func_type_name
+nl_func_leave_one_liners = false # true/false
+
+# Don't split one-line C++11 lambdas, as in '[]() { return 0; }'.
+nl_cpp_lambda_leave_one_liners = false # true/false
+
+# Don't split one-line if/else statements, as in 'if(...) b++;'.
+nl_if_leave_one_liners = false # true/false
+
+# Don't split one-line while statements, as in 'while(...) b++;'.
+nl_while_leave_one_liners = false # true/false
+
+# Don't split one-line do statements, as in 'do { b++; } while(...);'.
+nl_do_leave_one_liners = false # true/false
+
+# Don't split one-line for statements, as in 'for(...) b++;'.
+nl_for_leave_one_liners = false # true/false
+
+# (OC) Don't split one-line Objective-C messages.
+nl_oc_msg_leave_one_liner = false # true/false
+
+# (OC) Add or remove newline between method declaration and '{'.
+nl_oc_mdef_brace = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove newline between Objective-C block signature and '{'.
+nl_oc_block_brace = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove blank line before '@interface' statement.
+nl_oc_before_interface = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove blank line before '@implementation' statement.
+nl_oc_before_implementation = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove blank line before '@end' statement.
+nl_oc_before_end = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove newline between '@interface' and '{'.
+nl_oc_interface_brace = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove newline between '@implementation' and '{'.
+nl_oc_implementation_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newlines at the start of the file.
+nl_start_of_file = ignore # ignore/add/remove/force/not_defined
+
+# The minimum number of newlines at the start of the file (only used if
+# nl_start_of_file is 'add' or 'force').
+nl_start_of_file_min = 0 # unsigned number
+
+# Add or remove newline at the end of the file.
+nl_end_of_file = ignore # ignore/add/remove/force/not_defined
+
+# The minimum number of newlines at the end of the file (only used if
+# nl_end_of_file is 'add' or 'force').
+nl_end_of_file_min = 0 # unsigned number
+
+# Add or remove newline between '=' and '{'.
+nl_assign_brace = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove newline between '=' and '['.
+nl_assign_square = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between '[]' and '{'.
+nl_tsquare_brace = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove newline after '= ['. Will also affect the newline before
+# the ']'.
+nl_after_square_assign = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between a function call's ')' and '{', as in
+# 'list_for_each(item, &list) { }'.
+nl_fcall_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'enum' and '{'.
+nl_enum_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'enum' and 'class'.
+nl_enum_class = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'enum class' and the identifier.
+nl_enum_class_identifier = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'enum class' type and ':'.
+nl_enum_identifier_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'enum class identifier :' and type.
+nl_enum_colon_type = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'struct and '{'.
+nl_struct_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'union' and '{'.
+nl_union_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'if' and '{'.
+nl_if_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between '}' and 'else'.
+nl_brace_else = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'else if' and '{'. If set to ignore,
+# nl_if_brace is used instead.
+nl_elseif_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'else' and '{'.
+nl_else_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'else' and 'if'.
+nl_else_if = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline before '{' opening brace
+nl_before_opening_brace_func_class_def = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline before 'if'/'else if' closing parenthesis.
+nl_before_if_closing_paren = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between '}' and 'finally'.
+nl_brace_finally = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'finally' and '{'.
+nl_finally_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'try' and '{'.
+nl_try_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between get/set and '{'.
+nl_getset_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'for' and '{'.
+nl_for_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline before the '{' of a 'catch' statement, as in
+# 'catch (decl) <here> {'.
+nl_catch_brace = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove newline before the '{' of a '@catch' statement, as in
+# '@catch (decl) <here> {'. If set to ignore, nl_catch_brace is used.
+nl_oc_catch_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between '}' and 'catch'.
+nl_brace_catch = ignore # ignore/add/remove/force/not_defined
+
+# (OC) Add or remove newline between '}' and '@catch'. If set to ignore,
+# nl_brace_catch is used.
+nl_oc_brace_catch = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between '}' and ']'.
+nl_brace_square = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between '}' and ')' in a function invocation.
+nl_brace_fparen = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'while' and '{'.
+nl_while_brace = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove newline between 'scope (x)' and '{'.
+nl_scope_brace = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove newline between 'unittest' and '{'.
+nl_unittest_brace = ignore # ignore/add/remove/force/not_defined
+
+# (D) Add or remove newline between 'version (x)' and '{'.
+nl_version_brace = ignore # ignore/add/remove/force/not_defined
+
+# (C#) Add or remove newline between 'using' and '{'.
+nl_using_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between two open or close braces. Due to general
+# newline/brace handling, REMOVE may not work.
+nl_brace_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'do' and '{'.
+nl_do_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between '}' and 'while' of 'do' statement.
+nl_brace_while = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'switch' and '{'.
+nl_switch_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'synchronized' and '{'.
+nl_synchronized_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add a newline between ')' and '{' if the ')' is on a different line than the
+# if/for/etc.
+#
+# Overrides nl_for_brace, nl_if_brace, nl_switch_brace, nl_while_switch and
+# nl_catch_brace.
+nl_multi_line_cond = false # true/false
+
+# Add a newline after '(' if an if/for/while/switch condition spans multiple
+# lines
+nl_multi_line_sparen_open = ignore # ignore/add/remove/force/not_defined
+
+# Add a newline before ')' if an if/for/while/switch condition spans multiple
+# lines. Overrides nl_before_if_closing_paren if both are specified.
+nl_multi_line_sparen_close = ignore # ignore/add/remove/force/not_defined
+
+# Force a newline in a define after the macro name for multi-line defines.
+nl_multi_line_define = false # true/false
+
+# Whether to add a newline before 'case', and a blank line before a 'case'
+# statement that follows a ';' or '}'.
+nl_before_case = false # true/false
+
+# Whether to add a newline after a 'case' statement.
+nl_after_case = false # true/false
+
+# Add or remove newline between a case ':' and '{'.
+#
+# Overrides nl_after_case.
+nl_case_colon_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between ')' and 'throw'.
+nl_before_throw = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'namespace' and '{'.
+nl_namespace_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<...>' of a template class.
+nl_template_class = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<...>' of a template class declaration.
+#
+# Overrides nl_template_class.
+nl_template_class_decl = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<>' of a specialized class declaration.
+#
+# Overrides nl_template_class_decl.
+nl_template_class_decl_special = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<...>' of a template class definition.
+#
+# Overrides nl_template_class.
+nl_template_class_def = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<>' of a specialized class definition.
+#
+# Overrides nl_template_class_def.
+nl_template_class_def_special = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<...>' of a template function.
+nl_template_func = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<...>' of a template function
+# declaration.
+#
+# Overrides nl_template_func.
+nl_template_func_decl = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<>' of a specialized function
+# declaration.
+#
+# Overrides nl_template_func_decl.
+nl_template_func_decl_special = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<...>' of a template function
+# definition.
+#
+# Overrides nl_template_func.
+nl_template_func_def = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<>' of a specialized function
+# definition.
+#
+# Overrides nl_template_func_def.
+nl_template_func_def_special = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after 'template<...>' of a template variable.
+nl_template_var = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'template<...>' and 'using' of a templated
+# type alias.
+nl_template_using = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'class' and '{'.
+nl_class_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline before or after (depending on pos_class_comma,
+# may not be IGNORE) each',' in the base class list.
+nl_class_init_args = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after each ',' in the constructor member
+# initialization. Related to nl_constr_colon, pos_constr_colon and
+# pos_constr_comma.
+nl_constr_init_args = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline before first element, after comma, and after last
+# element, in 'enum'.
+nl_enum_own_lines = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between return type and function name in a function
+# definition.
+# might be modified by nl_func_leave_one_liners
+nl_func_type_name = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between return type and function name inside a class
+# definition. If set to ignore, nl_func_type_name or nl_func_proto_type_name
+# is used instead.
+nl_func_type_name_class = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between class specification and '::'
+# in 'void A::f() { }'. Only appears in separate member implementation (does
+# not appear with in-line implementation).
+nl_func_class_scope = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between function scope and name, as in
+# 'void A :: <here> f() { }'.
+nl_func_scope_name = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between return type and function name in a prototype.
+nl_func_proto_type_name = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between a function name and the opening '(' in the
+# declaration.
+nl_func_paren = ignore # ignore/add/remove/force/not_defined
+
+# Overrides nl_func_paren for functions with no parameters.
+nl_func_paren_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between a function name and the opening '(' in the
+# definition.
+nl_func_def_paren = ignore # ignore/add/remove/force/not_defined
+
+# Overrides nl_func_def_paren for functions with no parameters.
+nl_func_def_paren_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between a function name and the opening '(' in the
+# call.
+nl_func_call_paren = ignore # ignore/add/remove/force/not_defined
+
+# Overrides nl_func_call_paren for functions with no parameters.
+nl_func_call_paren_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after '(' in a function declaration.
+nl_func_decl_start = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after '(' in a function definition.
+nl_func_def_start = ignore # ignore/add/remove/force/not_defined
+
+# Overrides nl_func_decl_start when there is only one parameter.
+nl_func_decl_start_single = ignore # ignore/add/remove/force/not_defined
+
+# Overrides nl_func_def_start when there is only one parameter.
+nl_func_def_start_single = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline after '(' in a function declaration if '(' and ')'
+# are in different lines. If false, nl_func_decl_start is used instead.
+nl_func_decl_start_multi_line = false # true/false
+
+# Whether to add a newline after '(' in a function definition if '(' and ')'
+# are in different lines. If false, nl_func_def_start is used instead.
+nl_func_def_start_multi_line = false # true/false
+
+# Add or remove newline after each ',' in a function declaration.
+nl_func_decl_args = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after each ',' in a function definition.
+nl_func_def_args = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline after each ',' in a function call.
+nl_func_call_args = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline after each ',' in a function declaration if '('
+# and ')' are in different lines. If false, nl_func_decl_args is used instead.
+nl_func_decl_args_multi_line = false # true/false
+
+# Whether to add a newline after each ',' in a function definition if '('
+# and ')' are in different lines. If false, nl_func_def_args is used instead.
+nl_func_def_args_multi_line = false # true/false
+
+# Add or remove newline before the ')' in a function declaration.
+nl_func_decl_end = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline before the ')' in a function definition.
+nl_func_def_end = ignore # ignore/add/remove/force/not_defined
+
+# Overrides nl_func_decl_end when there is only one parameter.
+nl_func_decl_end_single = ignore # ignore/add/remove/force/not_defined
+
+# Overrides nl_func_def_end when there is only one parameter.
+nl_func_def_end_single = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline before ')' in a function declaration if '(' and ')'
+# are in different lines. If false, nl_func_decl_end is used instead.
+nl_func_decl_end_multi_line = false # true/false
+
+# Whether to add a newline before ')' in a function definition if '(' and ')'
+# are in different lines. If false, nl_func_def_end is used instead.
+nl_func_def_end_multi_line = false # true/false
+
+# Add or remove newline between '()' in a function declaration.
+nl_func_decl_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between '()' in a function definition.
+nl_func_def_empty = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between '()' in a function call.
+nl_func_call_empty = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline after '(' in a function call,
+# has preference over nl_func_call_start_multi_line.
+nl_func_call_start = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline before ')' in a function call.
+nl_func_call_end = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline after '(' in a function call if '(' and ')' are in
+# different lines.
+nl_func_call_start_multi_line = false # true/false
+
+# Whether to add a newline after each ',' in a function call if '(' and ')'
+# are in different lines.
+nl_func_call_args_multi_line = false # true/false
+
+# Whether to add a newline before ')' in a function call if '(' and ')' are in
+# different lines.
+nl_func_call_end_multi_line = false # true/false
+
+# Whether to respect nl_func_call_XXX option in case of closure args.
+nl_func_call_args_multi_line_ignore_closures = false # true/false
+
+# Whether to add a newline after '<' of a template parameter list.
+nl_template_start = false # true/false
+
+# Whether to add a newline after each ',' in a template parameter list.
+nl_template_args = false # true/false
+
+# Whether to add a newline before '>' of a template parameter list.
+nl_template_end = false # true/false
+
+# (OC) Whether to put each Objective-C message parameter on a separate line.
+# See nl_oc_msg_leave_one_liner.
+nl_oc_msg_args = false # true/false
+
+# (OC) Minimum number of Objective-C message parameters before applying nl_oc_msg_args.
+nl_oc_msg_args_min_params = 0 # unsigned number
+
+# (OC) Max code width of Objective-C message before applying nl_oc_msg_args.
+nl_oc_msg_args_max_code_width = 0 # unsigned number
+
+# Add or remove newline between function signature and '{'.
+nl_fdef_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between function signature and '{',
+# if signature ends with ')'. Overrides nl_fdef_brace.
+nl_fdef_brace_cond = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between C++11 lambda signature and '{'.
+nl_cpp_ldef_brace = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'return' and the return expression.
+nl_return_expr = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline between 'throw' and the throw expression.
+nl_throw_expr = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline after semicolons, except in 'for' statements.
+nl_after_semicolon = false # true/false
+
+# (Java) Add or remove newline between the ')' and '{{' of the double brace
+# initializer.
+nl_paren_dbrace_open = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline after the type in an unnamed temporary
+# direct-list-initialization, better:
+# before a direct-list-initialization.
+nl_type_brace_init_lst = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline after the open brace in an unnamed temporary
+# direct-list-initialization.
+nl_type_brace_init_lst_open = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline before the close brace in an unnamed temporary
+# direct-list-initialization.
+nl_type_brace_init_lst_close = ignore # ignore/add/remove/force/not_defined
+
+# Whether to add a newline before '{'.
+nl_before_brace_open = false # true/false
+
+# Whether to add a newline after '{'.
+nl_after_brace_open = false # true/false
+
+# Whether to add a newline between the open brace and a trailing single-line
+# comment. Requires nl_after_brace_open=true.
+nl_after_brace_open_cmt = false # true/false
+
+# Whether to add a newline after a virtual brace open with a non-empty body.
+# These occur in un-braced if/while/do/for statement bodies.
+nl_after_vbrace_open = false # true/false
+
+# Whether to add a newline after a virtual brace open with an empty body.
+# These occur in un-braced if/while/do/for statement bodies.
+nl_after_vbrace_open_empty = false # true/false
+
+# Whether to add a newline after '}'. Does not apply if followed by a
+# necessary ';'.
+nl_after_brace_close = false # true/false
+
+# Whether to add a newline after a virtual brace close,
+# as in 'if (foo) a++; <here> return;'.
+nl_after_vbrace_close = false # true/false
+
+# Add or remove newline between the close brace and identifier,
+# as in 'struct { int a; } <here> b;'. Affects enumerations, unions and
+# structures. If set to ignore, uses nl_after_brace_close.
+nl_brace_struct_var = ignore # ignore/add/remove/force/not_defined
+
+# Whether to alter newlines in '#define' macros.
+nl_define_macro = false # true/false
+
+# Whether to alter newlines between consecutive parenthesis closes. The number
+# of closing parentheses in a line will depend on respective open parenthesis
+# lines.
+nl_squeeze_paren_close = false # true/false
+
+# Whether to remove blanks after '#ifxx' and '#elxx', or before '#elxx' and
+# '#endif'. Does not affect top-level #ifdefs.
+nl_squeeze_ifdef = false # true/false
+
+# Makes the nl_squeeze_ifdef option affect the top-level #ifdefs as well.
+nl_squeeze_ifdef_top_level = false # true/false
+
+# Add or remove blank line before 'if'.
+nl_before_if = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line after 'if' statement. Add/Force work only if the
+# next token is not a closing brace.
+nl_after_if = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line before 'for'.
+nl_before_for = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line after 'for' statement.
+nl_after_for = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line before 'while'.
+nl_before_while = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line after 'while' statement.
+nl_after_while = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line before 'switch'.
+nl_before_switch = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line after 'switch' statement.
+nl_after_switch = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line before 'synchronized'.
+nl_before_synchronized = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line after 'synchronized' statement.
+nl_after_synchronized = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line before 'do'.
+nl_before_do = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove blank line after 'do/while' statement.
+nl_after_do = ignore # ignore/add/remove/force/not_defined
+
+# Ignore nl_before_{if,for,switch,do,synchronized} if the control
+# statement is immediately after a case statement.
+# if nl_before_{if,for,switch,do} is set to remove, this option
+# does nothing.
+nl_before_ignore_after_case = false # true/false
+
+# Whether to put a blank line before 'return' statements, unless after an open
+# brace.
+nl_before_return = false # true/false
+
+# Whether to put a blank line after 'return' statements, unless followed by a
+# close brace.
+nl_after_return = false # true/false
+
+# Whether to put a blank line before a member '.' or '->' operators.
+nl_before_member = ignore # ignore/add/remove/force/not_defined
+
+# (Java) Whether to put a blank line after a member '.' or '->' operators.
+nl_after_member = ignore # ignore/add/remove/force/not_defined
+
+# Whether to double-space commented-entries in 'struct'/'union'/'enum'.
+nl_ds_struct_enum_cmt = false # true/false
+
+# Whether to force a newline before '}' of a 'struct'/'union'/'enum'.
+# (Lower priority than eat_blanks_before_close_brace.)
+nl_ds_struct_enum_close_brace = false # true/false
+
+# Add or remove newline before or after (depending on pos_class_colon) a class
+# colon, as in 'class Foo <here> : <or here> public Bar'.
+nl_class_colon = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove newline around a class constructor colon. The exact position
+# depends on nl_constr_init_args, pos_constr_colon and pos_constr_comma.
+nl_constr_colon = ignore # ignore/add/remove/force/not_defined
+
+# Whether to collapse a two-line namespace, like 'namespace foo\n{ decl; }'
+# into a single line. If true, prevents other brace newline rules from turning
+# such code into four lines. If true, it also preserves one-liner namespaces.
+nl_namespace_two_to_one_liner = false # true/false
+
+# Whether to remove a newline in simple unbraced if statements, turning them
+# into one-liners, as in 'if(b)\n i++;' => 'if(b) i++;'.
+nl_create_if_one_liner = false # true/false
+
+# Whether to remove a newline in simple unbraced for statements, turning them
+# into one-liners, as in 'for (...)\n stmt;' => 'for (...) stmt;'.
+nl_create_for_one_liner = false # true/false
+
+# Whether to remove a newline in simple unbraced while statements, turning
+# them into one-liners, as in 'while (expr)\n stmt;' => 'while (expr) stmt;'.
+nl_create_while_one_liner = false # true/false
+
+# Whether to collapse a function definition whose body (not counting braces)
+# is only one line so that the entire definition (prototype, braces, body) is
+# a single line.
+nl_create_func_def_one_liner = false # true/false
+
+# Whether to split one-line simple list definitions into three lines by
+# adding newlines, as in 'int a[12] = { <here> 0 <here> };'.
+nl_create_list_one_liner = false # true/false
+
+# Whether to split one-line simple unbraced if statements into two lines by
+# adding a newline, as in 'if(b) <here> i++;'.
+nl_split_if_one_liner = false # true/false
+
+# Whether to split one-line simple unbraced for statements into two lines by
+# adding a newline, as in 'for (...) <here> stmt;'.
+nl_split_for_one_liner = false # true/false
+
+# Whether to split one-line simple unbraced while statements into two lines by
+# adding a newline, as in 'while (expr) <here> stmt;'.
+nl_split_while_one_liner = false # true/false
+
+# Don't add a newline before a cpp-comment in a parameter list of a function
+# call.
+donot_add_nl_before_cpp_comment = false # true/false
+
+#
+# Blank line options
+#
+
+# The maximum number of consecutive newlines (3 = 2 blank lines).
+nl_max = 0 # unsigned number
+
+# The maximum number of consecutive newlines in a function.
+nl_max_blank_in_func = 0 # unsigned number
+
+# The number of newlines inside an empty function body.
+# This option overrides eat_blanks_after_open_brace and
+# eat_blanks_before_close_brace, but is ignored when
+# nl_collapse_empty_body_functions=true
+nl_inside_empty_func = 0 # unsigned number
+
+# The number of newlines before a function prototype.
+nl_before_func_body_proto = 0 # unsigned number
+
+# The number of newlines before a multi-line function definition. Where
+# applicable, this option is overridden with eat_blanks_after_open_brace=true
+nl_before_func_body_def = 0 # unsigned number
+
+# The number of newlines before a class constructor/destructor prototype.
+nl_before_func_class_proto = 0 # unsigned number
+
+# The number of newlines before a class constructor/destructor definition.
+nl_before_func_class_def = 0 # unsigned number
+
+# The number of newlines after a function prototype.
+nl_after_func_proto = 0 # unsigned number
+
+# The number of newlines after a function prototype, if not followed by
+# another function prototype.
+nl_after_func_proto_group = 0 # unsigned number
+
+# The number of newlines after a class constructor/destructor prototype.
+nl_after_func_class_proto = 0 # unsigned number
+
+# The number of newlines after a class constructor/destructor prototype,
+# if not followed by another constructor/destructor prototype.
+nl_after_func_class_proto_group = 0 # unsigned number
+
+# Whether one-line method definitions inside a class body should be treated
+# as if they were prototypes for the purposes of adding newlines.
+#
+# Requires nl_class_leave_one_liners=true. Overrides nl_before_func_body_def
+# and nl_before_func_class_def for one-liners.
+nl_class_leave_one_liner_groups = false # true/false
+
+# The number of newlines after '}' of a multi-line function body.
+#
+# Overrides nl_min_after_func_body and nl_max_after_func_body.
+nl_after_func_body = 0 # unsigned number
+
+# The minimum number of newlines after '}' of a multi-line function body.
+#
+# Only works when nl_after_func_body is 0.
+nl_min_after_func_body = 0 # unsigned number
+
+# The maximum number of newlines after '}' of a multi-line function body.
+#
+# Only works when nl_after_func_body is 0.
+# Takes precedence over nl_min_after_func_body.
+nl_max_after_func_body = 0 # unsigned number
+
+# The number of newlines after '}' of a multi-line function body in a class
+# declaration. Also affects class constructors/destructors.
+#
+# Overrides nl_after_func_body.
+nl_after_func_body_class = 0 # unsigned number
+
+# The number of newlines after '}' of a single line function body. Also
+# affects class constructors/destructors.
+#
+# Overrides nl_after_func_body and nl_after_func_body_class.
+nl_after_func_body_one_liner = 0 # unsigned number
+
+# The number of newlines before a block of typedefs. If nl_after_access_spec
+# is non-zero, that option takes precedence.
+#
+# 0: No change (default).
+nl_typedef_blk_start = 0 # unsigned number
+
+# The number of newlines after a block of typedefs.
+#
+# 0: No change (default).
+nl_typedef_blk_end = 0 # unsigned number
+
+# The maximum number of consecutive newlines within a block of typedefs.
+#
+# 0: No change (default).
+nl_typedef_blk_in = 0 # unsigned number
+
+# The minimum number of blank lines after a block of variable definitions
+# at the top of a function body. If any preprocessor directives appear
+# between the opening brace of the function and the variable block, then
+# it is considered as not at the top of the function.Newlines are added
+# before trailing preprocessor directives, if any exist.
+#
+# 0: No change (default).
+nl_var_def_blk_end_func_top = 0 # unsigned number
+
+# The minimum number of empty newlines before a block of variable definitions
+# not at the top of a function body. If nl_after_access_spec is non-zero,
+# that option takes precedence. Newlines are not added at the top of the
+# file or just after an opening brace. Newlines are added above any
+# preprocessor directives before the block.
+#
+# 0: No change (default).
+nl_var_def_blk_start = 0 # unsigned number
+
+# The minimum number of empty newlines after a block of variable definitions
+# not at the top of a function body. Newlines are not added if the block
+# is at the bottom of the file or just before a preprocessor directive.
+#
+# 0: No change (default).
+nl_var_def_blk_end = 0 # unsigned number
+
+# The maximum number of consecutive newlines within a block of variable
+# definitions.
+#
+# 0: No change (default).
+nl_var_def_blk_in = 0 # unsigned number
+
+# The minimum number of newlines before a multi-line comment.
+# Doesn't apply if after a brace open or another multi-line comment.
+nl_before_block_comment = 0 # unsigned number
+
+# The minimum number of newlines before a single-line C comment.
+# Doesn't apply if after a brace open or other single-line C comments.
+nl_before_c_comment = 0 # unsigned number
+
+# The minimum number of newlines before a CPP comment.
+# Doesn't apply if after a brace open or other CPP comments.
+nl_before_cpp_comment = 0 # unsigned number
+
+# Whether to force a newline after a multi-line comment.
+nl_after_multiline_comment = false # true/false
+
+# Whether to force a newline after a label's colon.
+nl_after_label_colon = false # true/false
+
+# The number of newlines before a struct definition.
+nl_before_struct = 0 # unsigned number
+
+# The number of newlines after '}' or ';' of a struct/enum/union definition.
+nl_after_struct = 0 # unsigned number
+
+# The number of newlines before a class definition.
+nl_before_class = 0 # unsigned number
+
+# The number of newlines after '}' or ';' of a class definition.
+nl_after_class = 0 # unsigned number
+
+# The number of newlines before a namespace.
+nl_before_namespace = 0 # unsigned number
+
+# The number of newlines after '{' of a namespace. This also adds newlines
+# before the matching '}'.
+#
+# 0: Apply eat_blanks_after_open_brace or eat_blanks_before_close_brace if
+# applicable, otherwise no change.
+#
+# Overrides eat_blanks_after_open_brace and eat_blanks_before_close_brace.
+nl_inside_namespace = 0 # unsigned number
+
+# The number of newlines after '}' of a namespace.
+nl_after_namespace = 0 # unsigned number
+
+# The number of newlines before an access specifier label. This also includes
+# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count
+# if after a brace open.
+#
+# 0: No change (default).
+nl_before_access_spec = 0 # unsigned number
+
+# The number of newlines after an access specifier label. This also includes
+# the Qt-specific 'signals:' and 'slots:'. Will not change the newline count
+# if after a brace open.
+#
+# 0: No change (default).
+#
+# Overrides nl_typedef_blk_start and nl_var_def_blk_start.
+nl_after_access_spec = 0 # unsigned number
+
+# The number of newlines between a function definition and the function
+# comment, as in '// comment\n <here> void foo() {...}'.
+#
+# 0: No change (default).
+nl_comment_func_def = 0 # unsigned number
+
+# The number of newlines after a try-catch-finally block that isn't followed
+# by a brace close.
+#
+# 0: No change (default).
+nl_after_try_catch_finally = 0 # unsigned number
+
+# (C#) The number of newlines before and after a property, indexer or event
+# declaration.
+#
+# 0: No change (default).
+nl_around_cs_property = 0 # unsigned number
+
+# (C#) The number of newlines between the get/set/add/remove handlers.
+#
+# 0: No change (default).
+nl_between_get_set = 0 # unsigned number
+
+# (C#) Add or remove newline between property and the '{'.
+nl_property_brace = ignore # ignore/add/remove/force/not_defined
+
+# Whether to remove blank lines after '{'.
+eat_blanks_after_open_brace = false # true/false
+
+# Whether to remove blank lines before '}'.
+eat_blanks_before_close_brace = false # true/false
+
+# How aggressively to remove extra newlines not in preprocessor.
+#
+# 0: No change (default)
+# 1: Remove most newlines not handled by other config
+# 2: Remove all newlines and reformat completely by config
+nl_remove_extra_newlines = 0 # unsigned number
+
+# (Java) Add or remove newline after an annotation statement. Only affects
+# annotations that are after a newline.
+nl_after_annotation = ignore # ignore/add/remove/force/not_defined
+
+# (Java) Add or remove newline between two annotations.
+nl_between_annotation = ignore # ignore/add/remove/force/not_defined
+
+# The number of newlines before a whole-file #ifdef.
+#
+# 0: No change (default).
+nl_before_whole_file_ifdef = 0 # unsigned number
+
+# The number of newlines after a whole-file #ifdef.
+#
+# 0: No change (default).
+nl_after_whole_file_ifdef = 0 # unsigned number
+
+# The number of newlines before a whole-file #endif.
+#
+# 0: No change (default).
+nl_before_whole_file_endif = 0 # unsigned number
+
+# The number of newlines after a whole-file #endif.
+#
+# 0: No change (default).
+nl_after_whole_file_endif = 0 # unsigned number
+
+#
+# Positioning options
+#
+
+# The position of arithmetic operators in wrapped expressions.
+pos_arith = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of assignment in wrapped expressions. Do not affect '='
+# followed by '{'.
+pos_assign = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of Boolean operators in wrapped expressions.
+pos_bool = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of comparison operators in wrapped expressions.
+pos_compare = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of conditional operators, as in the '?' and ':' of
+# 'expr ? stmt : stmt', in wrapped expressions.
+pos_conditional = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of the comma in wrapped expressions.
+pos_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of the comma in enum entries.
+pos_enum_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of the comma in the base class list if there is more than one
+# line. Affects nl_class_init_args.
+pos_class_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of the comma in the constructor initialization list.
+# Related to nl_constr_colon, nl_constr_init_args and pos_constr_colon.
+pos_constr_comma = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of trailing/leading class colon, between class and base class
+# list. Affects nl_class_colon.
+pos_class_colon = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of colons between constructor and member initialization.
+# Related to nl_constr_colon, nl_constr_init_args and pos_constr_comma.
+pos_constr_colon = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+# The position of shift operators in wrapped expressions.
+pos_shift = ignore # ignore/break/force/lead/trail/join/lead_break/lead_force/trail_break/trail_force
+
+#
+# Line splitting options
+#
+
+# Try to limit code width to N columns.
+code_width = 100 # unsigned number
+
+# Whether to fully split long 'for' statements at semi-colons.
+ls_for_split_full = false # true/false
+
+# Whether to fully split long function prototypes/calls at commas.
+# The option ls_code_width has priority over the option ls_func_split_full.
+ls_func_split_full = false # true/false
+
+# Whether to split lines as close to code_width as possible and ignore some
+# groupings.
+# The option ls_code_width has priority over the option ls_func_split_full.
+ls_code_width = false # true/false
+
+#
+# Code alignment options (not left column spaces/tabs)
+#
+
+# Whether to keep non-indenting tabs.
+align_keep_tabs = false # true/false
+
+# Whether to use tabs for aligning.
+align_with_tabs = false # true/false
+
+# Whether to bump out to the next tab when aligning.
+align_on_tabstop = false # true/false
+
+# Whether to right-align numbers.
+align_number_right = false # true/false
+
+# Whether to keep whitespace not required for alignment.
+align_keep_extra_space = false # true/false
+
+# Whether to align variable definitions in prototypes and functions.
+align_func_params = false # true/false
+
+# The span for aligning parameter definitions in function on parameter name.
+#
+# 0: Don't align (default).
+align_func_params_span = 0 # unsigned number
+
+# The threshold for aligning function parameter definitions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_func_params_thresh = 0 # number
+
+# The gap for aligning function parameter definitions.
+align_func_params_gap = 0 # unsigned number
+
+# The span for aligning constructor value.
+#
+# 0: Don't align (default).
+align_constr_value_span = 0 # unsigned number
+
+# The threshold for aligning constructor value.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_constr_value_thresh = 0 # number
+
+# The gap for aligning constructor value.
+align_constr_value_gap = 0 # unsigned number
+
+# Whether to align parameters in single-line functions that have the same
+# name. The function names must already be aligned with each other.
+align_same_func_call_params = false # true/false
+
+# The span for aligning function-call parameters for single line functions.
+#
+# 0: Don't align (default).
+align_same_func_call_params_span = 0 # unsigned number
+
+# The threshold for aligning function-call parameters for single line
+# functions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_same_func_call_params_thresh = 0 # number
+
+# The span for aligning variable definitions.
+#
+# 0: Don't align (default).
+align_var_def_span = 0 # unsigned number
+
+# How to consider (or treat) the '*' in the alignment of variable definitions.
+#
+# 0: Part of the type 'void * foo;' (default)
+# 1: Part of the variable 'void *foo;'
+# 2: Dangling 'void *foo;'
+# Dangling: the '*' will not be taken into account when aligning.
+align_var_def_star_style = 0 # unsigned number
+
+# How to consider (or treat) the '&' in the alignment of variable definitions.
+#
+# 0: Part of the type 'long & foo;' (default)
+# 1: Part of the variable 'long &foo;'
+# 2: Dangling 'long &foo;'
+# Dangling: the '&' will not be taken into account when aligning.
+align_var_def_amp_style = 0 # unsigned number
+
+# The threshold for aligning variable definitions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_var_def_thresh = 0 # number
+
+# The gap for aligning variable definitions.
+align_var_def_gap = 0 # unsigned number
+
+# Whether to align the colon in struct bit fields.
+align_var_def_colon = false # true/false
+
+# The gap for aligning the colon in struct bit fields.
+align_var_def_colon_gap = 0 # unsigned number
+
+# Whether to align any attribute after the variable name.
+align_var_def_attribute = false # true/false
+
+# Whether to align inline struct/enum/union variable definitions.
+align_var_def_inline = false # true/false
+
+# The span for aligning on '=' in assignments.
+#
+# 0: Don't align (default).
+align_assign_span = 0 # unsigned number
+
+# The span for aligning on '=' in function prototype modifier.
+#
+# 0: Don't align (default).
+align_assign_func_proto_span = 0 # unsigned number
+
+# The threshold for aligning on '=' in assignments.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_assign_thresh = 0 # number
+
+# Whether to align on the left most assignment when multiple
+# definitions are found on the same line.
+# Depends on 'align_assign_span' and 'align_assign_thresh' settings.
+align_assign_on_multi_var_defs = false # true/false
+
+# The span for aligning on '{' in braced init list.
+#
+# 0: Don't align (default).
+align_braced_init_list_span = 0 # unsigned number
+
+# The threshold for aligning on '{' in braced init list.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_braced_init_list_thresh = 0 # number
+
+# How to apply align_assign_span to function declaration "assignments", i.e.
+# 'virtual void foo() = 0' or '~foo() = {default|delete}'.
+#
+# 0: Align with other assignments (default)
+# 1: Align with each other, ignoring regular assignments
+# 2: Don't align
+align_assign_decl_func = 0 # unsigned number
+
+# The span for aligning on '=' in enums.
+#
+# 0: Don't align (default).
+align_enum_equ_span = 0 # unsigned number
+
+# The threshold for aligning on '=' in enums.
+# Use a negative number for absolute thresholds.
+#
+# 0: no limit (default).
+align_enum_equ_thresh = 0 # number
+
+# The span for aligning class member definitions.
+#
+# 0: Don't align (default).
+align_var_class_span = 0 # unsigned number
+
+# The threshold for aligning class member definitions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_var_class_thresh = 0 # number
+
+# The gap for aligning class member definitions.
+align_var_class_gap = 0 # unsigned number
+
+# The span for aligning struct/union member definitions.
+#
+# 0: Don't align (default).
+align_var_struct_span = 0 # unsigned number
+
+# The threshold for aligning struct/union member definitions.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_var_struct_thresh = 0 # number
+
+# The gap for aligning struct/union member definitions.
+align_var_struct_gap = 0 # unsigned number
+
+# The span for aligning struct initializer values.
+#
+# 0: Don't align (default).
+align_struct_init_span = 0 # unsigned number
+
+# The span for aligning single-line typedefs.
+#
+# 0: Don't align (default).
+align_typedef_span = 0 # unsigned number
+
+# The minimum space between the type and the synonym of a typedef.
+align_typedef_gap = 0 # unsigned number
+
+# How to align typedef'd functions with other typedefs.
+#
+# 0: Don't mix them at all (default)
+# 1: Align the open parenthesis with the types
+# 2: Align the function type name with the other type names
+align_typedef_func = 0 # unsigned number
+
+# How to consider (or treat) the '*' in the alignment of typedefs.
+#
+# 0: Part of the typedef type, 'typedef int * pint;' (default)
+# 1: Part of type name: 'typedef int *pint;'
+# 2: Dangling: 'typedef int *pint;'
+# Dangling: the '*' will not be taken into account when aligning.
+align_typedef_star_style = 0 # unsigned number
+
+# How to consider (or treat) the '&' in the alignment of typedefs.
+#
+# 0: Part of the typedef type, 'typedef int & intref;' (default)
+# 1: Part of type name: 'typedef int &intref;'
+# 2: Dangling: 'typedef int &intref;'
+# Dangling: the '&' will not be taken into account when aligning.
+align_typedef_amp_style = 0 # unsigned number
+
+# The span for aligning comments that end lines.
+#
+# 0: Don't align (default).
+align_right_cmt_span = 0 # unsigned number
+
+# Minimum number of columns between preceding text and a trailing comment in
+# order for the comment to qualify for being aligned. Must be non-zero to have
+# an effect.
+align_right_cmt_gap = 0 # unsigned number
+
+# If aligning comments, whether to mix with comments after '}' and #endif with
+# less than three spaces before the comment.
+align_right_cmt_mix = false # true/false
+
+# Whether to only align trailing comments that are at the same brace level.
+align_right_cmt_same_level = false # true/false
+
+# Minimum column at which to align trailing comments. Comments which are
+# aligned beyond this column, but which can be aligned in a lesser column,
+# may be "pulled in".
+#
+# 0: Ignore (default).
+align_right_cmt_at_col = 0 # unsigned number
+
+# The span for aligning function prototypes.
+#
+# 0: Don't align (default).
+align_func_proto_span = 0 # unsigned number
+
+# Whether to ignore continuation lines when evaluating the number of
+# new lines for the function prototype alignment's span.
+#
+# false: continuation lines are part of the newlines count
+# true: continuation lines are not counted
+align_func_proto_span_ignore_cont_lines = false # true/false
+
+# How to consider (or treat) the '*' in the alignment of function prototypes.
+#
+# 0: Part of the type 'void * foo();' (default)
+# 1: Part of the function 'void *foo();'
+# 2: Dangling 'void *foo();'
+# Dangling: the '*' will not be taken into account when aligning.
+align_func_proto_star_style = 0 # unsigned number
+
+# How to consider (or treat) the '&' in the alignment of function prototypes.
+#
+# 0: Part of the type 'long & foo();' (default)
+# 1: Part of the function 'long &foo();'
+# 2: Dangling 'long &foo();'
+# Dangling: the '&' will not be taken into account when aligning.
+align_func_proto_amp_style = 0 # unsigned number
+
+# The threshold for aligning function prototypes.
+# Use a negative number for absolute thresholds.
+#
+# 0: No limit (default).
+align_func_proto_thresh = 0 # number
+
+# Minimum gap between the return type and the function name.
+align_func_proto_gap = 0 # unsigned number
+
+# Whether to align function prototypes on the 'operator' keyword instead of
+# what follows.
+align_on_operator = false # true/false
+
+# Whether to mix aligning prototype and variable declarations. If true,
+# align_var_def_XXX options are used instead of align_func_proto_XXX options.
+align_mix_var_proto = false # true/false
+
+# Whether to align single-line functions with function prototypes.
+# Uses align_func_proto_span.
+align_single_line_func = false # true/false
+
+# Whether to align the open brace of single-line functions.
+# Requires align_single_line_func=true. Uses align_func_proto_span.
+align_single_line_brace = false # true/false
+
+# Gap for align_single_line_brace.
+align_single_line_brace_gap = 0 # unsigned number
+
+# (OC) The span for aligning Objective-C message specifications.
+#
+# 0: Don't align (default).
+align_oc_msg_spec_span = 0 # unsigned number
+
+# Whether and how to align backslashes that split a macro onto multiple lines.
+# This will not work right if the macro contains a multi-line comment.
+#
+# 0: Do nothing (default)
+# 1: Align the backslashes in the column at the end of the longest line
+# 2: Align with the backslash that is farthest to the left, or, if that
+# backslash is farther left than the end of the longest line, at the end of
+# the longest line
+# 3: Align with the backslash that is farthest to the right
+align_nl_cont = 1 # unsigned number
+
+# The minimum number of spaces between the end of a line and its continuation
+# backslash. Requires align_nl_cont.
+#
+# Default: 1
+align_nl_cont_spaces = 1 # unsigned number
+
+# Whether to align macro functions and variables together.
+align_pp_define_together = false # true/false
+
+# The span for aligning on '#define' bodies.
+#
+# =0: Don't align (default)
+# >0: Number of lines (including comments) between blocks
+align_pp_define_span = 0 # unsigned number
+
+# The minimum space between label and value of a preprocessor define.
+align_pp_define_gap = 0 # unsigned number
+
+# Whether to align lines that start with '<<' with previous '<<'.
+#
+# Default: true
+align_left_shift = true # true/false
+
+# Whether to align comma-separated statements following '<<' (as used to
+# initialize Eigen matrices).
+align_eigen_comma_init = false # true/false
+
+# Whether to align text after 'asm volatile ()' colons.
+align_asm_colon = false # true/false
+
+# (OC) Span for aligning parameters in an Objective-C message call
+# on the ':'.
+#
+# 0: Don't align.
+align_oc_msg_colon_span = 0 # unsigned number
+
+# (OC) Whether to always align with the first parameter, even if it is too
+# short.
+align_oc_msg_colon_first = false # true/false
+
+# (OC) Whether to align parameters in an Objective-C '+' or '-' declaration
+# on the ':'.
+align_oc_decl_colon = false # true/false
+
+# (OC) Whether to not align parameters in an Objectve-C message call if first
+# colon is not on next line of the message call (the same way Xcode does
+# alignment)
+align_oc_msg_colon_xcode_like = false # true/false
+
+#
+# Comment modification options
+#
+
+# Try to wrap comments at N columns.
+cmt_width = 0 # unsigned number
+
+# How to reflow comments.
+#
+# 0: No reflowing (apart from the line wrapping due to cmt_width) (default)
+# 1: No touching at all
+# 2: Full reflow (enable cmt_indent_multi for indent with line wrapping due to cmt_width)
+cmt_reflow_mode = 0 # unsigned number
+
+# Path to a file that contains regular expressions describing patterns for
+# which the end of one line and the beginning of the next will be folded into
+# the same sentence or paragraph during full comment reflow. The regular
+# expressions are described using ECMAScript syntax. The syntax for this
+# specification is as follows, where "..." indicates the custom regular
+# expression and "n" indicates the nth end_of_prev_line_regex and
+# beg_of_next_line_regex regular expression pair:
+#
+# end_of_prev_line_regex[1] = "...$"
+# beg_of_next_line_regex[1] = "^..."
+# end_of_prev_line_regex[2] = "...$"
+# beg_of_next_line_regex[2] = "^..."
+# .
+# .
+# .
+# end_of_prev_line_regex[n] = "...$"
+# beg_of_next_line_regex[n] = "^..."
+#
+# Note that use of this option overrides the default reflow fold regular
+# expressions, which are internally defined as follows:
+#
+# end_of_prev_line_regex[1] = "[\w,\]\)]$"
+# beg_of_next_line_regex[1] = "^[\w,\[\(]"
+# end_of_prev_line_regex[2] = "\.$"
+# beg_of_next_line_regex[2] = "^[A-Z]"
+cmt_reflow_fold_regex_file = "" # string
+
+# Whether to indent wrapped lines to the start of the encompassing paragraph
+# during full comment reflow (cmt_reflow_mode = 2). Overrides the value
+# specified by cmt_sp_after_star_cont.
+#
+# Note that cmt_align_doxygen_javadoc_tags overrides this option for
+# paragraphs associated with javadoc tags
+cmt_reflow_indent_to_paragraph_start = false # true/false
+
+# Whether to convert all tabs to spaces in comments. If false, tabs in
+# comments are left alone, unless used for indenting.
+cmt_convert_tab_to_spaces = false # true/false
+
+# Whether to apply changes to multi-line comments, including cmt_width,
+# keyword substitution and leading chars.
+#
+# Default: true
+cmt_indent_multi = true # true/false
+
+# Whether to align doxygen javadoc-style tags ('@param', '@return', etc.)
+# and corresponding fields such that groups of consecutive block tags,
+# parameter names, and descriptions align with one another. Overrides that
+# which is specified by the cmt_sp_after_star_cont. If cmt_width > 0, it may
+# be necessary to enable cmt_indent_multi and set cmt_reflow_mode = 2
+# in order to achieve the desired alignment for line-wrapping.
+cmt_align_doxygen_javadoc_tags = false # true/false
+
+# The number of spaces to insert after the star and before doxygen
+# javadoc-style tags (@param, @return, etc). Requires enabling
+# cmt_align_doxygen_javadoc_tags. Overrides that which is specified by the
+# cmt_sp_after_star_cont.
+#
+# Default: 1
+cmt_sp_before_doxygen_javadoc_tags = 1 # unsigned number
+
+# Whether to change trailing, single-line c-comments into cpp-comments.
+cmt_trailing_single_line_c_to_cpp = false # true/false
+
+# Whether to group c-comments that look like they are in a block.
+cmt_c_group = false # true/false
+
+# Whether to put an empty '/*' on the first line of the combined c-comment.
+cmt_c_nl_start = false # true/false
+
+# Whether to add a newline before the closing '*/' of the combined c-comment.
+cmt_c_nl_end = false # true/false
+
+# Whether to change cpp-comments into c-comments.
+cmt_cpp_to_c = false # true/false
+
+# Whether to group cpp-comments that look like they are in a block. Only
+# meaningful if cmt_cpp_to_c=true.
+cmt_cpp_group = false # true/false
+
+# Whether to put an empty '/*' on the first line of the combined cpp-comment
+# when converting to a c-comment.
+#
+# Requires cmt_cpp_to_c=true and cmt_cpp_group=true.
+cmt_cpp_nl_start = false # true/false
+
+# Whether to add a newline before the closing '*/' of the combined cpp-comment
+# when converting to a c-comment.
+#
+# Requires cmt_cpp_to_c=true and cmt_cpp_group=true.
+cmt_cpp_nl_end = false # true/false
+
+# Whether to put a star on subsequent comment lines.
+cmt_star_cont = false # true/false
+
+# The number of spaces to insert at the start of subsequent comment lines.
+cmt_sp_before_star_cont = 0 # unsigned number
+
+# The number of spaces to insert after the star on subsequent comment lines.
+cmt_sp_after_star_cont = 0 # unsigned number
+
+# For multi-line comments with a '*' lead, remove leading spaces if the first
+# and last lines of the comment are the same length.
+#
+# Default: true
+cmt_multi_check_last = true # true/false
+
+# For multi-line comments with a '*' lead, remove leading spaces if the first
+# and last lines of the comment are the same length AND if the length is
+# bigger as the first_len minimum.
+#
+# Default: 4
+cmt_multi_first_len_minimum = 4 # unsigned number
+
+# Path to a file that contains text to insert at the beginning of a file if
+# the file doesn't start with a C/C++ comment. If the inserted text contains
+# '$(filename)', that will be replaced with the current file's name.
+cmt_insert_file_header = "" # string
+
+# Path to a file that contains text to insert at the end of a file if the
+# file doesn't end with a C/C++ comment. If the inserted text contains
+# '$(filename)', that will be replaced with the current file's name.
+cmt_insert_file_footer = "" # string
+
+# Path to a file that contains text to insert before a function definition if
+# the function isn't preceded by a C/C++ comment. If the inserted text
+# contains '$(function)', '$(javaparam)' or '$(fclass)', these will be
+# replaced with, respectively, the name of the function, the javadoc '@param'
+# and '@return' stuff, or the name of the class to which the member function
+# belongs.
+cmt_insert_func_header = "" # string
+
+# Path to a file that contains text to insert before a class if the class
+# isn't preceded by a C/C++ comment. If the inserted text contains '$(class)',
+# that will be replaced with the class name.
+cmt_insert_class_header = "" # string
+
+# Path to a file that contains text to insert before an Objective-C message
+# specification, if the method isn't preceded by a C/C++ comment. If the
+# inserted text contains '$(message)' or '$(javaparam)', these will be
+# replaced with, respectively, the name of the function, or the javadoc
+# '@param' and '@return' stuff.
+cmt_insert_oc_msg_header = "" # string
+
+# Whether a comment should be inserted if a preprocessor is encountered when
+# stepping backwards from a function name.
+#
+# Applies to cmt_insert_oc_msg_header, cmt_insert_func_header and
+# cmt_insert_class_header.
+cmt_insert_before_preproc = false # true/false
+
+# Whether a comment should be inserted if a function is declared inline to a
+# class definition.
+#
+# Applies to cmt_insert_func_header.
+#
+# Default: true
+cmt_insert_before_inlines = true # true/false
+
+# Whether a comment should be inserted if the function is a class constructor
+# or destructor.
+#
+# Applies to cmt_insert_func_header.
+cmt_insert_before_ctor_dtor = false # true/false
+
+#
+# Code modifying options (non-whitespace)
+#
+
+# Add or remove braces on a single-line 'do' statement.
+mod_full_brace_do = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove braces on a single-line 'for' statement.
+mod_full_brace_for = ignore # ignore/add/remove/force/not_defined
+
+# (Pawn) Add or remove braces on a single-line function definition.
+mod_full_brace_function = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove braces on a single-line 'if' statement. Braces will not be
+# removed if the braced statement contains an 'else'.
+mod_full_brace_if = ignore # ignore/add/remove/force/not_defined
+
+# Whether to enforce that all blocks of an 'if'/'else if'/'else' chain either
+# have, or do not have, braces. Overrides mod_full_brace_if.
+#
+# 0: Don't override mod_full_brace_if
+# 1: Add braces to all blocks if any block needs braces and remove braces if
+# they can be removed from all blocks
+# 2: Add braces to all blocks if any block already has braces, regardless of
+# whether it needs them
+# 3: Add braces to all blocks if any block needs braces and remove braces if
+# they can be removed from all blocks, except if all blocks have braces
+# despite none needing them
+mod_full_brace_if_chain = 0 # unsigned number
+
+# Whether to add braces to all blocks of an 'if'/'else if'/'else' chain.
+# If true, mod_full_brace_if_chain will only remove braces from an 'if' that
+# does not have an 'else if' or 'else'.
+mod_full_brace_if_chain_only = false # true/false
+
+# Add or remove braces on single-line 'while' statement.
+mod_full_brace_while = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove braces on single-line 'using ()' statement.
+mod_full_brace_using = ignore # ignore/add/remove/force/not_defined
+
+# Don't remove braces around statements that span N newlines
+mod_full_brace_nl = 0 # unsigned number
+
+# Whether to prevent removal of braces from 'if'/'for'/'while'/etc. blocks
+# which span multiple lines.
+#
+# Affects:
+# mod_full_brace_for
+# mod_full_brace_if
+# mod_full_brace_if_chain
+# mod_full_brace_if_chain_only
+# mod_full_brace_while
+# mod_full_brace_using
+#
+# Does not affect:
+# mod_full_brace_do
+# mod_full_brace_function
+mod_full_brace_nl_block_rem_mlcond = false # true/false
+
+# Add or remove unnecessary parentheses on 'return' statement.
+mod_paren_on_return = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove unnecessary parentheses on 'throw' statement.
+mod_paren_on_throw = ignore # ignore/add/remove/force/not_defined
+
+# (Pawn) Whether to change optional semicolons to real semicolons.
+mod_pawn_semicolon = false # true/false
+
+# Whether to fully parenthesize Boolean expressions in 'while' and 'if'
+# statement, as in 'if (a && b > c)' => 'if (a && (b > c))'.
+mod_full_paren_if_bool = false # true/false
+
+# Whether to fully parenthesize Boolean expressions after '='
+# statement, as in 'x = a && b > c;' => 'x = (a && (b > c));'.
+mod_full_paren_assign_bool = false # true/false
+
+# Whether to fully parenthesize Boolean expressions after '='
+# statement, as in 'return a && b > c;' => 'return (a && (b > c));'.
+mod_full_paren_return_bool = false # true/false
+
+# Whether to remove superfluous semicolons.
+mod_remove_extra_semicolon = false # true/false
+
+# Whether to remove duplicate include.
+mod_remove_duplicate_include = false # true/false
+
+# the following options (mod_XX_closebrace_comment) use different comment,
+# depending of the setting of the next option.
+# false: Use the c comment (default)
+# true : Use the cpp comment
+mod_add_force_c_closebrace_comment = false # true/false
+
+# If a function body exceeds the specified number of newlines and doesn't have
+# a comment after the close brace, a comment will be added.
+mod_add_long_function_closebrace_comment = 0 # unsigned number
+
+# If a namespace body exceeds the specified number of newlines and doesn't
+# have a comment after the close brace, a comment will be added.
+mod_add_long_namespace_closebrace_comment = 0 # unsigned number
+
+# If a class body exceeds the specified number of newlines and doesn't have a
+# comment after the close brace, a comment will be added.
+mod_add_long_class_closebrace_comment = 0 # unsigned number
+
+# If a switch body exceeds the specified number of newlines and doesn't have a
+# comment after the close brace, a comment will be added.
+mod_add_long_switch_closebrace_comment = 0 # unsigned number
+
+# If an #ifdef body exceeds the specified number of newlines and doesn't have
+# a comment after the #endif, a comment will be added.
+mod_add_long_ifdef_endif_comment = 0 # unsigned number
+
+# If an #ifdef or #else body exceeds the specified number of newlines and
+# doesn't have a comment after the #else, a comment will be added.
+mod_add_long_ifdef_else_comment = 0 # unsigned number
+
+# Whether to take care of the case by the mod_sort_xx options.
+mod_sort_case_sensitive = false # true/false
+
+# Whether to sort consecutive single-line 'import' statements.
+mod_sort_import = false # true/false
+
+# (C#) Whether to sort consecutive single-line 'using' statements.
+mod_sort_using = false # true/false
+
+# Whether to sort consecutive single-line '#include' statements (C/C++) and
+# '#import' statements (Objective-C). Be aware that this has the potential to
+# break your code if your includes/imports have ordering dependencies.
+mod_sort_include = false # true/false
+
+# Whether to prioritize '#include' and '#import' statements that contain
+# filename without extension when sorting is enabled.
+mod_sort_incl_import_prioritize_filename = false # true/false
+
+# Whether to prioritize '#include' and '#import' statements that does not
+# contain extensions when sorting is enabled.
+mod_sort_incl_import_prioritize_extensionless = false # true/false
+
+# Whether to prioritize '#include' and '#import' statements that contain
+# angle over quotes when sorting is enabled.
+mod_sort_incl_import_prioritize_angle_over_quotes = false # true/false
+
+# Whether to ignore file extension in '#include' and '#import' statements
+# for sorting comparison.
+mod_sort_incl_import_ignore_extension = false # true/false
+
+# Whether to group '#include' and '#import' statements when sorting is enabled.
+mod_sort_incl_import_grouping_enabled = false # true/false
+
+# Whether to move a 'break' that appears after a fully braced 'case' before
+# the close brace, as in 'case X: { ... } break;' => 'case X: { ... break; }'.
+mod_move_case_break = false # true/false
+
+# Whether to move a 'return' that appears after a fully braced 'case' before
+# the close brace, as in 'case X: { ... } return;' => 'case X: { ... return; }'.
+mod_move_case_return = false # true/false
+
+# Add or remove braces around a fully braced case statement. Will only remove
+# braces if there are no variable declarations in the block.
+mod_case_brace = ignore # ignore/add/remove/force/not_defined
+
+# Whether to remove a void 'return;' that appears as the last statement in a
+# function.
+mod_remove_empty_return = false # true/false
+
+# Add or remove the comma after the last value of an enumeration.
+mod_enum_last_comma = ignore # ignore/add/remove/force/not_defined
+
+# Syntax to use for infinite loops.
+#
+# 0: Leave syntax alone (default)
+# 1: Rewrite as `for(;;)`
+# 2: Rewrite as `while(true)`
+# 3: Rewrite as `do`...`while(true);`
+# 4: Rewrite as `while(1)`
+# 5: Rewrite as `do`...`while(1);`
+#
+# Infinite loops that do not already match one of these syntaxes are ignored.
+# Other options that affect loop formatting will be applied after transforming
+# the syntax.
+mod_infinite_loop = 0 # unsigned number
+
+# Add or remove the 'int' keyword in 'int short'.
+mod_int_short = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove the 'int' keyword in 'short int'.
+mod_short_int = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove the 'int' keyword in 'int long'.
+mod_int_long = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove the 'int' keyword in 'long int'.
+mod_long_int = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove the 'int' keyword in 'int signed'.
+mod_int_signed = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove the 'int' keyword in 'signed int'.
+mod_signed_int = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove the 'int' keyword in 'int unsigned'.
+mod_int_unsigned = ignore # ignore/add/remove/force/not_defined
+
+# Add or remove the 'int' keyword in 'unsigned int'.
+mod_unsigned_int = ignore # ignore/add/remove/force/not_defined
+
+# If there is a situation where mod_int_* and mod_*_int would result in
+# multiple int keywords, whether to keep the rightmost int (the default) or the
+# leftmost int.
+mod_int_prefer_int_on_left = false # true/false
+
+# (OC) Whether to organize the properties. If true, properties will be
+# rearranged according to the mod_sort_oc_property_*_weight factors.
+mod_sort_oc_properties = false # true/false
+
+# (OC) Weight of a class property modifier.
+mod_sort_oc_property_class_weight = 0 # number
+
+# (OC) Weight of 'atomic' and 'nonatomic'.
+mod_sort_oc_property_thread_safe_weight = 0 # number
+
+# (OC) Weight of 'readwrite' when organizing properties.
+mod_sort_oc_property_readwrite_weight = 0 # number
+
+# (OC) Weight of a reference type specifier ('retain', 'copy', 'assign',
+# 'weak', 'strong') when organizing properties.
+mod_sort_oc_property_reference_weight = 0 # number
+
+# (OC) Weight of getter type ('getter=') when organizing properties.
+mod_sort_oc_property_getter_weight = 0 # number
+
+# (OC) Weight of setter type ('setter=') when organizing properties.
+mod_sort_oc_property_setter_weight = 0 # number
+
+# (OC) Weight of nullability type ('nullable', 'nonnull', 'null_unspecified',
+# 'null_resettable') when organizing properties.
+mod_sort_oc_property_nullability_weight = 0 # number
+
+#
+# Preprocessor options
+#
+
+# How to use tabs when indenting preprocessor code.
+#
+# -1: Use 'indent_with_tabs' setting (default)
+# 0: Spaces only
+# 1: Indent with tabs to brace level, align with spaces
+# 2: Indent and align with tabs, using spaces when not on a tabstop
+#
+# Default: -1
+pp_indent_with_tabs = -1 # number
+
+# Add or remove indentation of preprocessor directives inside #if blocks
+# at brace level 0 (file-level).
+pp_indent = ignore # ignore/add/remove/force/not_defined
+
+# Whether to indent #if/#else/#endif at the brace level. If false, these are
+# indented from column 1.
+pp_indent_at_level = false # true/false
+
+# Whether to indent #if/#else/#endif at the parenthesis level if the brace
+# level is 0. If false, these are indented from column 1.
+pp_indent_at_level0 = false # true/false
+
+# Specifies the number of columns to indent preprocessors per level
+# at brace level 0 (file-level). If pp_indent_at_level=false, also specifies
+# the number of columns to indent preprocessors per level
+# at brace level > 0 (function-level).
+#
+# Default: 1
+pp_indent_count = 1 # unsigned number
+
+# Add or remove space after # based on pp level of #if blocks.
+pp_space_after = ignore # ignore/add/remove/force/not_defined
+
+# Sets the number of spaces per level added with pp_space_after.
+pp_space_count = 0 # unsigned number
+
+# The indent for '#region' and '#endregion' in C# and '#pragma region' in
+# C/C++. Negative values decrease indent down to the first column.
+pp_indent_region = 0 # number
+
+# Whether to indent the code between #region and #endregion.
+pp_region_indent_code = false # true/false
+
+# If pp_indent_at_level=true, sets the indent for #if, #else and #endif when
+# not at file-level. Negative values decrease indent down to the first column.
+#
+# =0: Indent preprocessors using output_tab_size
+# >0: Column at which all preprocessors will be indented
+pp_indent_if = 0 # number
+
+# Whether to indent the code between #if, #else and #endif.
+pp_if_indent_code = false # true/false
+
+# Whether to indent the body of an #if that encompasses all the code in the file.
+pp_indent_in_guard = false # true/false
+
+# Whether to indent '#define' at the brace level. If false, these are
+# indented from column 1.
+pp_define_at_level = false # true/false
+
+# Whether to indent '#include' at the brace level.
+pp_include_at_level = false # true/false
+
+# Whether to ignore the '#define' body while formatting.
+pp_ignore_define_body = false # true/false
+
+# An offset value that controls the indentation of the body of a multiline #define.
+# 'body' refers to all the lines of a multiline #define except the first line.
+# Requires 'pp_ignore_define_body = false'.
+#
+# <0: Absolute column: the body indentation starts off at the specified column
+# (ex. -3 ==> the body is indented starting from column 3)
+# >=0: Relative to the column of the '#' of '#define'
+# (ex. 3 ==> the body is indented starting 3 columns at the right of '#')
+#
+# Default: 8
+pp_multiline_define_body_indent = 8 # number
+
+# Whether to indent case statements between #if, #else, and #endif.
+# Only applies to the indent of the preprocessor that the case statements
+# directly inside of.
+#
+# Default: true
+pp_indent_case = true # true/false
+
+# Whether to indent whole function definitions between #if, #else, and #endif.
+# Only applies to the indent of the preprocessor that the function definition
+# is directly inside of.
+#
+# Default: true
+pp_indent_func_def = true # true/false
+
+# Whether to indent extern C blocks between #if, #else, and #endif.
+# Only applies to the indent of the preprocessor that the extern block is
+# directly inside of.
+#
+# Default: true
+pp_indent_extern = true # true/false
+
+# How to indent braces directly inside #if, #else, and #endif.
+# Requires pp_if_indent_code=true and only applies to the indent of the
+# preprocessor that the braces are directly inside of.
+# 0: No extra indent
+# 1: Indent by one level
+# -1: Preserve original indentation
+#
+# Default: 1
+pp_indent_brace = 1 # number
+
+# Whether to print warning messages for unbalanced #if and #else blocks.
+# This will print a message in the following cases:
+# - if an #ifdef block ends on a different indent level than
+# where it started from. Example:
+#
+# #ifdef TEST
+# int i;
+# {
+# int j;
+# #endif
+#
+# - an #elif/#else block ends on a different indent level than
+# the corresponding #ifdef block. Example:
+#
+# #ifdef TEST
+# int i;
+# #else
+# }
+# int j;
+# #endif
+pp_warn_unbalanced_if = false # true/false
+
+#
+# Sort includes options
+#
+
+# The regex for include category with priority 0.
+include_category_0 = "" # string
+
+# The regex for include category with priority 1.
+include_category_1 = "" # string
+
+# The regex for include category with priority 2.
+include_category_2 = "" # string
+
+#
+# Use or Do not Use options
+#
+
+# true: indent_func_call_param will be used (default)
+# false: indent_func_call_param will NOT be used
+#
+# Default: true
+use_indent_func_call_param = true # true/false
+
+# The value of the indentation for a continuation line is calculated
+# differently if the statement is:
+# - a declaration: your case with QString fileName ...
+# - an assignment: your case with pSettings = new QSettings( ...
+#
+# At the second case the indentation value might be used twice:
+# - at the assignment
+# - at the function call (if present)
+#
+# To prevent the double use of the indentation value, use this option with the
+# value 'true'.
+#
+# true: indent_continue will be used only once
+# false: indent_continue will be used every time (default)
+#
+# Requires indent_ignore_first_continue=false.
+use_indent_continue_only_once = false # true/false
+
+# The indentation can be:
+# - after the assignment, at the '[' character
+# - at the beginning of the lambda body
+#
+# true: indentation will be at the beginning of the lambda body
+# false: indentation will be after the assignment (default)
+indent_cpp_lambda_only_once = false # true/false
+
+# Whether sp_after_angle takes precedence over sp_inside_fparen. This was the
+# historic behavior, but is probably not the desired behavior, so this is off
+# by default.
+use_sp_after_angle_always = false # true/false
+
+# Whether to apply special formatting for Qt SIGNAL/SLOT macros. Essentially,
+# this tries to format these so that they match Qt's normalized form (i.e. the
+# result of QMetaObject::normalizedSignature), which can slightly improve the
+# performance of the QObject::connect call, rather than how they would
+# otherwise be formatted.
+#
+# See options_for_QT.cpp for details.
+#
+# Default: true
+use_options_overriding_for_qt_macros = true # true/false
+
+# If true: the form feed character is removed from the list of whitespace
+# characters. See https://en.cppreference.com/w/cpp/string/byte/isspace.
+use_form_feed_no_more_as_whitespace_character = false # true/false
+
+#
+# Warn levels - 1: error, 2: warning (default), 3: note
+#
+
+# (C#) Warning is given if doing tab-to-\t replacement and we have found one
+# in a C# verbatim string literal.
+#
+# Default: 2
+warn_level_tabs_found_in_verbatim_string_literals = 2 # unsigned number
+
+# Limit the number of loops.
+# Used by uncrustify.cpp to exit from infinite loop.
+# 0: no limit.
+debug_max_number_of_loops = 0 # number
+
+# Set the number of the line to protocol;
+# Used in the function prot_the_line if the 2. parameter is zero.
+# 0: nothing protocol.
+debug_line_number_to_protocol = 0 # number
+
+# Set the number of second(s) before terminating formatting the current file,
+# 0: no timeout.
+# only for linux
+debug_timeout = 0 # number
+
+# Set the number of characters to be printed if the text is too long,
+# 0: do not truncate.
+debug_truncate = 0 # unsigned number
+
+# sort (or not) the tracking info.
+#
+# Default: true
+debug_sort_the_tracks = true # true/false
+
+# decode (or not) the flags as a new line.
+# only if the -p option is set.
+debug_decode_the_flags = false # true/false
+
+# use (or not) the exit(EX_SOFTWARE) function.
+#
+# Default: true
+debug_use_the_exit_function_pop = true # true/false
+
+# insert the number of the line at the beginning of each line
+set_numbering_for_html_output = false # true/false
+
+# Meaning of the settings:
+# Ignore - do not do any changes
+# Add - makes sure there is 1 or more space/brace/newline/etc
+# Force - makes sure there is exactly 1 space/brace/newline/etc,
+# behaves like Add in some contexts
+# Remove - removes space/brace/newline/etc
+#
+#
+# - Token(s) can be treated as specific type(s) with the 'set' option:
+# `set tokenType tokenString [tokenString...]`
+#
+# Example:
+# `set BOOL __AND__ __OR__`
+#
+# tokenTypes are defined in src/token_enum.h, use them without the
+# 'CT_' prefix: 'CT_BOOL' => 'BOOL'
+#
+#
+# - Token(s) can be treated as type(s) with the 'type' option.
+# `type tokenString [tokenString...]`
+#
+# Example:
+# `type int c_uint_8 Rectangle`
+#
+# This can also be achieved with `set TYPE int c_uint_8 Rectangle`
+#
+#
+# To embed whitespace in tokenStrings use the '\' escape character, or quote
+# the tokenStrings. These quotes are supported: "'`
+#
+#
+# - Support for the auto detection of languages through the file ending can be
+# added using the 'file_ext' command.
+# `file_ext langType langString [langString..]`
+#
+# Example:
+# `file_ext CPP .ch .cxx .cpp.in`
+#
+# langTypes are defined in uncrusify_types.h in the lang_flag_e enum, use
+# them without the 'LANG_' prefix: 'LANG_CPP' => 'CPP'
+#
+#
+# - Custom macro-based indentation can be set up using 'macro-open',
+# 'macro-else' and 'macro-close'.
+# `(macro-open | macro-else | macro-close) tokenString`
+#
+# Example:
+# `macro-open BEGIN_TEMPLATE_MESSAGE_MAP`
+# `macro-open BEGIN_MESSAGE_MAP`
+# `macro-close END_MESSAGE_MAP`
+#
+#
+# option(s) with 'not default' value: 0
+#