From e0b7d30fd437292c88141fb08d60681870b86c6e Mon Sep 17 00:00:00 2001 From: Tomi Valkeinen Date: Fri, 8 May 2026 17:22:58 +0300 Subject: Squashed 'subprojects/pixpat/' content from commit d444626 git-subtree-dir: subprojects/pixpat git-subtree-split: d444626e6ba988ec6d487800721e447f94b1eaf5 --- .github/workflows/ci.yml | 86 + .gitignore | 9 + LICENSE.md | 157 ++ README.md | 502 ++++ meson.build | 102 + meson.options | 4 + pixpat-native/codegen/gen_pixpat.py | 267 ++ pixpat-native/cross/aarch64-linux-gnu.txt | 11 + pixpat-native/inc/pixpat/pixpat.h | 264 ++ pixpat-native/profiles/no_hotpath.toml | 12 + pixpat-native/profiles/pattern_only.toml | 11 + pixpat-native/profiles/pixpat.toml | 31 + pixpat-native/profiles/to_bgr888.toml | 17 + pixpat-native/src/color.h | 199 ++ pixpat-native/src/error.h | 16 + pixpat-native/src/format_catalog.h | 140 ++ pixpat-native/src/formats.h | 13 + pixpat-native/src/formats/bayer.h | 97 + pixpat-native/src/formats/grayscale.h | 78 + pixpat-native/src/formats/rgb.h | 267 ++ pixpat-native/src/formats/yuv_packed.h | 136 + pixpat-native/src/formats/yuv_planar.h | 76 + pixpat-native/src/formats/yuv_semiplanar.h | 79 + pixpat-native/src/io.h | 13 + pixpat-native/src/io/bayer.h | 318 +++ pixpat-native/src/io/csi2.h | 80 + pixpat-native/src/io/detail.h | 62 + pixpat-native/src/io/gray.h | 153 ++ pixpat-native/src/io/gray_packed.h | 78 + pixpat-native/src/io/mono_rgb.h | 72 + pixpat-native/src/io/packed.h | 106 + pixpat-native/src/io/packed_yuv.h | 89 + pixpat-native/src/io/planar.h | 257 ++ pixpat-native/src/io/semiplanar.h | 242 ++ pixpat-native/src/layout.h | 141 ++ pixpat-native/src/params.h | 219 ++ pixpat-native/src/pattern.h | 597 +++++ pixpat-native/src/pattern_catalog.h | 64 + pixpat-native/src/pipeline.h | 44 + pixpat-native/src/pixpat.cpp | 355 +++ pixpat-native/src/pixpat_convert.cpp | 201 ++ pixpat-native/src/pixpat_internal.h | 89 + pixpat-native/src/pixpat_pattern.cpp | 168 ++ pixpat-native/src/threading.h | 95 + pixpat-native/tests/meson.build | 11 + pixpat-native/tests/test_pixpat.cpp | 107 + pixpat-native/tests/test_pixpat_c.c | 68 + pixpat-python/pixpat/__init__.py | 489 ++++ pixpat-python/pixpat/_lib/.gitkeep | 3 + pixpat-python/pixpat/_native.py | 203 ++ pixpat-python/scripts/build_wheel.sh | 39 + pixpat-python/scripts/perf_test.py | 434 ++++ pixpat-python/tests/test_basic.py | 539 ++++ pixpat-python/tests/test_numpy.py | 110 + pixpat-python/tests/test_threading.py | 187 ++ pyproject.toml | 26 + scripts/build_profiles.sh | 135 + scripts/format-all.sh | 3 + setup.py | 152 ++ uncrustify.cfg | 3708 ++++++++++++++++++++++++++++ 60 files changed, 12231 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 meson.build create mode 100644 meson.options create mode 100644 pixpat-native/codegen/gen_pixpat.py create mode 100644 pixpat-native/cross/aarch64-linux-gnu.txt create mode 100644 pixpat-native/inc/pixpat/pixpat.h create mode 100644 pixpat-native/profiles/no_hotpath.toml create mode 100644 pixpat-native/profiles/pattern_only.toml create mode 100644 pixpat-native/profiles/pixpat.toml create mode 100644 pixpat-native/profiles/to_bgr888.toml create mode 100644 pixpat-native/src/color.h create mode 100644 pixpat-native/src/error.h create mode 100644 pixpat-native/src/format_catalog.h create mode 100644 pixpat-native/src/formats.h create mode 100644 pixpat-native/src/formats/bayer.h create mode 100644 pixpat-native/src/formats/grayscale.h create mode 100644 pixpat-native/src/formats/rgb.h create mode 100644 pixpat-native/src/formats/yuv_packed.h create mode 100644 pixpat-native/src/formats/yuv_planar.h create mode 100644 pixpat-native/src/formats/yuv_semiplanar.h create mode 100644 pixpat-native/src/io.h create mode 100644 pixpat-native/src/io/bayer.h create mode 100644 pixpat-native/src/io/csi2.h create mode 100644 pixpat-native/src/io/detail.h create mode 100644 pixpat-native/src/io/gray.h create mode 100644 pixpat-native/src/io/gray_packed.h create mode 100644 pixpat-native/src/io/mono_rgb.h create mode 100644 pixpat-native/src/io/packed.h create mode 100644 pixpat-native/src/io/packed_yuv.h create mode 100644 pixpat-native/src/io/planar.h create mode 100644 pixpat-native/src/io/semiplanar.h create mode 100644 pixpat-native/src/layout.h create mode 100644 pixpat-native/src/params.h create mode 100644 pixpat-native/src/pattern.h create mode 100644 pixpat-native/src/pattern_catalog.h create mode 100644 pixpat-native/src/pipeline.h create mode 100644 pixpat-native/src/pixpat.cpp create mode 100644 pixpat-native/src/pixpat_convert.cpp create mode 100644 pixpat-native/src/pixpat_internal.h create mode 100644 pixpat-native/src/pixpat_pattern.cpp create mode 100644 pixpat-native/src/threading.h create mode 100644 pixpat-native/tests/meson.build create mode 100644 pixpat-native/tests/test_pixpat.cpp create mode 100644 pixpat-native/tests/test_pixpat_c.c create mode 100644 pixpat-python/pixpat/__init__.py create mode 100644 pixpat-python/pixpat/_lib/.gitkeep create mode 100644 pixpat-python/pixpat/_native.py create mode 100755 pixpat-python/scripts/build_wheel.sh create mode 100755 pixpat-python/scripts/perf_test.py create mode 100644 pixpat-python/tests/test_basic.py create mode 100644 pixpat-python/tests/test_numpy.py create mode 100644 pixpat-python/tests/test_threading.py create mode 100644 pyproject.toml create mode 100755 scripts/build_profiles.sh create mode 100755 scripts/format-all.sh create mode 100644 setup.py create mode 100644 uncrustify.cfg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..3eb168a --- /dev/null +++ b/.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/.gitignore b/.gitignore new file mode 100644 index 0000000..48c95cf --- /dev/null +++ b/.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/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..6fb6a01 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,157 @@ +# GNU LESSER GENERAL PUBLIC LICENSE + +Version 3, 29 June 2007 + +Copyright (C) 2007 Free Software Foundation, Inc. + + +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/README.md b/README.md new file mode 100644 index 0000000..c09e97e --- /dev/null +++ b/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 + +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-/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 +struct Plane; + +template +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>; + +using NV12 = Layout, + Plane>; +``` + +`Plane` exposes `constexpr` helpers — `find_pos`, `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` 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(buf, src, ...) + if RGB ↔ YUV: + in-place ColorXfm pass over buf + pack_from_norm(dst, buf, ...) +``` + +`unpack_to_norm` and `pack_from_norm` 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`. + +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/meson.build b/meson.build new file mode 100644 index 0000000..bd8d5e0 --- /dev/null +++ b/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/meson.options b/meson.options new file mode 100644 index 0000000..625f243 --- /dev/null +++ b/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/pixpat-native/codegen/gen_pixpat.py b/pixpat-native/codegen/gen_pixpat.py new file mode 100644 index 0000000..36fac3b --- /dev/null +++ b/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 (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 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/pixpat-native/cross/aarch64-linux-gnu.txt b/pixpat-native/cross/aarch64-linux-gnu.txt new file mode 100644 index 0000000..0c33261 --- /dev/null +++ b/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/pixpat-native/inc/pixpat/pixpat.h b/pixpat-native/inc/pixpat/pixpat.h new file mode 100644 index 0000000..dd7c24e --- /dev/null +++ b/pixpat-native/inc/pixpat/pixpat.h @@ -0,0 +1,264 @@ +#pragma once + +#include +#include + +#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, 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=" + * (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=" + * (signed integer, top edge in pixels; negative values + * clip at the top) and optional "width=" (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/pixpat-native/profiles/no_hotpath.toml b/pixpat-native/profiles/no_hotpath.toml new file mode 100644 index 0000000..e6fdda9 --- /dev/null +++ b/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/pixpat-native/profiles/pattern_only.toml b/pixpat-native/profiles/pattern_only.toml new file mode 100644 index 0000000..4410f50 --- /dev/null +++ b/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/pixpat-native/profiles/pixpat.toml b/pixpat-native/profiles/pixpat.toml new file mode 100644 index 0000000..fb27009 --- /dev/null +++ b/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 and Converter +# 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/pixpat-native/profiles/to_bgr888.toml b/pixpat-native/profiles/to_bgr888.toml new file mode 100644 index 0000000..66fbcae --- /dev/null +++ b/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/pixpat-native/src/color.h b/pixpat-native/src/color.h new file mode 100644 index 0000000..16dfb7d --- /dev/null +++ b/pixpat-native/src/color.h @@ -0,0 +1,199 @@ +#pragma once + +#include +#include +#include + +#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 +struct ColorXfm; + +template <> +struct ColorXfm { + static constexpr RGB16 apply(RGB16 p) noexcept { + return p; + } + static constexpr RGB16 apply(RGB16 p, const ColorCoeffs&) noexcept { + return p; + } +}; + +template <> +struct ColorXfm { + 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 { + 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 { + 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::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::apply(yuv, c); + std::memcpy(buf + i * sizeof(RGB16), &rgb, sizeof(RGB16)); + } +} + +} // namespace pixpat diff --git a/pixpat-native/src/error.h b/pixpat-native/src/error.h new file mode 100644 index 0000000..83a3596 --- /dev/null +++ b/pixpat-native/src/error.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +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/pixpat-native/src/format_catalog.h b/pixpat-native/src/format_catalog.h new file mode 100644 index 0000000..287d773 --- /dev/null +++ b/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 + +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/pixpat-native/src/formats.h b/pixpat-native/src/formats.h new file mode 100644 index 0000000..68bdeec --- /dev/null +++ b/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/pixpat-native/src/formats/bayer.h b/pixpat-native/src/formats/bayer.h new file mode 100644 index 0000000..057c342 --- /dev/null +++ b/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 >; +using Bayer10 = Layout >; +using Bayer12 = Layout >; +using Bayer16 = Layout >; +// 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 >; +using Bayer12P = Layout >; + +} // 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; \ + using Sink = BayerSink_ ## pat; \ + } + +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; \ + using Sink = BayerPackedSink_ ## pat_depth; \ + } + +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/pixpat-native/src/formats/grayscale.h b/pixpat-native/src/formats/grayscale.h new file mode 100644 index 0000000..b1cd294 --- /dev/null +++ b/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 { \ + using Source = GraySource; \ + using Sink = GraySink; \ + } + +PIXPAT_GRAY(Y8, + Plane); + +PIXPAT_GRAY(Y10, + Plane); + +PIXPAT_GRAY(Y12, + Plane); + +PIXPAT_GRAY(Y16, + Plane); + +#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 > { + using Source = MonoRGBSource; + using Sink = MonoRGBSink; +}; + +struct XYYY2101010 : Layout > { + using Source = MultiPixelGraySource; + using Sink = MultiPixelGraySink; +}; + +// 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 >; +using Gray12P = Layout >; +} // namespace gray_csi2_detail + +struct Y10P : gray_csi2_detail::Gray10P { + using Source = GrayPackedSource; + using Sink = GrayPackedSink; +}; + +struct Y12P : gray_csi2_detail::Gray12P { + using Source = GrayPackedSource; + using Sink = GrayPackedSink; +}; + +} // namespace pixpat::formats diff --git a/pixpat-native/src/formats/rgb.h b/pixpat-native/src/formats/rgb.h new file mode 100644 index 0000000..19d007a --- /dev/null +++ b/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 { \ + using Source = PackedSource; \ + using Sink = PackedSink; \ + } + +// --------------------------------------------------------------------- +// 32-bit packed RGB, 8-bit components. +// --------------------------------------------------------------------- + +PIXPAT_RGB_PACKED(XRGB8888, + Plane); + +PIXPAT_RGB_PACKED(ARGB8888, + Plane); + +PIXPAT_RGB_PACKED(XBGR8888, + Plane); + +PIXPAT_RGB_PACKED(ABGR8888, + Plane); + +PIXPAT_RGB_PACKED(RGBX8888, + Plane); + +PIXPAT_RGB_PACKED(RGBA8888, + Plane); + +PIXPAT_RGB_PACKED(BGRX8888, + Plane); + +PIXPAT_RGB_PACKED(BGRA8888, + Plane); + +// --------------------------------------------------------------------- +// 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); + +PIXPAT_RGB_PACKED(BGR888, + Plane); + +// --------------------------------------------------------------------- +// 16-bit packed RGB, sub-byte components. +// --------------------------------------------------------------------- + +PIXPAT_RGB_PACKED(RGB565, + Plane); + +PIXPAT_RGB_PACKED(BGR565, + Plane); + +// 8-bit packed RGB: 3-bit R / 3-bit G / 2-bit B in a single byte. + +PIXPAT_RGB_PACKED(RGB332, + Plane); + +PIXPAT_RGB_PACKED(XRGB1555, + Plane); + +PIXPAT_RGB_PACKED(ARGB1555, + Plane); + +PIXPAT_RGB_PACKED(XBGR1555, + Plane); + +PIXPAT_RGB_PACKED(ABGR1555, + Plane); + +PIXPAT_RGB_PACKED(XRGB4444, + Plane); + +PIXPAT_RGB_PACKED(ARGB4444, + Plane); + +PIXPAT_RGB_PACKED(XBGR4444, + Plane); + +PIXPAT_RGB_PACKED(ABGR4444, + Plane); + +PIXPAT_RGB_PACKED(RGBX4444, + Plane); + +PIXPAT_RGB_PACKED(RGBA4444, + Plane); + +// --------------------------------------------------------------------- +// 32-bit packed RGB, 10-bit components. +// --------------------------------------------------------------------- + +PIXPAT_RGB_PACKED(XRGB2101010, + Plane); + +PIXPAT_RGB_PACKED(ARGB2101010, + Plane); + +PIXPAT_RGB_PACKED(XBGR2101010, + Plane); + +PIXPAT_RGB_PACKED(ABGR2101010, + Plane); + +PIXPAT_RGB_PACKED(RGBX1010102, + Plane); + +PIXPAT_RGB_PACKED(RGBA1010102, + Plane); + +PIXPAT_RGB_PACKED(BGRX1010102, + Plane); + +PIXPAT_RGB_PACKED(BGRA1010102, + Plane); + +// --------------------------------------------------------------------- +// 64-bit normalized wide RGB (16 bits per component). +// --------------------------------------------------------------------- + +PIXPAT_RGB_PACKED(ABGR16161616, + Plane); + +#undef PIXPAT_RGB_PACKED + +} // namespace pixpat::formats diff --git a/pixpat-native/src/formats/yuv_packed.h b/pixpat-native/src/formats/yuv_packed.h new file mode 100644 index 0000000..8e88f10 --- /dev/null +++ b/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 > { + using Source = PackedSource; + using Sink = PackedSink; +}; + +struct XVUY8888 : Layout > { + using Source = PackedSource; + using Sink = PackedSink; +}; + +struct XVUY2101010 : Layout > { + using Source = PackedSource; + using Sink = PackedSink; +}; + +struct AVUY16161616 : Layout > { + using Source = PackedSource; + using Sink = PackedSink; +}; + +// 2-pixel-per-word 4:2:2 (uses PackedYUVSource/Sink). + +#define PIXPAT_PACKED_YUV422(name, ...) \ + struct name : Layout > { \ + using Source = PackedYUVSource; \ + using Sink = PackedYUVSink; \ + } + +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 > { + using Source = PackedYUVSource; + using Sink = PackedYUVSink; +}; + +struct Y212 : Layout > { + using Source = PackedYUVSource; + using Sink = PackedYUVSink; +}; + +struct Y216 : Layout > { + using Source = PackedYUVSource; + using Sink = PackedYUVSink; +}; + +} // namespace pixpat::formats diff --git a/pixpat-native/src/formats/yuv_planar.h b/pixpat-native/src/formats/yuv_planar.h new file mode 100644 index 0000000..bb6a415 --- /dev/null +++ b/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 { \ + using Source = PlanarSource; \ + using Sink = PlanarSink; \ + } + +PIXPAT_PLANAR(YUV420, 2, 2, + Plane, + Plane, + Plane); + +PIXPAT_PLANAR(YVU420, 2, 2, + Plane, + Plane, + Plane); + +PIXPAT_PLANAR(YUV422, 2, 1, + Plane, + Plane, + Plane); + +PIXPAT_PLANAR(YVU422, 2, 1, + Plane, + Plane, + Plane); + +PIXPAT_PLANAR(YUV444, 1, 1, + Plane, + Plane, + Plane); + +PIXPAT_PLANAR(YVU444, 1, 1, + Plane, + Plane, + Plane); + +#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, + Plane, + Plane > { + using Source = MultiPixelPlanarSource; + using Sink = MultiPixelPlanarSink; +}; + +} // namespace pixpat::formats diff --git a/pixpat-native/src/formats/yuv_semiplanar.h b/pixpat-native/src/formats/yuv_semiplanar.h new file mode 100644 index 0000000..34aea22 --- /dev/null +++ b/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, + Plane > { + using Source = SemiplanarSource; + using Sink = SemiplanarSink; +}; + +struct NV21 : Layout, + Plane > { + using Source = SemiplanarSource; + using Sink = SemiplanarSink; +}; + +struct NV16 : Layout, + Plane > { + using Source = SemiplanarSource; + using Sink = SemiplanarSink; +}; + +struct NV61 : Layout, + Plane > { + using Source = SemiplanarSource; + using Sink = SemiplanarSink; +}; + +// 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, + Plane > { + using Source = MultiPixelSemiplanarSource; + using Sink = MultiPixelSemiplanarSink; +}; + +struct P230 : Layout, + Plane > { + using Source = MultiPixelSemiplanarSource; + using Sink = MultiPixelSemiplanarSink; +}; + +} // namespace pixpat::formats diff --git a/pixpat-native/src/io.h b/pixpat-native/src/io.h new file mode 100644 index 0000000..af24232 --- /dev/null +++ b/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/pixpat-native/src/io/bayer.h b/pixpat-native/src/io/bayer.h new file mode 100644 index 0000000..6b30c0e --- /dev/null +++ b/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 +#include + +#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 +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(); + 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)); + 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 +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(); + static constexpr size_t x_idx = P::template find_pos(); + 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 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::pack(v)); + } +}; + +// Aliases so X-macro can register without nested template-template params. +template using BayerSource_RGGB = BayerSource; +template using BayerSource_BGGR = BayerSource; +template using BayerSource_GRBG = BayerSource; +template using BayerSource_GBRG = BayerSource; + +template using BayerSink_RGGB = BayerSink; +template using BayerSink_BGGR = BayerSink; +template using BayerSink_GRBG = BayerSink; +template using BayerSink_GBRG = BayerSink; + +// MIPI CSI-2 packed Bayer. The bit layout doesn't fit +// `Plane` 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 +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; + 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(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 +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; + 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 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(dst, vals); + } +}; + +template using BayerPackedSource_RGGB10 = BayerPackedSource; +template using BayerPackedSource_BGGR10 = BayerPackedSource; +template using BayerPackedSource_GRBG10 = BayerPackedSource; +template using BayerPackedSource_GBRG10 = BayerPackedSource; +template using BayerPackedSource_RGGB12 = BayerPackedSource; +template using BayerPackedSource_BGGR12 = BayerPackedSource; +template using BayerPackedSource_GRBG12 = BayerPackedSource; +template using BayerPackedSource_GBRG12 = BayerPackedSource; + +template using BayerPackedSink_RGGB10 = BayerPackedSink; +template using BayerPackedSink_BGGR10 = BayerPackedSink; +template using BayerPackedSink_GRBG10 = BayerPackedSink; +template using BayerPackedSink_GBRG10 = BayerPackedSink; +template using BayerPackedSink_RGGB12 = BayerPackedSink; +template using BayerPackedSink_BGGR12 = BayerPackedSink; +template using BayerPackedSink_GRBG12 = BayerPackedSink; +template using BayerPackedSink_GBRG12 = BayerPackedSink; + +} // namespace pixpat diff --git a/pixpat-native/src/io/csi2.h b/pixpat-native/src/io/csi2.h new file mode 100644 index 0000000..59a8f8d --- /dev/null +++ b/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 +#include +#include + +namespace pixpat::detail::csi2 +{ + +template +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 +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 +inline void pack_group( + uint8_t* dst, + const std::array::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/pixpat-native/src/io/detail.h b/pixpat-native/src/io/detail.h new file mode 100644 index 0000000..cb2b9fb --- /dev/null +++ b/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 +#include + +#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 +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 +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/pixpat-native/src/io/gray.h b/pixpat-native/src/io/gray.h new file mode 100644 index 0000000..d175b68 --- /dev/null +++ b/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 + +#include "../layout.h" +#include "detail.h" + +namespace pixpat +{ + +template +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(); + 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)); + return YUV16{ + detail::decode_norm(P::comps[y_idx].bits, vals[y_idx]), + 0x8000, 0x8000, uint16_t(0), + }; + } +}; + +template +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(); + static constexpr size_t x_idx = P::template find_pos(); + 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 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::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 +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(); + static_assert(ppw >= 1); + + // All Y positions share the same bit width. + static constexpr unsigned y_bits = P::comps[P::template find_pos(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)); + + // 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(off); + + return YUV16{ + detail::decode_norm(y_bits, vals[y_pos]), + 0x8000, 0x8000, uint16_t(0), + }; + } +}; + +template +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(); + static constexpr size_t x_idx = P::template find_pos(); + 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 v{}; + // All Y slots share the same bit width. + constexpr unsigned y_bits = P::comps[P::template find_pos(0)].bits; + for (size_t i = 0; i < ppw; ++i) { + const size_t pos = P::template find_pos(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::pack(v)); + } +}; + +} // namespace pixpat diff --git a/pixpat-native/src/io/gray_packed.h b/pixpat-native/src/io/gray_packed.h new file mode 100644 index 0000000..dc1fa68 --- /dev/null +++ b/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 +#include + +#include "../layout.h" +#include "csi2.h" + +namespace pixpat +{ + +template +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; + 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(src, x % ppg); + return YUV16{ + uint16_t(val << shift), + 0x8000, 0x8000, uint16_t(0), + }; + } +}; + +template +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; + 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 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(dst, vals); + } +}; + +} // namespace pixpat diff --git a/pixpat-native/src/io/mono_rgb.h b/pixpat-native/src/io/mono_rgb.h new file mode 100644 index 0000000..f2f8206 --- /dev/null +++ b/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 + +#include "../layout.h" +#include "detail.h" + +namespace pixpat +{ + +template +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(); + 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)); + const uint16_t r = detail::decode_norm(P::comps[r_idx].bits, vals[r_idx]); + return RGB16{ r, r, r, uint16_t(0) }; + } +}; + +template +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(); + static constexpr size_t x_idx = P::template find_pos(); + 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 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::pack(v)); + } +}; + +} // namespace pixpat diff --git a/pixpat-native/src/io/packed.h b/pixpat-native/src/io/packed.h new file mode 100644 index 0000000..9d953bc --- /dev/null +++ b/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 +#include + +#include "../layout.h" +#include "detail.h" + +namespace pixpat +{ + +template +struct PackedSource { + using Layout = L; + using Pixel = std::conditional_t; + + 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(); + static constexpr size_t i1 = P::template find_pos(); + static constexpr size_t i2 = P::template find_pos(); + static constexpr size_t a_idx = P::template find_pos(); + 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)); + 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 +struct PackedSink { + using Layout = L; + using Pixel = std::conditional_t; + + 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(); + static constexpr size_t i1 = P::template find_pos(); + static constexpr size_t i2 = P::template find_pos(); + static constexpr size_t x_idx = P::template find_pos(); + static constexpr size_t a_idx = P::template find_pos(); + 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 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::pack(v)); + } +}; + +} // namespace pixpat diff --git a/pixpat-native/src/io/packed_yuv.h b/pixpat-native/src/io/packed_yuv.h new file mode 100644 index 0000000..90c8b2f --- /dev/null +++ b/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(n). + +#include + +#include "../layout.h" +#include "detail.h" + +namespace pixpat +{ + +template +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(0); + static constexpr size_t y1_idx = P::template find_pos(1); + static constexpr size_t u_idx = P::template find_pos(); + static constexpr size_t v_idx = P::template find_pos(); + + 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)); + 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 +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(0); + static constexpr size_t y1_idx = P::template find_pos(1); + static constexpr size_t u_idx = P::template find_pos(); + static constexpr size_t v_idx = P::template find_pos(); + + 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 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::pack(v)); + } +}; + +} // namespace pixpat diff --git a/pixpat-native/src/io/planar.h b/pixpat-native/src/io/planar.h new file mode 100644 index 0000000..0dab685 --- /dev/null +++ b/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(), +// so swap_uv layouts (YVU vs YUV) work without separate templates. + +#include + +#include "../layout.h" +#include "detail.h" + +namespace pixpat +{ + +template +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(); + static constexpr size_t u_plane = L::template find_plane(); + static constexpr size_t v_plane = L::template find_plane(); + + using YP = typename L::template plane; + using UP = typename L::template plane; + using VP = typename L::template 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)); + + 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)); + const auto v_vals = VP::unpack(detail::load_word(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 +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(); + static constexpr size_t u_plane = L::template find_plane(); + static constexpr size_t v_plane = L::template find_plane(); + + using YP = typename L::template plane; + using UP = typename L::template plane; + using VP = typename L::template 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 v{}; + v[0] = detail::encode_norm(YP::comps[0].bits, block[dy][dx].y); + detail::store_word( + 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 uw{}; + uw[0] = detail::encode_norm(UP::comps[0].bits, uint16_t(u_sum / n)); + detail::store_word( + buf.data[u_plane] + cy * buf.stride[u_plane] + + cx * UP::bytes_per_pixel, + UP::pack(uw)); + + std::array vw{}; + vw[0] = detail::encode_norm(VP::comps[0].bits, uint16_t(v_sum / n)); + detail::store_word( + 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 +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(); + static constexpr size_t u_plane = L::template find_plane(); + static constexpr size_t v_plane = L::template find_plane(); + + using YP = typename L::template plane; + using UP = typename L::template plane; + using VP = typename L::template plane; + + static constexpr size_t ppw = YP::template component_count(); + static_assert(ppw == UP::template component_count()); + static_assert(ppw == VP::template component_count()); + + // All same-tag positions share the same bit width. + static constexpr unsigned y_bits = YP::comps[YP::template find_pos(0)].bits; + static constexpr unsigned u_bits = UP::comps[UP::template find_pos(0)].bits; + static constexpr unsigned v_bits = VP::comps[VP::template find_pos(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)); + const auto u_vals = UP::unpack(detail::load_word(up)); + const auto v_vals = VP::unpack(detail::load_word(vp)); + + return YUV16{ + detail::decode_norm(y_bits, y_vals[YP::template find_pos(off)]), + detail::decode_norm(u_bits, u_vals[UP::template find_pos(off)]), + detail::decode_norm(v_bits, v_vals[VP::template find_pos(off)]), + uint16_t(0), + }; + } +}; + +template +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(); + static constexpr size_t u_plane = L::template find_plane(); + static constexpr size_t v_plane = L::template find_plane(); + + using YP = typename L::template plane; + using UP = typename L::template plane; + using VP = typename L::template plane; + + static constexpr size_t ppw = YP::template component_count(); + + static constexpr size_t y_x_idx = YP::template find_pos(); + static constexpr size_t u_x_idx = UP::template find_pos(); + static constexpr size_t v_x_idx = VP::template find_pos(); + 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 yv{}; + std::array uv{}; + std::array vv{}; + + // All same-tag positions share the same bit width. + constexpr unsigned y_bits = YP::comps[YP::template find_pos(0)].bits; + constexpr unsigned u_bits = UP::comps[UP::template find_pos(0)].bits; + constexpr unsigned v_bits = VP::comps[VP::template find_pos(0)].bits; + for (size_t i = 0; i < ppw; ++i) { + yv[YP::template find_pos(i)] = + detail::encode_norm(y_bits, block[0][i].y); + uv[UP::template find_pos(i)] = + detail::encode_norm(u_bits, block[0][i].u); + vv[VP::template find_pos(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( + buf.data[y_plane] + by * buf.stride[y_plane] + + gx * YP::bytes_per_pixel, + YP::pack(yv)); + detail::store_word( + buf.data[u_plane] + by * buf.stride[u_plane] + + gx * UP::bytes_per_pixel, + UP::pack(uv)); + detail::store_word( + buf.data[v_plane] + by * buf.stride[v_plane] + + gx * VP::bytes_per_pixel, + VP::pack(vv)); + } +}; + +} // namespace pixpat diff --git a/pixpat-native/src/io/semiplanar.h b/pixpat-native/src/io/semiplanar.h new file mode 100644 index 0000000..00e7731 --- /dev/null +++ b/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 +// samples per storage word; the chroma plane has `pairs = +// component_count()` U/V pairs per storage word. block_w = +// pairs × h_sub, block_h = v_sub — each block exactly fills one +// chroma word. + +#include + +#include "../layout.h" +#include "detail.h" + +namespace pixpat +{ + +template +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(); + static constexpr size_t u_idx = CP::template find_pos(); + static constexpr size_t v_idx = CP::template find_pos(); + + 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)); + + 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)); + + 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 +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(); + static constexpr size_t u_idx = CP::template find_pos(); + static constexpr size_t v_idx = CP::template find_pos(); + + 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 v{}; + v[y_idx] = detail::encode_norm(YP::comps[y_idx].bits, + block[dy][dx].y); + detail::store_word( + 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 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::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 +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(); + static constexpr size_t pairs = CP::template component_count(); + static_assert(ppw_y >= 1 && pairs >= 1); + static_assert(pairs == CP::template component_count()); + + // All same-tag positions share the same bit width. + static constexpr unsigned y_bits = YP::comps[YP::template find_pos(0)].bits; + static constexpr unsigned u_bits = CP::comps[CP::template find_pos(0)].bits; + static constexpr unsigned v_bits = CP::comps[CP::template find_pos(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)); + + // 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)); + + return YUV16{ + detail::decode_norm(y_bits, y_vals[YP::template find_pos(y_off)]), + detail::decode_norm(u_bits, c_vals[CP::template find_pos(c_off)]), + detail::decode_norm(v_bits, c_vals[CP::template find_pos(c_off)]), + uint16_t(0), + }; + } +}; + +template +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(); + static constexpr size_t pairs = CP::template component_count(); + 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(0)].bits; + static constexpr unsigned u_bits = CP::comps[CP::template find_pos(0)].bits; + static constexpr unsigned v_bits = CP::comps[CP::template find_pos(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 v{}; + for (size_t i = 0; i < ppw_y; ++i) { + const size_t pos = YP::template find_pos(i); + v[pos] = detail::encode_norm( + y_bits, block[dy][w * ppw_y + i].y); + } + detail::store_word( + 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 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(p)] = + detail::encode_norm(u_bits, uint16_t(u_sum / n)); + uv[CP::template find_pos(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( + buf.data[1] + cy * buf.stride[1] + + uv_word_idx * CP::bytes_per_pixel, + CP::pack(uv)); + } +}; + +} // namespace pixpat diff --git a/pixpat-native/src/layout.h b/pixpat-native/src/layout.h new file mode 100644 index 0000000..d092bb1 --- /dev/null +++ b/pixpat-native/src/layout.h @@ -0,0 +1,141 @@ +#pragma once + +#include +#include +#include +#include + +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 +struct Plane { + using storage_t = Storage; + + static constexpr size_t num_comps = sizeof...(Cs); + static constexpr std::array 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 + 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 + 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& 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 unpack(Storage word) noexcept + { + std::array 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 +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 + using plane = std::tuple_element_t >; + + // 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 + static constexpr size_t find_plane() + { + return [&](std::index_sequence) { + size_t found = num_planes; + ((plane::template find_pos() < plane::num_comps + ? (found == num_planes ? (found = I, 0) : 0) + : 0), ...); + return found; + } (std::make_index_sequence{}); + } +}; + +template +struct Buffer { + std::array data; + std::array stride; +}; + +} // namespace pixpat diff --git a/pixpat-native/src/params.h b/pixpat-native/src/params.h new file mode 100644 index 0000000..aa2be67 --- /dev/null +++ b/pixpat-native/src/params.h @@ -0,0 +1,219 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#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 get(std::string_view key) const noexcept; + std::optional get_int(std::string_view key) const noexcept; + std::optional get_hex_color(std::string_view key) const noexcept; + +private: + std::vector > 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(s.front()))) + s.remove_prefix(1); + while (!s.empty() && std::isspace(static_cast(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 +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 +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 +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 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/pixpat-native/src/pattern.h b/pixpat-native/src/pattern.h new file mode 100644 index 0000000..fbee683 --- /dev/null +++ b/pixpat-native/src/pattern.h @@ -0,0 +1,597 @@ +#pragma once + +#include +#include + +#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=` 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=` (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(x); + const long long lo = pos_; + const long long hi = lo + static_cast(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(y); + const long long lo = pos_; + const long long hi = lo + static_cast(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; + 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(x); + const long long lo = pos_; + const long long hi = lo + static_cast(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; + 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(y); + const long long lo = pos_; + const long long hi = lo + static_cast(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/pixpat-native/src/pattern_catalog.h b/pixpat-native/src/pattern_catalog.h new file mode 100644 index 0000000..6576b2b --- /dev/null +++ b/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 +#include + +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/pixpat-native/src/pipeline.h b/pixpat-native/src/pipeline.h new file mode 100644 index 0000000..09e13bc --- /dev/null +++ b/pixpat-native/src/pipeline.h @@ -0,0 +1,44 @@ +#pragma once + +#include + +#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 +struct Converter { + using Xfm = ColorXfm; + static constexpr size_t bh = Sink::block_h; + static constexpr size_t bw = Sink::block_w; + + static void run(const Buffer& src, + Buffer& 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/pixpat-native/src/pixpat.cpp b/pixpat-native/src/pixpat.cpp new file mode 100644 index 0000000..ac21fac --- /dev/null +++ b/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 + +#include +#include +#include + +#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 +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(src); + auto* dst = reinterpret_cast(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 +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(dst); + auto* src = reinterpret_cast(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` / `&pack_from_norm` +// 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 +static constexpr UnpackFn unpack_for() noexcept +{ + if constexpr (Read) + return &unpack_to_norm; + else + return nullptr; +} + +template +static constexpr PackFn pack_for() noexcept +{ + if constexpr (Write) + return &pack_from_norm; + else + return nullptr; +} + +const FormatInfo s_format_info[] = { +#define CAPS(name) s_format_caps[size_t(FormatId::name)] +#define X(name) \ + { \ + unpack_for(), \ + pack_for(), \ + 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(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/pixpat-native/src/pixpat_convert.cpp b/pixpat-native/src/pixpat_convert.cpp new file mode 100644 index 0000000..63461d8 --- /dev/null +++ b/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 +#include +#include +#include + +#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 +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(src); + auto db = make_buffer

