Research-1318: Bounded vmaf-perShot scan loop with operator frame ceiling¶
Scope¶
This digest covers T-PER-SHOT-ENDLESS-INPUT-NOT-A-TIMEOUT-2026-09-21: resolving the operator hang on endless/stream inputs in vmaf-perShot (core/tools/vmaf_per_shot.c), providing the smallest backward-compatible operator-visible bound, and fixing the UINT32_MAX off-by-one boundary check from ADR-1287 under ADR-1318.
Reproducer and root cause¶
In ADR-1287, per_shot_scan_loop() was bounded using VMAF_PER_SHOT_MAX_FRAMES (UINT32_MAX). While this satisfied NASA/JPL Power of 10 Rule 2 for static loop boundedness and prevented uint32_t counter overflow in shot records, it did not offer a practical operator escape hatch for endless streams:
- Endless input hang: A reference path that never reaches end of file (such as a FIFO held open by a live writer, or
/dev/zero) required reading ~4.29 billion frames (~1.2 PB at 576x324 YUV420) before hittingUINT32_MAX. In interactive use or CI/CD pipelines, this appeared as an unescapable infinite hang. Reproducer:
mkfifo /tmp/endless.yuv
yes 2>/dev/null | tr -d '\n' > /tmp/endless.yuv &
core/build/tools/vmaf-perShot --reference /tmp/endless.yuv \
--width 16 --height 16 --pixel_format 420 --bitdepth 8 \
--output /tmp/plan.csv
# Hangs indefinitely until cancelled or killed
-
Off-by-one boundary error: ADR-1287 tested
ctx->frame_idx >= VMAF_PER_SHOT_MAX_FRAMESafter incrementing and exiting the loop. Consequently, an input containing exactlyUINT32_MAXframes was rejected with-EFBIGeven though all frames fit intouint32_twithout wrapping, limiting the largest accepted input toUINT32_MAX - 1frames. -
ADR-1287 alternative rejection: ADR-1287 rejected
--max-framesunder the assumption that it would default to an arbitrary small number that would silently truncate valid content. -
Incomplete-frame and read-error conflation: the boundary probe depended on
per_shot_read_luma(), which read luma and usedfseeko()/_fseeki64()to skip chroma. Seeking beyond EOF succeeds on regular files, so a complete frame followed by only luma (or partial chroma) produced a phantom second frame. The same function mappedfread() == 0directly to EOF without consultingferror(), so an I/O error after a valid prefix could silently produce a successful truncated plan.
Options¶
| Approach | Preserves finite streams | Bounded on FIFOs | Backward compatible | Decision |
|---|---|---|---|---|
| Wall-clock timeout on scan loop | Contention-sensitive | Yes | Breaks long legitimate scans | Rejected: nondeterministic under load. |
| Small default frame ceiling | Truncates long clips | Yes | No: breaks existing workflows | Rejected: silent data loss on long videos. |
Reject non-regular files (S_ISFIFO) | Yes | Partial (misses /dev/zero) | Breaks UNIX pipeline composition | Rejected: pipes from ffmpeg are legitimate. |
Explicit --frames flag defaulting to 0 (unbounded) | Yes | Yes (when specified) | Yes (100% backward compatible) | Selected (ADR-1318). |
For frame completeness, exact luma-plus-chroma consumption was selected over seek-based skipping. A file-length precheck could retain seeking for immutable regular files, but it cannot provide the same contract for pipes and can race a mutable file. Correctness is established first; measured optimisation belongs to the later performance-tuning phase.
Implementation¶
-
Settings and CLI Options:
struct vmaf_per_shot_settingsgainsuint32_t max_frames;defaulting to0U. Option-F, --frames <N>is added with aliases--frame_cnt <N>and--max-frames <N>. Parsing viaper_shot_parse_uint(optarg_, 0U, VMAF_PER_SHOT_MAX_FRAMES, &uv)ensures strictly non-negative values within[0, UINT32_MAX]. -
HISS-04 LOC Compliance:
per_shot_apply_opt()combined the'w'/'h'and'm'/'M'switch cases, reducing function length from 57 LOC to 56 LOC (well within the HISS-04 cap of 60 LOC). -
64-bit Frame Indexing and Off-by-One Fix:
ctx->frame_idxinstruct per_shot_scan_ctxis widened touint64_t. The scan loop ceiling is evaluated as:
const uint64_t ceiling =
(s->max_frames > 0U) ? (uint64_t)s->max_frames : ((uint64_t)VMAF_PER_SHOT_MAX_FRAMES + 1ULL);
At ctx->frame_idx >= VMAF_PER_SHOT_MAX_FRAMES, an attempt to read the next frame is made. If EOF is reached, the scan completes cleanly with exit code 0, accepting an input of exactly UINT32_MAX frames. Only if another frame actually exists is -EFBIG returned.
-
Deterministic Testing: A test-only build sets
VMAF_PER_SHOT_MAX_FRAMES=3U, making both sides of the production boundary logic executable without constructing aUINT32_MAX-frame input. The public CLI suite now covers one complete frame, a trailing luma-only frame, partial chroma, exactly three complete frames, and a fourth complete frame.test_vmaf_per_shot_inputcloses the backing descriptor after one valid frame and proves the next stdio read is reported as an error rather than EOF. -
Operator-ceiling Testing: Sections 9–14 in
core/tools/test/test_vmaf_per_shot.shretain the original operator-bound coverage: - Section 9:
--frames 10on the 48-frame fixture terminates at 10 frames. - Section 10:
--frames 0preserves the full 48-frame scan. - Section 11: Aliases
-F 10,--frame_cnt 10, and--max-frames 10yield identical output. - Section 12: Bounded read on
/dev/zerowith--frames 6terminates in milliseconds. - Section 13: Bounded read on a live endless FIFO terminates cleanly for 5 frames without hanging.
- Section 14: Negative and non-numeric
--framesvalues fail during option parsing.
Evidence¶
Red Phase (prior to implementation)¶
$ core/build/tools/vmaf-perShot --reference testdata/src.yuv --width 576 --height 324 \
--pixel_format 420 --bitdepth 8 --output /tmp/plan.csv --frames 10
vmaf-perShot: unrecognized option '--frames'
Usage: vmaf-perShot ...
$ meson test -C build-review --no-rebuild --print-errorlogs \
test_vmaf_per_shot_input test_vmaf_per_shot
test_vmaf_per_shot_input FAIL: luma-only frame was not rejected
test_vmaf_per_shot FAIL: expected failure on complete_then_luma_only_420
Green Phase (post implementation)¶
$ meson test -C build-review --no-rebuild test_vmaf_per_shot_input test_vmaf_per_shot
1/2 test_vmaf_per_shot_input OK
2/2 test_vmaf_per_shot OK
$ meson test -C build-review --suite=fast --print-errorlogs
162/162 test OK
Governance and Static Analysis¶
$ clang-format -n --Werror core/tools/vmaf_per_shot.c \
core/tools/vmaf_per_shot_input.c core/tools/vmaf_per_shot_input.h \
core/tools/test/test_vmaf_per_shot_input.c
(zero warnings / violations)
$ clang-tidy -p build-review core/tools/vmaf_per_shot_input.c \
core/tools/test/test_vmaf_per_shot_input.c core/tools/vmaf_per_shot.c --quiet
(zero warnings / violations)
$ make verify-all
[PASS] HISS invariant scan verified: 213 active violations within 229 baselined limit (all touched files clean).
Audit Summary: configured governance gates passed for vmafx/vmafx.
Limits and retained behavior¶
- Unbounded mode (
--frames 0, the default) retains the full-scan behavior up toUINT32_MAXframes. - Endless sources without
--frameswill continue reading; operators should supply--frameswhen reading from pipes or synthetic devices. - Netflix golden assertions and public libvmaf C API remain 100% untouched.