Skip to content

Pelorus interop ABI (vendored) — libvmaf/pelorus/

The Pelorus GPU pre-encode pipeline and vmafx share a small, versioned data-plane interop ABI: a self-describing per-frame metadata blob that Pelorus filters (vf_pelorus_*) write and vmafx reads for perceptually-weighted scoring. The blob rides each AVFrame as an AV_FRAME_DATA_SEI_UNREGISTERED payload, prefixed by a fixed project UUID so it round-trips a filtergraph and never collides with codec side data.

This ABI is single-sourced in Pelorus (Pelorus ADR-0103). vmafx carries a verbatim, pinned, read-only mirror of it so the two repos can build, test, and evolve independently without a submodule or a shared package. The vendoring decision is recorded in ADR-1113; the current safety re-pin and exact-mirror guard rails are recorded in ADR-1276.

The mirror is read-only

Do not edit the vendored files. They are byte-identical to their Pelorus origin (pinned at VMAFx/pelorus@11e183ec0aedf6b3e6447fda64acbb6072a1ae60) except for a VENDORED FROM ... DO NOT EDIT banner and the include-path rewrite described below. Fix any defect upstream in Pelorus, then re-sync.

What is vendored

Vendored path Pelorus origin (libpelorus/) Role
core/include/libvmaf/pelorus/pelorus.h include/pelorus/pelorus.h Umbrella header: version + pel_result codes.
core/include/libvmaf/pelorus/interop.h include/pelorus/interop.h The blob ABI: header, section catalogue, pack/parse API, _Static_assert size locks.
core/include/libvmaf/pelorus/deband.h include/pelorus/deband.h The deband filter parameter contract (control plane).
core/include/libvmaf/pelorus/denoise.h include/pelorus/denoise.h The temporal-denoise filter parameter contract (control plane, ABI 1.x).
core/src/interop/pelorus_interop.c src/interop.c pel_blob_pack / pel_blob_find_section / pel_blob_is_present / pel_blob_free.
core/src/interop/pelorus_deband_params.c src/deband_params.c pel_deband_params_default / pel_deband_params_validate.
core/src/interop/pelorus_denoise_params.c src/denoise_params.c pel_denoise_params_default / pel_denoise_params_validate.
core/src/interop/pelorus_qp_report_csv.c src/qp_report_csv.c x265 --csv reader: pel_x265_csv_parse / pel_qp_report_from_x265_frames (folds into PEL_SEC_QPREPORT).
core/src/interop/pelorus_version.c src/version.c pelorus_version* / pel_result_str.
core/test/test_pelorus_interop.c test/interop_test.c The shared conformance fixture (see below).

The five .c files compile straight into libvmaf (CPU-only, dependency-free; registered in core/src/meson.build). deband_params.c, denoise_params.c, qp_report_csv.c, and version.c are vendored alongside interop.c because the shared conformance fixture links the deband / denoise parameter contracts, the x265 CSV QP-report reader, and the version / result-string accessors — vendoring them keeps the fixture byte-identical to Pelorus's. (qp_report_csv.c is required to link: the ABI-1.3 fixture exercises pel_x265_csv_parse.)

The only local edits

  1. A VENDORED FROM VMAFx/pelorus@11e183ec0aedf6b3e6447fda64acbb6072a1ae60 — DO NOT EDIT banner inserted after the (unchanged) Pelorus license header.
  2. Intra-Pelorus #include "pelorus/<x>.h" rewritten to #include "libvmaf/pelorus/<x>.h" so the headers resolve under core/include/. Nothing else changes.

The conformance test additionally carries a vmafx-authored (Copyright 2026 Lusoris) header instead of cloning Pelorus's. From its first vendored #include onward, its body is exact Pelorus source apart from the same include rewrite. The sync script renders that prefix canonically from the pin and ABI version, then appends the transformed body. VMAFx format and tidy scopes exclude only the manifest-owned fixture path; lint-policy changes belong in VMAFx tooling, never in the fixture.

The blob, in brief

A blob is a flat, pointer-free, little-endian byte image:

[16-byte UUID][PelorusSideData header (48 B)][PelorusSectionDir dir[count] (16 B each)][section payloads, each 8-byte aligned]
  • Magic / identity: 8-byte "PELOR1\0\0" magic, a fixed 16-byte routing UUID (e1d7c4a2-6b93-4f08-9a55-0f3c2db17e64), and an ABI major/minor.
  • Sections (append-only bits, never reused): BANDING, VARIANCE, DENOISE, FILMGRAIN, MOTION, and (ABI ≥ 1.3) QPREPORT (1.1, encoder QP / bit readback), MOTION_CONF (1.2, per-block MV confidence), and COMPLEXITY (1.3, per-frame complexity scalar — consumed by perceptual weighting, see perceptual-weight.md). Each is independently optional and gated by section_mask.
  • Directory: one PelorusSectionDir{section_id, offset, size, struct_minor} per present section, so an older consumer can locate and tail-skip a newer producer's larger section.

The full struct layout lives in interop.h. Its _Static_assert size locks are compiled in every translation unit that includes the header, including the conformance test, so an accidental layout change is a build failure, not a silent corruption.

Struct Size (bytes)
PelorusSideData (header) 48
PelorusSectionDir 16
PelorusBandingSection 24
PelorusVarianceSection 28
PelorusDenoiseSection 28
PelorusFilmGrainSection 216
PelorusMotionSection 32
PelorusMotionConfSection 16
PelorusComplexitySection 16
PelorusQpReportSection 64

Pack / parse API (interop.h)

Function Purpose
pel_blob_pack(meta, sections, nb, &blob, &len) Allocate a UUID-prefixed blob from header fields + a set of sections.
pel_blob_free(blob) Free a blob returned by pel_blob_pack.
pel_blob_is_present(blob, len) Cheap pre-check: is this a valid Pelorus blob (UUID + magic + ABI major)?
pel_blob_find_section(blob, len, sec, known_size, &ptr, &size) Locate a section; returns a pointer into the blob plus min(producer, consumer) readable bytes.

Pelorus v0.2.2 accepts a blob at any caller-buffer base alignment. The parser and packer move wire headers and directory entries through aligned local objects with memcpy; they never cast an untrusted byte address to a structured pointer.

Framing still requires header_size and each directory offset to be 8-byte aligned relative to the blob. Malformed values return PEL_ERR_ABI.

Copy a section before typed access

A valid section pointer is relative to the caller's base, which may be unaligned. Whenever the caller buffer is not suitably aligned, memcpy the returned section into an aligned local object before reading its fields.

vmafx is a reader only: it never mutates a blob (single-writer invariant, Pelorus is the sole writer). The reader side ships as the perceptual pooling weights in perceptual-weight.md.

Forward / backward compatibility (R1–R6)

The ABI is normative; both repos depend on these rules (verbatim from interop.h):

  • R1 — Append-only. New fields are added at the END of a section struct, or as a NEW section with a new bit. Never reorder, resize, or repurpose a field.
  • R2 — Never remove a section bit or a field. Deprecate by documentation; the slot stays reserved forever.
  • R3 — Every section is independently optional, gated by section_mask. A consumer MUST ignore bits it does not understand (forward-compat) and MUST tolerate absence of bits it does understand (back-compat).
  • R4 — Explicit offsets/sizes (PelorusSectionDir) let an older consumer parse a newer producer's larger section: read min(known_size, dir.size) bytes and ignore the tail.
  • R5 — Flat, pointer-free, little-endian byte image. A byte-swap path is a future, additive change gated on PELORUS_ABI_MINOR.
  • R6 — PELORUS_ABI_MAJOR bumps only on a breaking change (which R1/R2 forbid for additive evolution) — in practice it never bumps. PELORUS_ABI_MINOR bumps when a new section bit or appended field lands.

The current library release is 0.2.2 and the ABI remains 1.3 (PELORUS_ABI_MAJOR=1, PELORUS_ABI_MINOR=3). Minor 1.1 added PEL_SEC_QPREPORT, 1.2 added PEL_SEC_MOTION_CONF, 1.3 added PEL_SEC_COMPLEXITY — all append-only, so the major stayed at 1 and the mirror re-pin (ADR-1120) was non-breaking. The 0.2.2 re-pin changes parser safety semantics only; it does not add or alter an ABI field or section.

Conformance fixture — the byte-compat proof

core/test/test_pelorus_interop.c is the same fixture both repos run. It exercises the following vectors against the vendored parser (the ABI-1.3 fixture grew the original seven to sixteen):