(dst); + Converter::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 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 +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, 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 +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( \ + 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 would be instantiated for +// every catalog format, not just hot pivots. +template +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( + dst_id, src, dst, W, H, by_start, by_end, spec); + return true; + } + } + return false; +} + +template +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( + 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( \ + src_id, dst_id, src, dst, W, H, by_start, by_end, spec)) \ + return; \ + if (try_hot_dst( \ + 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/pixpat-native/src/pixpat_internal.h b/pixpat-native/src/pixpat_internal.h new file mode 100644 index 0000000..50d3405 --- /dev/null +++ b/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 +#include + +#include + +#include "color.h" +#include "format_catalog.h" +#include "layout.h" +#include "pattern_catalog.h" + +namespace pixpat +{ + +template +inline Buffer make_buffer(const pixpat_buffer* b) noexcept +{ + Buffer out{}; + for (size_t i = 0; i < Layout::num_planes; ++i) { + out.data[i] = static_cast(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/pixpat-native/src/pixpat_pattern.cpp b/pixpat-native/src/pixpat_pattern.cpp new file mode 100644 index 0000000..e8ac780 --- /dev/null +++ b/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 +#include +#include +#include +#include + +#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 +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; + + 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 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(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 +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. +template +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; + constexpr bool has_yuv = !std::is_void_v; + 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(params, id, dst, W, H, + by_start, by_end, spec); + else + run_one_pattern(params, id, dst, W, H, + by_start, by_end, spec); + } else if constexpr (has_rgb) { + run_one_pattern(params, id, dst, W, H, + by_start, by_end, spec); + } else { + run_one_pattern(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( \ + 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/pixpat-native/src/threading.h b/pixpat-native/src/threading.h new file mode 100644 index 0000000..5e7fc01 --- /dev/null +++ b/pixpat-native/src/threading.h @@ -0,0 +1,95 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include + +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(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 +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(n_threads) > max_useful) + n_threads = static_cast(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 errors(n_threads); + std::vector 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/pixpat-native/tests/meson.build b/pixpat-native/tests/meson.build new file mode 100644 index 0000000..cbdfda2 --- /dev/null +++ b/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/pixpat-native/tests/test_pixpat.cpp b/pixpat-native/tests/test_pixpat.cpp new file mode 100644 index 0000000..8319968 --- /dev/null +++ b/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 + +#include +#include +#include + +// 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 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 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/pixpat-native/tests/test_pixpat_c.c b/pixpat-native/tests/test_pixpat_c.c new file mode 100644 index 0000000..e80780a --- /dev/null +++ b/pixpat-native/tests/test_pixpat_c.c @@ -0,0 +1,68 @@ +/* + * Native C smoke test for libpixpat. + * + * Sole purpose: prove that 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 + +#include +#include +#include +#include + +/* 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/pixpat-python/pixpat/__init__.py b/pixpat-python/pixpat/__init__.py new file mode 100644 index 0000000..6531662 --- /dev/null +++ b/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/pixpat-python/pixpat/_lib/.gitkeep b/pixpat-python/pixpat/_lib/.gitkeep new file mode 100644 index 0000000..7319712 --- /dev/null +++ b/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/pixpat-python/pixpat/_native.py b/pixpat-python/pixpat/_native.py new file mode 100644 index 0000000..46fe452 --- /dev/null +++ b/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/pixpat-python/scripts/build_wheel.sh b/pixpat-python/scripts/build_wheel.sh new file mode 100755 index 0000000..1eb2586 --- /dev/null +++ b/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 +# +# 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-/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 " >&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-/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/pixpat-python/scripts/perf_test.py b/pixpat-python/scripts/perf_test.py new file mode 100755 index 0000000..100c16e --- /dev/null +++ b/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 `` -> `` 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 ``" -> "``: ``--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/pixpat-python/tests/test_basic.py b/pixpat-python/tests/test_basic.py new file mode 100644 index 0000000..2ed8bb7 --- /dev/null +++ b/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/pixpat-python/tests/test_numpy.py b/pixpat-python/tests/test_numpy.py new file mode 100644 index 0000000..6dbd7bb --- /dev/null +++ b/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/pixpat-python/tests/test_threading.py b/pixpat-python/tests/test_threading.py new file mode 100644 index 0000000..99d4620 --- /dev/null +++ b/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/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2de83a4 --- /dev/null +++ b/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/scripts/build_profiles.sh b/scripts/build_profiles.sh new file mode 100755 index 0000000..0ae6d63 --- /dev/null +++ b/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/scripts/format-all.sh b/scripts/format-all.sh new file mode 100755 index 0000000..156d807 --- /dev/null +++ b/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/setup.py b/setup.py new file mode 100644 index 0000000..0a24682 --- /dev/null +++ b/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 — sets PIXPAT_TARGET_ARCH + and runs `python -m build`; meson lands in pixpat-python/build-/ + 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[-]/.""" + + 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. -> .so... + # 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/uncrustify.cfg b/uncrustify.cfg new file mode 100644 index 0000000..a0a0bb4 --- /dev/null +++ b/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>=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 '[] (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 '[] { ... }'. +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 +# '[]( ){ ... }' +# 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 +# '[]( int x ){ ... }'. +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) { ... }'. +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 '[]( ... ){ ... } ( ... )'. +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(foo);'. +sp_angle_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '>' and '()' as found in 'new List();'. +sp_angle_paren_empty = ignore # ignore/add/remove/force/not_defined + +# Add or remove space between '>' and a word as in 'List m;' or +# 'template 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 >'. 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 ( ; ; )'. +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 ( ; ; )'. +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 ( ; ; )'. +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' or '@interface MyClass : NSObject'. +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):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) {'. +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) {'. +# 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 : 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 : expr)'. +sp_before_for_colon = ignore # ignore/add/remove/force/not_defined + +# (D) Add or remove space between 'extern' and '(' as in 'extern (C)'. +sp_extern_paren = ignore # ignore/add/remove/force/not_defined + +# Add or remove space after the opening of a C++ comment, as in '// 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 '// 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 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) {'. +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) {'. 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 :: 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++; return;'. +nl_after_vbrace_close = false # true/false + +# Add or remove newline between the close brace and identifier, +# as in 'struct { int a; } 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 : 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] = { 0 };'. +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) 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 (...) 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) 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 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 +# -- cgit v1.2.3