ADR-0250: Tiny-AI extractor template — shared scaffolding header¶
- Status: Accepted
- Date: 2026-04-29
- Deciders: Lusoris, Claude (Anthropic)
- Tags:
ai,dnn,refactor,dx,fork-local
Context¶
Every tiny-AI feature extractor (feature_lpips.c, fastdvdnet_pre.c, the in-flight feature_mobilesal.c from PR #208, the planned feature_transnet_v2.c) opens a VmafDnnSession, resolves an ONNX path via a feature option with an env-var fallback, and (for the colour-sensitive ones) converts BT.709 limited-range YUV → RGB with nearest-neighbour chroma upsampling. The extractors that landed first copy-pasted ~70 lines of identical setup / option-table / cleanup plumbing into each new file. PR #208 made the duplication visible at review time — the mobilesal yuv8_to_rgb8_planes body is byte-for-byte identical to the LPIPS one, including comments.
The fork already enforces NASA/JPL Power-of-10 (rule 1: simple control flow, rule 9: limited macro use) and SEI CERT C, so the dedup mechanism must not introduce setjmp/longjmp tricks, recursion, or unbounded preprocessor wizardry. New extractors should be ~30 LOC of extractor-specific tensor wiring, not ~150 LOC where 70 % is plumbing.
Decision¶
We will introduce core/src/dnn/tiny_extractor_template.h, a shared header that exposes three static inline helpers and one struct-literal-emitting macro:
vmaf_tiny_ai_resolve_model_path(name, option, env_var)— feature- option-then-env-var lookup with a single user-facingno model pathlog line on failure.vmaf_tiny_ai_open_session(name, path, &out)—vmaf_dnn_session_openwrapper with the standardvmaf_dnn_session_open(<path>) failed: <rc>log line on error.vmaf_tiny_ai_yuv8_to_rgb8_planes(pic, dst_r, dst_g, dst_b)— BT.709 limited-range YUV → RGB with nearest-neighbour chroma upsampling (bit-exact with the previous per-extractor copies).VMAF_TINY_AI_MODEL_PATH_OPTION(state_t, help_text)— emits the standardmodel_pathrow of a per-extractorVmafOption[]table.
The extractor's init / extract / close lifecycle stays hand-written per file — the shapes vary too much (single-frame, ring-buffer, large-window) for a generic lifecycle macro to be worthwhile. The recipe lives in docs/ai/extractor-template.md.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| A. Inline helpers + one option-table macro (chosen) | Power-of-10 friendly (no recursion / no setjmp / bounded macros), clangd jumps directly to source, debugger steps through normally. Each helper is ≤ 25 LOC and trivially auditable. | Doesn't dedup the lifecycle skeleton — that stays per-file. | — |
B. Codegen (Jinja template emitting a stub feature_<name>.c) | Maximum dedup; new extractor = one YAML file. | Adds a Python build dep + an extra meson custom-target step. Generated C is harder to debug (stack traces point at generated lines). Pre-commit and clang-tidy paths get longer. The variation between extractors (LPIPS two-input, FastDVDnet 5-frame ring, TransNet 100-frame window) needs four templates anyway. | Cost > savings for a 4-file population. |
C. Helper functions exposed via a fnptr table (VmafTinyAiHooks) — extractors fill init_alloc / release / extract callbacks, the template orchestrates | Cleanest factoring on paper; every extractor reduces to the hook impls. | Indirect calls hide control flow from the static analysers (CERT MSC22-C-friendly but lints noisily). The varying extract shapes (different tensor names, output shapes, score names emitted) push most of the per-frame logic back into the hook anyway. Frame-window extractors need the hook to stash a ring-buffer slot — the template fights the data. | Power-of-10 rule 9 (limited use of function pointers) + the pattern doesn't actually save LOC. |
D. Per-extractor variants of the template via feature-flag macros (VMAF_TINY_AI_WITH_RING_BUFFER, VMAF_TINY_AI_WINDOW_SIZE = N) | Single template handles all four cases. | Macro chain becomes the actual API surface; debugging a misbehaving extractor means debugging the macro expansion. Violates Power-of-10 rule 9 in spirit (heavy preprocessor reliance). | Same cost > savings argument as B with worse debuggability. |
Consequences¶
- Positive: New tiny-AI extractors drop ~70 LOC of plumbing each.
feature_lpips.cshrinks from 305 → ~205 LOC,fastdvdnet_pre.cfrom 341 → ~317 LOC. Bit-exact behaviour preserved (the YUV→RGB body and option-table layout are literal moves). When PR #208 (mobilesal) and the TransNet V2 work land, they pick up the helpers in their next rebase and shed another ~140 LOC of duplication. - Negative: One additional include in each extractor; clangd needs the
libvmaf/srcinclude path configured (already the case in the meson build — the worktree-side IDE warning is cosmetic). The shared YUV→RGB helper now sits indnn/instead offeature/— slight conceptual stretch, justified by it being tiny-AI-specific (not shared with non-tiny-AI extractors like ciede). - Neutral / follow-ups: The follow-up PRs that land
feature_mobilesal.candfeature_transnet_v2.care expected to use the helpers from day one; their rebase notes track the dependency. The template header itself is a no-op at link time (everything isstatic inline) so it doesn't widen the public ABI.
References¶
req(paraphrased): user requested extracting a tiny-AI extractor template + macros to deduplicate the boilerplate across four extractors, with new extractors targeting ~30 LOC instead of ~150.- Related ADRs: ADR-0041 (LPIPS surface), ADR-0042 (tiny-AI doc bar), ADR-0215 (FastDVDnet), ADR-0218 (MobileSal).
- Implementation:
core/src/dnn/tiny_extractor_template.h,core/src/feature/feature_lpips.c,core/src/feature/fastdvdnet_pre.c. - Recipe doc:
docs/ai/extractor-template.md.