# Vector What it proves
1 roundtrip pack three sections, parse them back, verify scalars + the 8-byte-aligned 64-bit seed survives.
2 forward_compat an older consumer that knows a smaller struct gets min(producer, consumer) readable bytes (R4).
3 abi_major_mismatch a bumped major is detected and rejected, not misread (R6).
4 foreign_buffer a non-Pelorus SEI (e.g. an x264 user-data blob) is cleanly ignored.
5 header_only a zero-section blob is valid and parses.
6 truncation a short buffer is detected, never read out of bounds.
7 misaligned_offset a section whose dir.offset is not 8-aligned is rejected (R5) rather than handed out for an unaligned cast.
8 misaligned_blob_base a valid blob re-homed at every base skew from one through seven bytes parses without undefined behavior; the section payload is read safely through memcpy.
9 unaligned_header_size a header size that would place the directory on a non-8-byte boundary is rejected as corrupt framing.
10 pack_size_overflow a section size that would overflow the framing is rejected, not wrapped.
11 qp_report_roundtrip pack/parse the PEL_SEC_QPREPORT section (ABI 1.1).
12 motion_conf_roundtrip pack/parse the PEL_SEC_MOTION_CONF section (1.2).
13 complexity_roundtrip pack/parse the PEL_SEC_COMPLEXITY section (1.3).
14 qp_report_fold fold parsed frames into a bit-weighted QP report.
15 x265_csv_reader pel_x265_csv_parse reads a real x265 --csv table, drops the aggregate row, and computes honored_fraction.
16 deband_params the deband contract: defaults validate, out-of-range is rejected with the offending field name.

Run the fixture

A green run here proves vmafx's vendored parser is byte-identical to Pelorus's writer: a blob packed by Pelorus parses in vmafx and vice versa. The fixture is wired into the fast suite:

meson setup core/build-cpu core -Denable_cuda=false -Denable_sycl=false
ninja -C core/build-cpu
python3 scripts/ci/run_meson_test.py -- \
  -C core/build-cpu test_pelorus_interop   # all vectors, must pass

Re-syncing the mirror

The pin and the drift guard live in scripts/sync-pelorus-interop.sh. It reads the vendored sources from the pinned commit's git tree object (git show 11e183ec0aedf6b3e6447fda64acbb6072a1ae60:libpelorus/…), so it stays accurate even when the local Pelorus checkout's HEAD has moved past the pin. A directory that is not a Git checkout, or a checkout that lacks the exact object, fails closed.

Check and re-vendor

Check for drift (the CI gate). The path defaults to $PELORUS_DIR or ../pelorus:

scripts/sync-pelorus-interop.sh /path/to/pelorus

The output is OK - mirror matches pelorus@11e183ec0aedf6b3e6447fda64acbb6072a1ae60, or FAIL - mirror has drifted with a diff and exit code 1.

To re-vendor after a reviewed Pelorus ABI addition or a released parser correctness or security fix (an ABI-minor bump is not required):

  1. Bump PELORUS_VENDOR_SHA in the script, in this page and in the ADR. The banners are rewritten automatically by --update.
  2. Add any new Pelorus source or header to the script's render manifest and to scripts/ci/pelorus-mirror-paths.txt, plus core/src/meson.build and the test target if it must compile or link. The guard requires both path sets to match exactly.
  3. Re-vendor:
scripts/sync-pelorus-interop.sh --update /path/to/pelorus
  1. Confirm the mirror is clean:
scripts/sync-pelorus-interop.sh /path/to/pelorus
  1. Rebuild and run the conformance fixture; it must stay green.

What the guard enforces

--update re-vendors both the manifest files and the complete conformance fixture:

  • It regenerates the canonical Lusoris-authored prefix from the pinned commit and ABI version, then appends the Pelorus body with the pelorus/ to libvmaf/pelorus/ include rewrite.
  • The drift check compares every complete rendered file byte for byte through end of file. A prefix mutation, a stale fixture pin or a missing or extra final newline is drift.
  • It rejects any added or missing tracked file in the exact-mirror lint namespaces unless the manifest owns that path.

Local formatting or tidy edits therefore fail the guard instead of becoming a second implementation or silently gaining a lint exemption.

A re-sync that changes the ABI is an ADR-worthy event (a new section bit or an appended field bumps PELORUS_ABI_MINOR); ADR-1120 records the 1.0 → 1.3 re-pin as a follow-up to ADR-1113. A released parser-only correctness/security fix keeps the ABI number intact and is recorded in a new follow-up decision plus research evidence; ADR-1276 records the v0.2.2 safety re-pin while preserving ADR-1113's immutable body.