Updated: 2026-06-27 (T-BUGHUNT-GO-RUST-BUILD-2026-06-27 closed — go-rust-build bug-hunt sweep re-derived cleanly onto current master. (1) pkg/gpu/detect.go::runProbe set cmd.WaitDelay but never gave the command a context — WaitDelay alone cannot cap a child that emits no output, so a hung GPU probe (wedged nvidia-smi, driver-blocked rocm-smi) could stall gpu.Detect()/node startup forever; switched to exec.CommandContext + context.WithTimeout(probeTimeout) plus a 1 s grace WaitDelay. (2) pkg/ai/infer.go::Registry.Infer ran vmafx-ort-runner via exec.Command (no context); added a ctx context.Context first parameter + exec.CommandContext with an inferTimeout fallback so the inference subprocess is cancellable (3 test call-sites updated). (3) Rust vmafx-sys/src/safe.rs::read_pictures borrowed &mut VmafPicture while keeping unref_picture public — a post-transfer double-free footgun; now consumes the pictures by value (use-after-move = compile error). The error path does NOT manually unref, matching the libvmaf ownership contract and the vmafx crate's Context::read_pictures (PR #1056 round-3 R3-2). NOTE: the vmafx crate's separate read_pictures double-free was ALREADY fixed by PR #1056 (manual unref dropped) — not re-touched here. (4) core/src/meson.build comment claimed enable_rust_features defaults true; core/meson_options.txt is value: false — comment corrected. No golden assertions touched. ALSO: restored docs/state.md content accidentally truncated to 0 bytes by PR #1055 (pelorus ABI re-vendor) and docs/rebase-notes.md likewise wiped by PR #1060 (FMA-ADM fix) — both restored in this change. Row added to Recently closed.) Updated: 2026-06-27 (T-BUGHUNT-FEATURE-CPU-2026-06-27 closed — feature-cpu bug-hunt sweep: (1) CIEDE2000 4:2:2 chroma-upsample flag swap in core/src/feature/ciede.c (scale_chroma_planes + _hbd) — horizontal index used ss_ver and vertical advance used ss_hor (transposed; upstream Netflix carries the identical bug), causing heap OOB read + wrong ciede2000 scores on YUV422P. Fixed to horizontal→ss_hor / vertical→ss_ver; regression tests test_ciede_scale_chroma_422_8b/_16b added. (2) cambi init() partial-failure leak — error returns left picture pool + contrast/tvi/c-values/histogram/mask buffers + feature-name dict allocated; routed all error paths through a fail: label calling the null-tolerant close_cambi(). SKIPPED: ssimulacra2 init OOM leak (already fixed in tree — goto fail frees all 12 buffers). Golden-safe: only CIEDE golden test runs 420P where the fix is a proven no-op (CIEDE2000 score 33.10755745833333 unchanged). Row added to Recently closed.) Updated: 2026-06-27 (T-BUGHUNT-DNN-2026-06-27 closed — three DNN (tiny-AI / ONNX Runtime) correctness bugs from the 2026-06-27 bug-hunt sweep fixed in fix/bughunt-dnn. (1) f32_to_f16_one (core/src/dnn/tensor_io.c) turned any large finite float that overflows the f16 range (e.g. 70000.0f, 1e30f) into a NaN instead of ±inf: the exp >= 31 branch propagated the input mantissa unconditionally, so every overflowing finite value got a non-zero half-mantissa = NaN. Fix: only set the NaN mantissa when the f32 input is actually inf/NaN (input biased exponent == 0xff); overflowing finite values now map to clean ±inf, mirroring ort_backend.c:fp32_to_fp16. Regression test test_f16_finite_overflow_to_inf in core/test/dnn/test_tensor_io.c. (2) copy_output_tensor (core/src/dnn/ort_backend.c) memcpy'd a non-float ORT output tensor as float, yielding garbage scores when a model declares a DOUBLE/INT64/INT32 output. Fix: branch per element type — FLOAT (memcpy), FLOAT16 (existing cast), DOUBLE/INT64/INT32 (per-element convert), -ENOTSUP for anything else. (3) vmaf_ort_infer skipped the positive-dimension and overflow checks that vmaf_ort_run performs; the shared build_input_tensor now rejects empty rank, non-positive dims, and an element-count product that overflows size_t (-EINVAL / -EOVERFLOW), guarding both callers. Regression test test_ort_infer_rejects_bad_shape in core/test/dnn/test_ort_internals.c. GOLDEN-SAFE: no Netflix golden assertion touched; the bugs are in the DNN tiny-AI path, not the VMAF metric engine. All 13 DNN meson tests pass (CPU + ORT 1.20.1). No ADR (bug fixes). Reproducer: vmaf_f32_to_f16 on 1e30f returned NaN; a model with a non-float output head scored garbage. (4) Round-2 finding R2-7 folded in: dnn_attach_nchw (core/src/libvmaf.c) + the luma fast-path in vmaf_dnn_session_open (core/src/dnn/dnn_api.c) narrowed ONNX spatial dims (int64_t) to int with only a positivity check; an untrusted export with H/W > INT_MAX would truncate into a wrong geometry. Both now reject dims > 32768 (mirrors VMAF_PIC_DIM_MAX) before the narrowing (CERT INT31-C; -ENOTSUP / fast-path fall-through). Compile-verified; CI + DNN build covers the gated path. Row added to Recently closed.) Updated: 2026-06-14 (T-PELORUS-SIDEDATA-READER-WEIGHTING-2026-06-14 closed — vmafx now reads the Pelorus per-frame side-data (vendored interop ABI, ADR-1113) and perceptually re-weights VMAF's spatial pooling: regions at high banding risk count more. New opt-in C-API (vmaf_set_perceptual_weight_enabled / _strength / vmaf_set_perceptual_sidedata, libvmaf/perceptual_weight.h); weight module core/src/feature/perceptual_weight.c; weighting hook in vmaf_feature_score_pooled; vf_libvmaf reader + perceptual_weight AVOption (ffmpeg-patch 0017). GOLDEN-GATE ISOLATION (#1 requirement): inert unless the opt-in is set AND a valid blob is present for the frame — the no-side-data path runs the literal upstream pooling expression and is byte-identical, so the Netflix golden pairs (no side-data) score bit-exact, proven by test_perceptual_weight.c. R1–R6 graceful degrade (grid==0 → frame-level scalar; bad ABI → unweighted + log). CPU-only, no GPU. ADR-1118. Row added to Recently closed.) Updated: 2026-06-14 (T-PU21-HDR-METRIC-MISSING-2026-06-14 closed — the fork had no perceptually-uniform HDR adapter: every FR metric (PSNR/SSIM/MS-SSIM/CIEDE2000/SSIMULACRA2/CAMBI/PSNR-HVS) is invalid on absolute-luminance HDR. Added a CPU pu21 extractor (pu21_psnr + pu21_ssim) that PU21-encodes the luma plane (PQ ST.2084 EOTF × 10000 → cd/m² → 7-coefficient transfer, default banding_glare) then scores PSNR (peak=256, no SDR cap) + a self-contained L=256 Gaussian SSIM. PU-SSIM uses its OWN SSIM (pu21_ssim.c), NOT the golden iqa_ssim (L=255). PQ input only for RC. Verified at places=4 vs the fp64 dossier oracle. ADR-1111. Row added to Recently closed.) Updated: 2026-06-13 (T-JSON-MODEL-FEATURE-NAME-DUP-KEY-LEAK-2026-06-13 closed — the JSON model parser's append_feature_name (core/src/read_json_model.c + C++23 twin read_json_model.cpp) strdup'd a feature name over feature[index].name without freeing any prior value. A duplicate feature_names key re-runs parse_feature_names from index 0, orphaning the first name; vmaf_model_destroy walks only the current slot occupants, so the orphan leaked on both the validation-error path (*model nulled, caller must not destroy) and the success path. Found by the nightly fuzz_json_model LeakSanitizer lane. Fix: free the prior name before the overwrite in both variants; ASan regression test test_json_model_feature_names_duplicate_key_no_leak added to core/test/test_model.c. No ADR (bug fix). Row added to Recently closed.) Updated: 2026-06-13 (T-FLOAT-VIF-AVX512-GOLDEN-REGRESSION-2026-06-13 closed — Netflix golden VMAFEXEC score regression (76.66729 vs assertion 76.66740433333332, Δ≈1.1e-4 > 5e-5 = places=4 threshold) caused by ADR-0504's AVX-512 float convolution dispatch. AVX-512 FMA uses a wider partial-sum tree (16 floats vs 8 in AVX2) producing different IEEE-754 rounding. Regression was latent because GitHub Actions runners lack AVX-512. Fix: remove HAVE_AVX512 dispatch blocks from vif_filter1d_s/sq_s/xy_s; float VIF now uses AVX2 path matching upstream Netflix/vmaf. Result: 271 passed / 12 skipped / 0 failed across all golden test files. ADR-1104. Row added to Recently closed.) Updated: 2026-06-13 (T-DOC-LEGACY-RUNNER-MISSING-DEPRECATION-2026-05-29 moved to Recently closed — state.md stale-open drift corrected; deprecation entry was already in docs/development/deprecations.md since PR #852 (2026-06-08). Cambi docs Vulkan section removed per ADR-0726 (stale reference to dropped backend). docs/metrics/cambi.md cleanup — no user-visible feature change.) Updated: 2026-06-13 (T-HIP-VIF-PARITY-PLACES4-2026-06-13 closed — integer_vif_hip had a residual parity gap of places~2.75 (max |HIP−CPU| ≈ 0.0018 per VIF scale) after ADR-0563 fixed the carry-bit catastrophe. Root cause: filter-loop boundary reads used clamp_i (replicate-edge) instead of the CPU's PADDING_SQ_DATA symmetric reflect. The CUDA twin uses a two-bounce mirror in its shared-memory load stage; HIP used clamp. Fix: add mirror2_i device helper (two-bounce mirror matching CPU + CUDA) and replace all 6 filter-loop clamp_i calls. Measured on gfx1030 wave32, 48 Netflix src01 frames: max |HIP−CPU| = 1e-6 (places~6), all frames ≤ 1e-4 (places=4). Pooled VMAF delta 0.000017. Parity test tolerance tightened 1e-3→1e-4. ADR-1103. Row added to Recently closed.) Updated: 2026-06-08 (T-LOCAL-EXPLAINER-BOOTSTRAP-NEON-RECAL-2026-06-08 closed + T-DOCKERFILE-LDCONFIG-MISSING-2026-06-08 closed — two CI failures from PR #855 tip (765af26c8) fixed in fix/master-855-tip-3-reds: (1) test_run_vmaf_runner_local_explainer_with_bootstrap_model asserted VMAF_LE_score at places=4 (tolerance 5e-5) against a Linux-calibrated value. After the NEON uint64-truncation fix in PR #834 / commit 43cf4c9aa, macOS arm64 Apple libm produces a slightly different SVM-prediction result (~6e-5 delta). All other bootstrap-score assertions in the same file already use places=3 + # ADR-0418 macOS-libm Δ relax; this assertion was added without the relaxation. Fix: recalibrate expected value to the post-NEON-fix value and relax to places=3 per ADR-0418 pattern. (2) Dockerfile was missing RUN ldconfig after make install. The NVIDIA CUDA Ubuntu 24.04 base image's /etc/ld.so.conf does not include /usr/local/lib/x86_64-linux-gnu; meson strips RPATH on install; without ldconfig the installed /usr/local/bin/vmaf binary could not find libvmaf.so.3 at runtime and exited silently, producing zero smoke-test stdout.) Updated: 2026-06-08 (T-CPP23-READ-JSON-MODEL-PENDING-2026-05-29 closed — stale row removed from Open; conversion already landed in PR #531 (2026-06-02) per ADR-0846 Wave 8.) Updated: 2026-06-08 (T-DOCKER-SMOKE closed — Docker image CI job promoted from advisory (continue-on-error: true) to blocking after 3 consecutive green master runs. A pixel-level VMAF score assertion step was added: vmaf --backend cpu on the 576x324 fixture pair from testdata/, expected mean ≈ 94.32 ± 0.5. Timeout raised from 30 min to 45 min to accommodate the additional score computation. chore/promote-docker-smoke-blocking.) Updated: 2026-06-08 (T-SYCL-ARC-FLOAT-SSIM-PARITY-2026-06-03 closed — added arc:dg2-g10 calibration entry to scripts/ci/gpu_ulp_calibration.yaml with float_ssim: 5.0e-4 (places=3) per Research-0985 §3 / Research-0730 §6.1 / ADR-0234; promoted test_sycl_float_ssim_parity to CI required-status list via new sycl-float-ssim-parity job in tests-and-quality-gates.yml + aggregator entry. Branch: fix/sycl-arc-float-ssim-calibration.) Updated: 2026-06-08 (T-PIC-POOL-ODR-CUDA-BUF-TYPE-2026-06-08 closed + T-CUDA-GPUMASK-TIMEOUT-2026-06-08 closed + T-COVERAGE-ORT-FLOOR-OVERSHOOT-2026-06-08 closed + T-VIFKS360-PYTEST-TIMEOUT-2026-06-08 closed — four pre-existing CI failures bundled in fix/pic-pool-odr-cuda-gpumask-cov-floor: (1) picture_pool_cpp23_lib compiled without -DHAVE_CUDA, shifting VmafPicturePrivate.buf_type from offset 16 to offset 56 in the pool-allocator TU vs consumer TUs (ODR violation); validate_pic_params read garbage and returned -EINVAL on every vmaf_read_pictures call; fixed by adding cpp_args with HAVE_CUDA/HAVE_SYCL to the static_library() call. (2) test_vmaf_cuda_gpumask.sh only checked ldconfig for libcuda.so — the CUDA 13.2 toolkit stub passes even without GPU hardware; added nvidia-smi -L guard (exit 77 = meson SKIP) plus meson timeout: 10 to prevent 30 s hangs. (3) ADR-0922 ratcheted ort_backend.c per-file floor from 78 to 83 but measured coverage is structurally capped at ~79% (ORT error-path lines unreachable without error injection); floor reset to 79 pending dedicated ORT error-injection test. (4) vifks360o97 uses a 65-tap Gaussian taking ~138 s in debug+gcov builds; pytest-timeout was 60 s, killing the suite before DNN/ORT tests contributed coverage; raised to 180 s.) Updated: 2026-06-07 (T-CUDA-DONE-PATH-DOUBLE-UNREF-2026-06-07 closed + T-COVERAGE-ORT-DEAD-ELSE-2026-06-07 closed — two regressions fixed in fix/cuda-done-path-double-unref-ort-coverage: (1) PR #838 added read_pictures_cuda_cleanup() in the done=true early-return branch of vmaf_read_pictures. When threaded mode is active, threaded_read_pictures_batch already calls vmaf_picture_unref on ref_host/dist_host at line 1858; the new call in the done=true branch then released the same pictures a second time, corrupting the pool free-list and deadlocking subsequent vmaf_picture_pool_fetch calls. Fixed by splitting read_pictures_cuda_cleanup into a full variant (host+device) and a _device_only variant used only in the done=true path. (2) ort_log_and_release_status had a dead else branch — ORT always produces a non-empty message when st != NULL — causing coverage to sit below the 83% security floor (ADR-0922). Fixed by collapsing to a single vmaf_log call with an inline ternary. Rows added to Recently closed.) Updated: 2026-06-07 (T-GO-CI-LIBVMAF-SO-RUNTIME-2026-06-07 closed + T-GO-CI-LASTHEARTBEAT-PRECISION-2026-06-07 closed + T-MCP-RESOURCE-URI-VALIDATION-REGRESSION-2026-06-07 closed + T-RUST-CI-BINDGEN-DOCTEST-2026-06-07 closed — four Go/Rust CI failures fixed in fix/go-rust-red-adr1041: (1) go test ./... failed with "libvmaf.so.3: cannot open shared object file" for cgo-linked packages; fixed by adding LD_LIBRARY_PATH: ${{ github.workspace }}/core/build-cpu/src to go test step env in go-ci.yml. (2) VmafxNode controller test sets Healthy = false when LastHeartbeat is stale failed with timestamp precision mismatch — metav1.NewTime captures nanoseconds but the Kubernetes API server truncates to second precision on read-back; fixed by .Truncate(time.Second). (3) TestResolveModelArgToPath_AllowlistEnforced failed because PR #791 inadvertently removed the libvmaf.ValidatePath calls added by PR #813, allowing MCP clients with VMAFX_MCP_DIRECT=1 to read arbitrary files; restored ValidatePath enforcement on all resolved path candidates. (4) cargo test -p vmafx-sys --all-features failed with doc-test compilation errors in bindgen-generated bindings.rs (C header comments contain "On x86 / x86_64:" and backticks that are not valid Rust doc-test code); fixed by adding [lib] doctest = false to vmafx-sys/Cargo.toml. Rows added to Recently closed.) Updated: 2026-06-07 (T-CI-VULKAN-OPTION-REMOVED-2026-06-07 closed + T-UBSAN-ENUM-INVALID-VALUE-LOG-OPT-2026-06-06 closed (real fix) + T-TSAN-OOM-ABORT-POOL-UAF-2026-06-07 closed + T-COVERAGE-GATE-ORT-BACKEND-FLOOR-BREACH-2026-05-30 closed (83% regression) — four tests-quality CI failures fixed: (1) Vulkan jobs failed with "Unknown options: enable_vulkan" — ADR-0726 removed the backend and its meson option; fixed by disabling both vulkan-vif-cross-backend and vulkan-parity-matrix-gate jobs with if: false. (2) test_log SIGABRT under UBSan — prior cast-to-int fix in vmaf_log (log.cpp) did not prevent the UBSan enum-invalid-value violation because UBSan fires on the function parameter LOAD before any code runs; fixed by annotating vmaf_log with __attribute__((no_sanitize("enum"))) guarded by clang/gcc version check (ADR-1080). (3) test_gpu_picture_pool_uaf SIGABRT under TSan — TSan's allocator aborts the process on huge allocations instead of returning NULL; fixed by adding TSAN_OPTIONS: allocator_may_return_null=1 to the sanitizer step env (same pattern as ASAN_OPTIONS already present). (4) ort_backend.c coverage dropped to 79% after ADR-0922 ratcheted the per-file floor from 78% to 83% — fixed by adding 12 EP-device fallback tests to test_ort_internals.c covering try_append_cuda/openvino/rocm/coreml and the ADR-0113 two-stage CreateSession fallback path.) Updated: 2026-06-07 (T-NEON-ANY-NONZERO-UINT64-TRUNC-2026-06-07 closed + T-CPUMASK-NEG-ONE-REJECTED-2026-06-07 closed + T-PTHREAD-POOL-WINDOWS-MISSING-2026-06-07 closed + T-CI-VULKAN-STALE-MATRIX-ROWS-2026-06-07 closed — four libvmaf-build-matrix red-badge root causes fixed. (1) neon_any_nonzero_s32 in motion_v2_neon.c reinterpreted int32x4_t as uint64x2_t, then OR'd the two uint64 lanes and cast to uint32. Lanes with non-zero values only in the upper 32 bits (e.g. when int32[0]==0, int32[1]!=0) produced a uint64 that is non-zero but whose uint32 truncation is 0, falsely reporting the row as all-zero. On checkerboard input, this skipped nearly all rows of the x-phase convolution, producing motion=0.0 on macOS arm64. Fix: replace uint64 reinterpret with a direct uint32-level OR of the four lanes via vget_low/high_u32 + vorr_u32. (2) Python harness disable_avx option emitted --cpumask -1; parse_unsigned (ADR-1088) now rejects negative strings; updated to 4294967295 (0xFFFFFFFF). (3) picture_pool_cpp23_lib and gpu_picture_pool_cpp23_lib in core/src/meson.build lacked dependencies: [pthread_dependency]; on Windows MSVC the win32 pthreads shim include path was not added, causing fatal error: 'pthread.h' file not found in both Windows GPU legs. Fix: add pthread_dependency to both static_library() calls. (4) Vulkan Build — Ubuntu Vulkan (T5-1b runtime) and Build — macOS Vulkan via MoltenVK (advisory) matrix rows passed -Denable_vulkan=enabled which no longer exists (ADR-0726); removed both rows. Rows added to Recently closed.) Updated: 2026-06-07 (T-BUILD-MATRIX-MESON-LIBVMAF-PATHS-2026-06-07 closed + T-ASAN-ALLOCATOR-NULL-RETURN-2026-06-07 closed + T-MOTION-V2-COVERAGE-LSAN-LEAK-2026-06-07 closed — three CI regressions fixed in state-sweep-fix: (1) all 20+ libvmaf-build-matrix jobs failed with "meson setup libvmaf core/build" — the libvmaf/ source directory no longer exists after ADR-0700 rename to core/; fixed by updating 4 meson setup/ninja -C invocations in libvmaf-build-matrix.yml. (2) test_gpu_picture_pool_uaf SIGABRTed under ASan because the allocator interceptor aborted on the intentionally-oversized alloc that exercises the NULL-return path; fixed by adding ASAN_OPTIONS: allocator_may_return_null=1 to the sanitizer step in tests-and-quality-gates.yml. (3) test_integer_motion_v2_coverage multi-frame tests (test_motion_v2_three_frame_flow, ..._moving_average_branch, ..._10bit_extract) manually set ctx->fex->prev_ref as a raw struct copy, bypassing vmaf_picture_ref; the PREV_REF wrapper in feature_extractor.cpp unreffed the raw copy and took a counted ref on each frame, leaving the last frame's ref count at 2; context_destroy + test loop only decremented once each — leak. Fix: remove all manual prev_ref assignments and memsets; the PREV_REF wrapper manages the field automatically. Rows added to Recently closed.) Updated: 2026-06-07 (T-FEX-FLAGS-ZERO-GPU-MISSELECT-2026-06-07 closed — vmaf_get_feature_extractor_by_feature_name(name, flags=0) had a no-op filter (if (flags && ...) always false for 0), letting GPU-flagged twins (SYCL/CUDA/HIP) sorted before their CPU counterparts in feature_extractor_list be returned to CPU-only callers. The selected extractor's init() guard (!fex->sycl_state) fired on every frame, returning -EINVAL and breaking vmaf_read_pictures entirely in all-backends builds without a GPU context. Fix: when flags == 0, skip any extractor with VMAF_FEATURE_EXTRACTOR_CUDA | SYCL | HIP bits set; CPU twins are then selected. ADR-1100. All 8 test_pic_preallocation sub-tests pass. Row added to Recently closed.) Updated: 2026-06-07 (T-SYCL-MOTION-ADD-UV-SIGSEGV-2026-06-07 closed — two root causes resolved (ADR-1099): (1) SYCL test executables linking libvmaf.a were not getting -fsycl at link time; without it clang-offload-wrapper is skipped, ProgramManager never registers device kernels, and the first q.submit() null-dereferences inside getDeviceKernelInfo. Fix: embed -fsycl in sycl_dependency.link_args in core/src/meson.build. (2) vmaf_feature_score_at_index queries in the test used raw VMAFscore names, but with motion_add_uv=true the feature-name system stores scores under aliased names (integer_motion2_mau, float_motion2_mau). Queries updated. should_fail removed. Row moved to Recently closed.) Updated: 2026-06-07 (T-HELM-ROLLING-UPDATE-CORRECTNESS-2026-06-07 closed — four rolling-update correctness gaps fixed: node Deployment now has explicit RollingUpdate strategy (maxUnavailable: 0), probes use tcpSocket instead of httpGet (no HTTP listener), PDB default is minAvailable: 1, terminationGracePeriodSeconds: 120. ADR-1094. Closed by PR #822.) Updated: 2026-06-07 (T-OTEL-GRPC-TRACE-CONTEXT-2026-06-07 closed — vmafx-server gRPC server was missing otelgrpc.NewServerHandler(), pkg/score.Dial was missing otelgrpc.NewClientHandler(), and ObserveScoreLatency passed context.Background() discarding trace context. Fixed by adding stats handlers and forwarding caller context. ADR-1095. Closed by PR #820.) Updated: 2026-06-07 (T-COMPAT-PYTHON-VMAF-MODE-SHIM-2026-06-07 closed — ProcessRunner.run in compat/python-vmaf/__init__.py used setdefault to inject C-locale, which is a no-op when the caller passes env=<dict>. Fix: build a merged env and unconditionally stamp LC_ALL=C/LANG=C on top. Same fix applied in core/matlab_feature_extractor.py. Stale python/vmaf/ path references in config.py docstrings updated to compat/python-vmaf/. No ADR: bug fix. Closed by PR #817.) Updated: 2026-06-07 (T-VMAF-PER-SHOT-UNKNOWN-OPT-HELP-CONFLATION-2026-06-07 closed — per_shot_parse_args used '?' as both the --help short-option and the getopt unknown-option sentinel, silently printing help on any mistyped flag. Fix: remap --help to 'H'; treat '?' as parse error. Also replace fseek((long)chroma_bytes) with fseeko/_fseeki64 to avoid 32-bit truncation. No ADR: bug fix. Closed by PR #816.) Updated: 2026-06-07 (T-ROI-FRAME-BYTES-ODD-DIMS-2026-06-07 closed — frame_bytes() in core/tools/vmaf_roi.c computed chroma plane sizes using integer-truncating arithmetic, causing fseeko() to land at the wrong byte offset for odd-dimension inputs and producing incorrect saliency maps. Fix: replace truncating expressions with ceiling-division cw = (w+1)/2, ch = (h+1)/2; four regression tests added. No ADR: bug fix. Closed by PR #815.) Updated: 2026-06-07 (T-BENCH-ALLOC-UNCHECKED-CLOCK-UB-2026-06-07 closed — vmaf_bench.c called vmaf_picture_alloc without checking the return value in both the warm-up and timed loops; on ENOMEM the immediately-following yuv_pair_read_frame dereferenced a null pointer. Additionally, clock() measured CPU time not wall-clock time, causing FPS measurements to be inaccurate on multi-threaded workloads. Fix: guard both vmaf_picture_alloc calls; replace clock() / CLOCKS_PER_SEC with clock_gettime(CLOCK_MONOTONIC, ...). ADR-1081. Closed by PR #790.) Updated: 2026-06-07 (T-ORT-ERROR-MSG-LOGGING-2026-06-07 closed — every api->ReleaseStatus() call in error paths of core/src/dnn/ort_backend.c silently discarded the ORT error message string, returning bare -EIO/-EINVAL with no diagnostic context. Additionally, the GetTensorElementType hard-error guard (PR #129, commit b8a51866e) was accidentally dropped when commit 35907a087 re-added ort_backend.c from a stale state during the Docker/CUDA 13.2.1 alignment PR, leaving input_elem_types[i]/output_elem_types[i] at ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED on failure instead of returning -EINVAL. Fix: add ort_log_and_release_status() helper that calls api->GetErrorMessage(), emits vmaf_log(WARNING), then ReleaseStatus(); update ORT_TRY macro; restore GetTensorElementType hard-error guard and #include "../log.h". No ADR: bug fix restoring previously-accepted behaviour. Closed by PR #792.) Updated: 2026-06-07 (T-CUDA-STREAM-EVENT-LEAK-INIT-PATHS-2026-06-07 closed — CUDA extractors (vmaf_cuda_picture_alloc, integer_vif_cuda, integer_adm_cuda, integer_motion_cuda, ssimulacra2_cuda) used a single shared fail: label for all error-path cleanup, which freed resources that had not been allocated yet on early failures, or skipped resources that had been allocated on late failures. Fix: replace single fail: with graduated cleanup chains (fail_event: / fail_stream: / fail_module: etc.) matching the allocation order. ADR-1090. Closed by PR #805. Row added to Recently closed.) Updated: 2026-06-07 (T-FRAMESYNC-PRODUCER-DEATH-DEADLOCK-2026-06-07 closed — vmaf_framesync_retrieve_filled_data contained an infinite pthread_cond_wait loop with no exit path for producer failure. If a producer thread died without calling vmaf_framesync_submit_filled_data, the consumer blocked forever. Additionally, calling vmaf_framesync_destroy while a consumer was in cond_wait was POSIX UB. Fix: add aborted flag + vmaf_framesync_abort() that sets the flag and broadcasts; retrieve_filled_data checks the flag and returns -ECANCELED; vmaf_framesync_destroy calls abort as a safety-net. ADR-1092. Closed by PR #803. Row added to Recently closed.) Updated: 2026-06-07 (T-GPU-DISPATCH-ENV-FAST-PATH-DATA-RACE-2026-06-06 closed — fast-path lockless scan in vmaf_gpu_dispatch_env_get (gpu_dispatch_env.cpp) read std::string_view + std::optional<std::string> without synchronisation while the slow-path writer populated them under the mutex — a data race under C++ [intro.races], TSan-detectable. Fix: add std::atomic<bool> ready publication flag per EnvRow; slow-path stores with memory_order_release after populating the slot; fast-path loads with memory_order_acquire before reading var_name/value. ADR-1068. Closed by PR #753. Row added to Recently closed.) Updated: 2026-06-07 (T-MCP-RESOURCE-URI-VALIDATION-2026-06-07 closed — cmd/vmafx-mcp/impl_direct.go::resolveModelArgToPath returned absolute paths supplied via model: "path=/arbitrary/path" or bare absolute paths without passing them through libvmaf.ValidatePath, allowing an MCP client with VMAFX_MCP_DIRECT=1 to read arbitrary files on the host filesystem. Fixed by routing every resolved candidate through libvmaf.ValidatePath before returning. Regression test: TestResolveModelArgToPath_AllowlistEnforced. Only affects operators with VMAFX_MCP_DIRECT=1 set; the default subprocess path was already protected. Row added to Recently closed.) Updated: 2026-06-06 (T-NEON-MOTION-ZERO-SKIP-2026-06-06 closed — motion_score_pipeline_8_neon and motion_score_pipeline_16_neon in core/src/feature/arm64/motion_v2_neon.c used vaddvq_s32 (signed horizontal sum) to check whether the phase-1 y-convolution row was all-zero. On checkerboard and other alternating-pixel inputs, positive and negative lane values cancelled each other to give sum = 0 even when individual lanes were non-zero, causing x_conv_row_sad_neon to be skipped for affected rows, producing motion_score = 0.0 on macOS ARM64. Fix: replace neon_hadd_s32 (removed) with neon_any_nonzero_s32 (OR-fold via uint64 reinterpret) so any set bit triggers the accumulation path. Row added to Recently closed.) Updated: 2026-06-06 (Second-pass sweep — PRs #712–#747 landed after the #691–#711 batch. Four state.md gaps backfilled: T-FFMPEG-PATCHES-SCORE-FMT-GAP-2026-06-06 closed (PR #723 / ADR-1064 — wire score_fmt to all FFmpeg filters); T-VENDORED-CJSON-PDJSON-SECURITY-2026-06-06 closed (PR #725 / ADR-1061 — five pdjson/cJSON security/correctness bugs); T-GO-STATICCHECK-R10-TIMER-BODY-2026-06-06 closed (PR #729 / ADR-1065 — Go timer leak + body cap + ReadTimeout); T-JSON-MODEL-SLOPES-FEATURE-CAP-OOB-2026-05-30 closed (PR #743 / ADR-0887 — heap-buffer-overflow in vmaf_model_destroy). Squash-merge commits for these PRs lost the state.md additions from their pre-squash branch commits; this sweep restores them. Other notable PRs in the batch that require no bug-row changes: #712 Doxygen drift fix, #713 CI SHA-pin, #714 vmaf-tune CLI tests, #715 C++23 r10 error-path cleanup, #716 fuzz harnesses, #717–#722 coverage rounds, #724 stale-row cleanup (already on master), #726 ADR cross-ref repair, #727 Rust clippy, #728 SVM/DRI regression tests, #730 orphan Vulkan file deletion, #731 CUDA adm_cm_module fix, #732 MSVC cpp_std per-target, #733–#735 CI fuzz/TSan fixes, #736 framesync seed fix, #737–#740 doc/CI/ffmpeg-patch fixes, #741–#742 test failures, #744–#747 further test failure fixes.) Updated: 2026-06-06 (state.md backfill for PRs #765–#771. Three rows added to Recently closed: T-GPU-POOL-UAF-OOM-ASAN-UBSAN-GAP-2026-06-06 (PR #767 — ASan+UBSan exclusion for huge-alloc tests), T-HIP-MOTION-DEBUG-BOOL-SYCL-GRAPH-DANGLING-2026-06-06 (PR #768 — HIP bool "1"→"true" + SYCL dangling priv SIGSEGV), T-MOTION-FIVE-FRAME-WINDOW-PYTHON-SKIP-2026-06-06 (PR #771 — Python test suite skip pending ADR-0337 C plumbing). PRs #765/#766/#769 were already tracked; PR #770 adds no new bug row (CI wiring fix only).) Updated: 2026-06-06 (2026-06-05/06 batch sweep — 18 PRs merged (#691–#711). T-NEON-FMA-FLOAT-ADM-DWT2-2026-06-06 opened. T-MSVC-CPP-STD-C23-2026-06-06 and T-CI-JIMVER-CUDA-133-NOT-AVAILABLE-2026-06-06 closed. Key: PR #695 reverts float-ADM SIMD wiring (PR #685) due to NEON FMA divergence — ADR-1057. PR #694 fixes macOS Clang+Metal linker failure (extern "C" in feature_collector.h). PRs #696/#700/#701/#699 fix SYCL/test build breaks. PR #702 restores motion_v2 CPU registration (duplicate of #673 content, different context). PR #704/#711 doc/fence fixes. PR #705 aligns DNN int8 test to ADR-1032 fp32-fallback. PR #706 repairs 11 MCP Smoke CI failures. PRs #707–#710 are r9 housekeeping: /dev/dri read_only=false, svm double-free, Docker CUDA 13.2.1 align, vif.c cppcheck bracket. Duplicates / housekeeping not repeated in Recently closed — only substance rows added. Stale open-section sweep: 2 rows removed from Open already in Recently Closed (T-CUDA-FILTER1D-RES-DISPATCH-CONFLICT-2026-05-29, duplicate T-CPP23-READ-JSON-MODEL-PENDING row). T-GPU-COVERAGE duplicate removed. Net Open change: -2 rows.) Updated: 2026-06-06 (T-NEON-FMA-FLOAT-ADM-DWT2-2026-06-06 opened — PR #685 (commit b1a6c0d62) wired the AdmSimdDispatch table so float-ADM NEON kernels were called at runtime. test_float_adm_dwt2_bitexact in test_float_adm_simd.c fails on ARM CI with a 1-ULP FMA gap: the NEON DWT2 uses hardware FMA by default; the scalar reference does not. A follow-up #pragma clang fp contract(off) carve-out (PR #690) was insufficient across all ARM toolchain configs tested in CI. Resolution: revert PR #685 in full (this PR). The float-ADM SIMD kernels remain compiled but are no longer dispatched. Follow-up required: rewire dispatch with an FMA-safe NEON DWT2 path. ADR-1057. Row added to Open until follow-up lands.) Updated: 2026-06-06 (T-MSVC-CPP-STD-C23-2026-06-06 closed — Build — Windows MSVC + CUDA CI leg failed at meson configure with ERROR: None of values ['c++23'] are supported by the CPP compiler after ADR-1003 set cpp_std=c++23 in default_options. Meson's MSVC backend accepted-values list omits c++23; valid tokens are c++11/14/17/20/vc++latest. Fix (ADR-1056): remove cpp_std from default_options; inject via add_project_arguments guarded by get_option('cpp_std')=='none' — MSVC receives /std:c++latest, all other compilers receive -std=c++23. SYCL leg unaffected. Row added to Recently closed.) Updated: 2026-06-06 (T-CI-JIMVER-CUDA-133-NOT-AVAILABLE-2026-06-06 closed — Build — Linux (GCC, all backends) and Build — Ubuntu SYCL + CUDA failed with Error: Version not available: 13.3.0. The Jimver/cuda-toolkit action v0.2.35 does not include 13.3.0 in its installer index; the version was bumped to 13.3.0 in PR #664 which works for apt-based installs (Containerfile, Dockerfile) but not for the GH-action path. Fix: revert CI CUDA pin to 13.2.0 in build.yml and libvmaf-build-matrix.yml (PR #691). Container installs stay at 13.3 (NVIDIA apt repo has it). No ADR: CI pin fix with no user-visible delta. Row added to Recently closed.) Updated: 2026-06-04 (T-MINGW64-CONSTINIT-MUTEX-2026-06-04 closed — Build — Windows MinGW64 CI failing with constinit variable 'g_lock' does not have a constant initializer: std::mutex::mutex() is not constexpr in GCC/MinGW libstdc++, so constinit std::mutex is ill-formed. Fix: drop constinit from g_lock in core/src/gpu_dispatch_env.cpp; static-duration zero-init prevents dynamic-init races regardless. std::array<EnvRow, kTableCap> retains constinit (aggregate-init is constexpr). Row added to Recently closed.) Updated: 2026-06-04 (T-STRING-VIEW-MISSING-INCLUDE-2026-06-04 closed — added #include <string_view> to core/tools/vmaf.cpp; unblocks macOS/FFmpeg-macOS builds. T-CUDA-CLOSE-VMAFCUDAFUNCTIONS-2026-06-04 closed — replaced fabricated VmafCudaFunctions type with correct CudaFunctions in 13 CUDA close callbacks; unblocks Docker/CUDA builds. Both fixed in fix/macos-docker-platform-unblock.) Updated: 2026-06-04 (T-SIMD-DIVERGENCE-CLUSTER-2026-06-04 closed — test_ms_ssim_decimate, test_psnr_hvs_simd, and test_ssimulacra2_simd now pass on master. Fix PR #681 (commit 9bd6bf80) cherry-picked three FMA-unification commits onto master: 15001cd6c3 (ffp-contract carve-outs + ssimulacra2 X reorder), 83698bd5b (-fp-model=precise for icx), and 31dce40e1 (FMA round-2, extend -fp-model=precise to libvmaf_feature_static_lib). ADR-0891. Row moved from Open to Recently closed.) Updated: 2026-06-04 (RC-push batch — ~66 PRs merged across P0-unblock (#648/#653/#654/#655), doc sweep (#613–#621), bug-fix trains r5 (#627–#633) and r6 (#637–#642), orphan-rescue (#665–#670), r9 helm/grpc/coverage (#671/#672), ARM regression (#673), unshipped-lands (#674/#676/#677), and CUDA 13.3 bump (#664). ALL R6 HIGH bugs closed: T-R6-CUDA-ADM-CM-OPERATOR-PRECEDENCE, T-R6-CUDA-VIF-FILTER1D-WIDTH-GUARD, T-R6-SYCL-VIF-RD-STRIDE-OOB, T-R6-SYCL-MOTION-UV-QUEUE-SYNC, T-HIP-ADM-DECOUPLE-DANGLING-BODY, T-HIP-VIF-WAVEFRONT-CARRY-DROP, T-METAL-FLOAT-MOTION-VERTICAL-HALO, T-CPU-SCORING-NAN-UB-GUARDS, T-VMAF-INIT-DOUBLE-INIT-GUARD. CUDA pin bumped 13.2.0 → 13.3.0. CLAUDE.md §1 SPDX corrected BSD-3-Clause-Plus-Patent → BSD-2-Clause-Patent. Branch hygiene: remote 178 → 73, local 861 → 286, worktrees 253 → 234. SIMD divergence cluster (test_ms_ssim_decimate / test_psnr_hvs_simd / test_ssimulacra2_simd) fix PR opening in parallel — still NOT on master. T-MACOS-SIGSEGV-UNRESOLVED-2026-05-19 closed — compile error (not SIGSEGV): integer_ssim_moments_t gated behind #if ARCH_X86; fix promoted typedef to shared header; landed PR #654 / ADR-1040.) Updated: 2026-06-04 (T-ARM-MOTION-V2-MISSING-2026-06-04 closed — vmaf_get_feature_extractor_by_name("motion_v2") returned NULL on CPU-only builds after PR #532 removed the integer_motion_v2.c registration. Fix: re-add integer_motion_v2.c to meson.build CPU sources, restore extern declaration and list entry in feature_extractor.c, and move get_score(motion2_score) to after flush() in test_integer_motion_coverage.c to match post-port contract. ADR-1052. Rows added to Recently closed.)__Updated: 2026-06-04 (T-R9-HELM-SCHEMA-DRIFT-2026-06-04 closed — four Helm chart correctness gaps fixed: (1) storage key added to values.yaml; (2) networkPolicy/auth/otelCollector added to schema; (3) gpu.count minimum raised to 1; (4) gpu.enabled added to required. ADR-1047. T-R9-VMAFTUNE-DURATION-SENTINEL-2026-06-04 closed — duration_s added to _stamp_tracked_default_sentinels tuple so ladder --duration is correctly detected as user-provided vs default. ADR-1048. T-R9-FEEDBACK-FIXED-RETRY-2026-06-04 closed — fixed 10s retry in online_feedback drainLoop replaced with exponential backoff (2s→2m). ADR-1049. Rows added to Recently closed.)__Updated: 2026-06-04 (T-CI-WF-CONCURRENCY-TIMEOUT-2026-06-04 closed — CI workflow concurrency guards + job timeouts + SHA-pinned action tags added. ADR-1035. Row added to Recently closed.) Updated: 2026-06-04 (T-DOCS-BROKEN-ADR-LINKS-2026-06-04 closed — Two broken intra-doc links to nonexistent ADR-0720 fixed (correct target: ADR-0865). 11 orphaned mkdocs.yml nav entries added. Row added to Recently closed.) Updated: 2026-06-04 (T-MCP-PRECISION-DEFAULT-DRIFT-2026-06-04 closed — Go and Python MCP surfaces used precision "17" / "6" defaults; C CLI default is %.6f ("legacy"). Fixed both surfaces. ADR-1038. Row added to Recently closed.) Updated: 2026-06-04 (T-VENDORED-SVM-REALLOC-OOM-2026-06-04 closed — three CERT MEM04-C realloc OOM defects in core/src/svm.cpp fixed. ADR-1039. Row added to Recently closed.) Updated: 2026-06-04 (T-SPDX-SVM-COPYRIGHT-2026-06-04 closed — SPDX identifier BSD-3-Clause-Plus-Patent corrected to BSD-2-Clause-Patent in 8 package manifests; missing libsvm BSD-3-Clause copyright added to svm.cpp. ADR-1036. Row added to Recently closed.) Updated: 2026-06-04 (T-SYCL-SPEED-INCOMPLETE-TYPE-ACCESS-2026-06-04 closed — Linux GCC SYCL build failure in speed_chroma_sycl.cpp and speed_temporal_sycl.cpp: both files accessed sycl_state->queue as a raw pointer cast, but VmafSyclState is an incomplete type at the call site. Fix: replace all 8 direct member dereferences with vmaf_sycl_get_queue_ptr(s->sycl_state). No kernel logic changed. Row added to Recently closed.) Updated: 2026-06-04 (T-INTEGER-SSIM-MOMENTS-TYPE-NON-X86-2026-06-04 closed — integer_ssim_moments_t promoted to shared integer_ssim.h header; macOS arm64 / Windows arm64 build unblocked. ADR-1040. Row added to Recently closed.) Updated: 2026-06-04 (T-CLI-NARROWING-CASTS-VMAF-CPP-2026-06-04 closed — VmafPictureConfiguration initializer in core/tools/vmaf.cpp:1360-1362 had three implicit int to unsigned narrowing conversions (.w = info.pic_w, .h = info.pic_h, .bpc = common_bitdepth). Clang -std=c++11 strict mode treats these as hard errors; GCC accepted them silently. Fix: add explicit static_cast<unsigned>(...) for all three. No ADR per CLAUDE §12 r8. Row added to Recently closed.) Updated: 2026-06-04 (T-SIMD-PSNR-16BIT-SCALAR-TAIL-OVERFLOW-2026-06-04 closed — scalar-tail loops in psnr_sse_line_16_avx2, psnr_sse_line_16_avx512, and psnr_sse_line_16_neon computed squared error as (int32_t)e * (int32_t)e where e can reach ±65535 — signed-integer overflow UB (65535² > INT32_MAX, UBSan-flagged). Fix: replace with (uint64_t)(uint32_t)abs((int32_t)ref[j] - (int32_t)dis[j]) * e unsigned-multiply pattern, mirroring sse_line_16_c in integer_psnr.c. No ADR per CLAUDE §12 r8. Row added to Recently closed.) Updated: 2026-06-04 (T-CPU-SCORING-NAN-UB-GUARDS-2026-06-04 opened — Round-6 audit surfaced 11 CPU-path scoring edge-case bugs across APSNR (log10(0)), MS-SSIM (pow(neg,frac) + size overflow), float SSIM/MS-SSIM (convert_to_db NaN), iqa/ssim_tools.c (assert abort), ADM (score_aim uninit + harmonic-mean NaN + skip_scale0 sentinel), MOTION (wrong bilinear stride + OOM crash), and CAMBI (v_band_size uint16_t overflow). All fixed in one cluster PR. DRAFT PR: fix/r6-cpu-scoring-nan-ub-guards. ADR-1033. Row will be moved to Recently closed when PR merges.) _Updated: 2026-06-04 (T-VMAF-INIT-DOUBLE-INIT-GUARD-2026-06-04 closed — three HIGH-severity API-contract bugs fixed: (1) vmaf_init unconditionally overwrote vmaf, leaking the old context on double-init; fix: return -EINVAL when vmaf is non-NULL. (2) vmaf_close left the caller pointer dangling; fix: document pointer-invalidity contract and null-after-close pattern in the public header. (3) DNN vmaf_dnn_session_open returned error on missing int8 sidecar instead of falling through to fp32; fix: rc=0 + fall-through. ADR-1032. Rows added to Recently closed.) _Updated: 2026-06-04 (T-R6-CUDA-VIF-FILTER1D-WIDTH-GUARD-2026-06-04 and T-R6-CUDA-ADM-CM-OPERATOR-PRECEDENCE-2026-06-04 closed — two HIGH-severity CUDA kernel arithmetic bugs fixed: (1) filter1d.cu 16-bit vertical rd-filter upper-bound guard typo fwidth_rd - fwidth_rd → fwidth - fwidth_rd, preventing OOB reads into vif_filt.filter[scale+1]; (2) adm_cm.cu lines 373+712 x_sq operator-precedence defect + add_shift_sq >> shift_sq → + add_shift_sq) >> shift_sq matching CPU reference macro. PR: fix/cuda-vif-filter1d-adm-cm-opprec.) Updated: 2026-06-04 (T-R6-SYCL-VIF-RD-STRIDE-OOB and T-R6-SYCL-MOTION-UV-SYNC closed — two HIGH-severity SYCL correctness bugs: (1) integer_vif rd_stride used truncating e_w/2 causing OOB writes on odd widths in both scalar and SIMD-16 kernel variants + allocation undersize; (2) integer_motion UV H2D copies via primary queue not synchronized before combined_queue compute causing wrong UV motion scores when motion_add_uv=true. ADR-1034. Rows added to Recently closed.) Updated: 2026-06-04 (T-HIP-ADM-DECOUPLE-DANGLING-BODY-2026-06-04 closed — core/src/feature/hip/integer_adm/adm_decouple.hip line 27 contained a bare { ... return temp; } block (body of get_best15_from32) without a declaration; the declaration was stripped in an earlier edit leaving an invalid C++ TU. Fix: #include "adm_decouple_inline.hip" added after common.h; duplicate COS_1DEG_SQ constexpr removed. ADR-1030. Row added to Recently closed.) Updated: 2026-06-04 (T-HIP-VIF-WAVEFRONT-CARRY-DROP-2026-06-04 closed — wavefront_reduce_i64 in core/src/feature/hip/integer_vif/vif_statistics.hip reassembled lo+hi halves with bitwise OR instead of integer addition, silently dropping carry from the lo-half lane-sum into the upper 32 bits when the sum exceeded 2^32. Affected num_non_log and other large VIF accumulators on high-contrast content. Fix: (int64_t)((uint64_t)lo + ((uint64_t)hi << 32)). ADR-1030. Row added to Recently closed.) Updated: 2026-06-04 (T-METAL-FLOAT-MOTION-VERTICAL-HALO-2026-06-04 closed — float_motion.metal used TILE_H=16 and wg_oy = bid.y * 16 (no vertical halo) for the shared-memory tile. The 5-tap vertical filter requires rows wg_oy − 2 and wg_oy − 1; without them tile_row went negative and clamped to 0, reading the wrong row. Every workgroup except the first had 2 corrupted blurred rows per frame, producing wrong SAD scores. Fix: TILE_H=20, wg_oy = bid.y * 16 - HALF_FW, ty = lid2.y + HALF_FW in both 8bpc and 16bpc kernels. ADR-1030. Row added to Recently closed.) Updated: 2026-06-04 (T-R5-MEMORY-ORDERING-2026-06-04 closed — three HIGH-severity concurrency bugs: acq_rel ref-count ordering, mutex-destroy-after-unlock in feature_collector, picture_pool slot copy before unlock. ADR-1020. Row added to Recently closed.) Updated: 2026-06-04 (T-Y4M-DST-BUF-READ-SZ-OVERFLOW-2026-06-04 closed — five dst_buf_read_sz arithmetic expressions in core/tools/y4m_input.c used bare int * int multiplication for chroma branches 420/420jpeg/420mpeg2, 420p10, 420p12, 422p10, and 422p12. Signed overflow underallocated dst_buf_read_sz vs dst_buf_sz; subsequent fread at line 931 could overrun the heap buffer. Fix: (size_t) casts on all five expressions. ADR-1022. Row added to Recently closed.) Updated: 2026-06-04 (T-CPP-STD-C23-BUMP-INCONSISTENCY-2026-06-04 closed — cpp_std=c++11 project default inconsistent with C++23 wave files; test_feature_collector_coverage link-fail fixed. ADR-1003. Row added to Recently closed.) Updated: 2026-06-04 (T-HIP-SPEED-INTERNAL-IMPL-MISSING-2026-05-31 closed — speed_internal_init_dimensions / speed_internal_float_stride implementation landed in core/src/feature/speed_internal.c via ADR-0964 / PR #465. speed_chroma_hip and speed_temporal_hip parity tests now ship in round 5 (ADR-1004), closing the round-4 carryover. HIP extractor parity coverage lifts to 17/17 (100%) for all non-deferred kernels. Row moved to Recently closed.) Updated: 2026-06-04 (T-ARM64-CLANG-STDATOMIC-TYPEDEF-2026-06-04 closed — Build — Ubuntu ARM clang (CPU) CI failing with 12 errors in feature_extractor.cpp: typedef redefinition with different types ('_Atomic(int)' vs 'atomic<int>'). Root cause: framesync.h included <stdatomic.h> unconditionally; on Ubuntu 24.04 ARM (aarch64, clang-18 + GCC-14 headers), GCC-14's <atomic> transitively includes GCC's <stdatomic.h> which defines atomic_int = atomic<int>, then clang-18's <stdatomic.h> (pulled in by framesync.h) tries to redefine atomic_int as _Atomic(int) — conflict. feature_extractor.h already had the correct #if defined(__cplusplus) guard for its own <stdatomic.h>, but framesync.h (included after) circumvented it. Fix: add the matching #if !defined(__cplusplus) guard to framesync.h's <stdatomic.h> include. The header's public interface is entirely opaque (VmafFrameSyncContext is forward-declared, not defined); no atomic type is part of the public contract. C TUs that include framesync.h are unaffected. Row added to Recently closed.) Updated: 2026-06-04 (T-TSAN-STDATOMIC-CXX-BUILD-BREAK-2026-06-04 closed — TSan CI job (sanitizers.yml) was failing to compile feature_extractor.cpp with 12 typedef redefinition errors. Root cause: framesync.h unconditionally included <stdatomic.h> despite declaring no atomic types; with GCC 14 + Clang-18, GCC's C++ stdatomic.h wrapper includes <atomic> (making atomic_int = atomic<int>), then Clang-18's own stdatomic.h fires and tries to typedef _Atomic(int) atomic_int — a typedef conflict. Fix (ADR-0999): guard #include <stdatomic.h> in framesync.h with #if !defined(__cplusplus) (include is vestigial there), and widen ref.h guard from defined(__cplusplus) && defined(_MSC_VER) to defined(__cplusplus) so all C++ compilers use the <atomic> path. Row added to Recently closed.) Updated: 2026-06-03 (T-COVERAGE-GATE-BUILD-BREAK-2026-06-03 closed — Coverage Gate (ADR-0922) was aborting at the build step on every master push since c658b3c452 with integer_motion.c:324: 'VmafFeatureExtractor' has no member named 'prev_prev_ref' and an undefined reference to vmaf_fex_integer_motion_v2 linker error. Root cause: (1) integer_motion.c::extract() referenced fex->prev_prev_ref for the motion_five_frame_window path — the field was never added to the struct; (2) feature_extractor.cpp still declared and listed vmaf_fex_integer_motion_v2 after the CPU source was removed from the meson build. Fix (ADR-0994): add -ENOTSUP guard in integer_motion.c::init() mirroring integer_motion_v2.c (ADR-0337), replace the dead prev_prev_ref reference in extract(), and remove the dangling vmaf_fex_integer_motion_v2 declaration and list entry from feature_extractor.cpp. Library and CLI build and link cleanly; gcovr can now run and report actual coverage. No score change. Row added to Recently closed.) Updated: 2026-06-03 (T-CUDA-PSNR-HVS-F3-LDG-2026-05-29 closed — F3 __ldg() + __restrict__ pointer extraction + __launch_bounds__(64) applied to the psnr_hvs CUDA kernel (core/src/feature/cuda/integer_psnr_hvs/psnr_hvs_score.cu). PR #107 rebase; mirrors ADR-0754 / ADR-0757 pattern. Predicted -3 to -5% kernel duration at 1080p; bit-identical scores (ADR-0214 places=4). ADR-0764. No bug; perf-only change.) Updated: 2026-06-03 (T-VMAFX-OPERATOR-STAGE2-2026-06-03 closed — vmafx-operator Stage 2 shipped (ADR-0786). Three CRD reconcilers promoted from Stage 1 stubs to functional loops: VmafxJob polls the vmafx-controller GetJob gRPC endpoint every 10 s and maps PENDING/RUNNING/COMPLETED/FAILED/CANCELLED to CR phase, writing Score and FinishedAt on completion; VmafxNode enforces a 60-second stale-heartbeat gate, marking nodes Healthy=false after missed probes; VmafxModelTraining polls a sidecar /status endpoint every 60 s and emits CheckpointWritten events. Admission webhooks added for VmafxJob and VmafxNode (URI validation, GPU-vendor enum enforcement). Per-controller RBAC split into three minimal ClusterRole files. gen/go/controller/controller.pb.go extended with FinalScore field. 7-spec envtest suite extended. No change to libvmaf C API or public CLI surface. PR: feat/vmafx-operator-stage2-reconcilers-20260529. ADR-0786.) Updated: 2026-05-29 (T-CUDA-MOTION-LAUNCH-OVERHEAD-20260529 opened — CUDA motion integer_motion_cuda.c rewritten to use MOTION_BATCH_DEPTH=8 per-frame SAD ring buffers, deferring cuStreamSynchronize from once-per-frame to once-per-8-frames (ADR-0845). Expected improvement: 576p from 0.22x CPU to >2x CPU. Correctness at places=4 and A/B measurement pending; DRAFT PR: perf/cuda-motion-launch-overhead-20260529. Research-0760 / ADR-0845.) _Updated: 2026-05-31 (T-LIBVMAF-SCORE-NEEDS-CTX-2026-05-31 closed — pkg/libvmaf.Scorer.Score and pkg/libvmaf.ScoreDirect now take context.Context as their first parameter and propagate it to the underlying vmaf subprocess via exec.CommandContext + 2-second WaitDelay (subprocess path) and to the per-frame loop via ctx.Err() checks at frame boundaries (cgo direct path). Five production call sites updated: cmd/vmafx-server/{http,grpc}_server.go, cmd/vmafx-controller/{http,grpc}_server.go, cmd/vmafx-node/executor.go. New tests cover subprocess SIGKILL on cancel, HTTP client-disconnect on server + controller, pre-cancelled fast paths, and in-loop cancellation of ScoreDirect. Closes the bug deferred from ADR-0978. Bug fix; no ADR per CLAUDE §12 r8. PR: fix/libvmaf-score-ctx.) Updated: 2026-05-31 (T-VMAFX-TUNE-GO-DEEP-BUG-AUDIT-2026-05-31 closed — deep-dive audit of cmd/vmafx-tune/ (Stage-1 Go CLI per ADR-0705 / ADR-0713) and its pkg/{report,bisect,encoder,ladder} dependencies, closing five distinct bugs in one PR. (1) JSON NaN propagation in bisect_samples — report.EmitJSON and cmd/vmafx-tune/cmd.emitSweepJSON previously sanitised only top-level row floats, leaving []bisect.Sample as raw float64 in the wire shape; one non-finite sample crashed json.MarshalIndent and broke AGENTS.md rebase-sensitive invariant #2 (Python ↔ Go parser parity). New public report.SanitizeBisectSamples walks the nested floats; mirrored in emitLadderJSON for Cloud + Hull + Renditions across BitratekBps, VMAF, TargetVMAF. (2) parseVMAFXMLMean accepted "NaN" / "+Inf" / "-Inf" — Go strconv.ParseFloat accepts those tokens silently, so a corrupt vmaf XML mean fed non-finite scores into the pipeline. Parser now rejects non-finite means at the source. (3)–(4)–(5) Subprocess hang risk — every exec.Command in pkg/encoder (ffmpeg encode, ffprobe probe, codec discovery) and pkg/bisect (vmaf scoring) ran with no context and no timeout. Switched to exec.CommandContext with per-stage upper bounds overridable via VMAFX_TUNE_ENCODE_TIMEOUT / VMAFX_TUNE_SCORE_TIMEOUT / VMAFX_TUNE_PROBE_TIMEOUT. (6) Codec-discovery cache stale-key — the previous sync.Once gate locked in whichever ffmpeg binary path was probed first, with _ = ffmpegBin masquerading as cache invalidation. Cache key is now the binary path. 8 new Go regression tests plus 2 updated existing tests. Python compare-parser tests (88 tests across tools/vmaf-tune/tests/test_{bisect,compare,compare_rate_quality_sweep,compare_no_bisect}.py) still pass. Bug fixes; no ADR per CLAUDE §12 r8. PR: fix/vmafx-tune-go-audit-20260531.) Updated: 2026-05-31 (T-TEST-SVM-PARSER-LINK-PLUS-OPERATOR-AUDIT-2026-05-31 closed — bundled fix for the pre-existing test_svm_parser Meson link break (the executable's source list omitted ../src/thread_locale.c, so svm.cpp's references to vmaf_thread_locale_push_c / vmaf_thread_locale_pop were unresolved; the sibling test_svm_api target proves the precedent) and a deep audit of cmd/vmafx-operator/. Operator audit findings: (1) VmafxNode.probeHealthz deferred resp.Body.Close() without draining — every 30-second probe of N VmafxNodes leaked one TCP connection to the controller because Go's net/http only returns a connection to the keep-alive pool if the body is fully read; fixed by draining via io.Copy(io.Discard, resp.Body) before Close. (2) vmafx.dev/v1 integer fields (VmafxJob.spec.priority, VmafxNode.spec.capacity, VmafxNode.status.assignedJobs, VmafxModelTraining.status.currentSamples, VmafxModelTraining.spec.checkpoint.minSamples) declared as Go int — violates Kubernetes API conventions because OpenAPI v3 has no architecture-dependent integer type; widened to int32. (3) Documented defaults (Backend=cpu, Priority=0, Capacity=1, Checkpoint.Interval=10m, Checkpoint.MinSamples=1000) were prose-only — added +kubebuilder:default: markers and default: keys in the CRD schemas + Helm chart copies so the apiserver actually applies them on admission. Three new go test regression tests (TestProbeHealthzDrainsBody, TestProbeHealthzNon200StillDrains, TestProbeHealthzTransportErrorReturnsFalse) exercise the body-drain fix without requiring envtest. Bug fixes; no ADR per CLAUDE §12 r8. PR: fix/test-svm-parser-link-plus-operator-audit.) Updated: 2026-05-31 (T-VMAFX-SERVER-BUG-AUDIT-2026-05-31 closed — deep-dive audit of cmd/vmafx-server/ + pkg/score/ (Go gRPC + HTTP scoring service per ADR-0703 / ADR-0933) fixed four real defects + one defensive cleanup. (1) pkg/observability.NewShutdownContext leaked one goroutine + one signal-handler subscription per stop()-before-signal cycle (early-exit paths in main() skipped defer stop() via os.Exit(1)) — fixed by delegating to stdlib signal.NotifyContext. (2) pkg/score.OpenScoreStream / PushFrame surfaced meaningless io.EOF instead of the server's real gRPC status when Send raced a server-side stream rejection — fixed via recvStatusOnEOF helper that drains Recv on Send-EOF. (3) cmd/vmafx-server POST /v1/score had no body size limit (multi-GB POST OOM vector) — added http.MaxBytesReader cap at 1 MiB + 413 mapping. (4) gRPC server had no panic-recovery interceptors — added unary + stream recover() wrappers that translate panic into codes.Internal and keep the server alive. (5) pkg/score.ScoreStream.Recv upgraded from err == io.EOF to errors.Is(err, io.EOF). Six new regression tests added across pkg/observability, pkg/score, cmd/vmafx-server. ADR-0978 + state.md row added. Scorer-subprocess context-cancellation (exec.CommandContext) tracked separately as Open bug T-LIBVMAF-SCORE-NEEDS-CTX-2026-05-31.) Updated: 2026-05-31 (T-CORE-TOOLS-INPUT-READER-SAFETY-2026-05-31 closed — deep-dive audit of core/tools/ fixed three real defects: (1) y4m_input_open_impl ignored failed malloc returns (NULL dst_buf surfaced as success, fread(NULL) SIGSEGV on first fetch); (2) both y4m_input.c and yuv_input.c computed dst_buf_sz in int/unsigned precision with size_t assignment — wrapped for headers near 32-bit ceiling; (3) vmaf_bench::bench_feature leaked CUDA/SYCL state on every success and most error paths. New test_y4m_alloc_failure regression test (POSIX-only, fast suite) using RLIMIT_AS to force malloc failure verifies fail-then-pass behaviour. ADR-0977 + state.md row added.) Updated: 2026-05-31 (T-MASTER-CI-VERIFIED-2026-05-31 closed — two master CI regressions on tip 4948b771c, both verified locally in vmaf-dev-mcp before being patched. (1) test_metal_float_ms_ssim_parity (3 macOS jobs) failed with CPU: vmaf_read_pictures failed because FIXTURE_H = 144u was below the float_ms_ssim 176-floor (Netflix#1414); fix: bump to 192. (2) test_ssimulacra2_simd::test_xyb (Linux all-backends) failed because icx ignores -ffp-contract=off and -fp-model=precise for inline scalar code and emits vfmadd against the AVX2 SIMD's explicit non-FMA intrinsics; fix: add #pragma clang fp contract(off) to the test TU. ADR-0973 + Research-0973 + state.md row added. No production binary changes; no score drift.) Updated: 2026-05-31 (T-MCP-HTTP-NO-AUTH-2026-05-31 closed — Round 26 audit A.1 fixed: MCP HTTP transport now enforces Bearer token auth (fail-closed), 4 MiB body limit, and loopback-only bind default. ADR-0967. Row added to Recently closed.) Updated: 2026-05-31 (T-HIP-KERNEL-COVERAGE-ROUND4-2026-05-31 closed — HIP kernel parity coverage round 4 lands test_hip_ssimulacra2_parity and test_hip_float_ssim_parity under core/test/ (ADR-0958). Closes 2 of the 4 originally-planned HIP-vs-CPU parity gaps after PR #443; HIP coverage lifts from 13/17 → 15/17 extractors (88%). The speed-family round-4 picks (speed_chroma_hip / speed_temporal_hip) discovered a pre-existing latent link defect — speed_internal_init_dimensions and speed_internal_float_stride declared in core/src/feature/speed_internal.h but no .c implementation exists. New row T-HIP-SPEED-INTERNAL-IMPL-MISSING-2026-05-31 added to Open bugs (the same defect blocks the CUDA + SYCL speed-family twins from linking). float_moment_hip row already on Deferred — CPU/HIP provided_features arrays do not share a key. PR: test/hip-kernel-coverage-round4.) Updated: 2026-05-31 (T-MCP-STOP-DOUBLE-JOIN-SEGV-2026-05-31 closed — PR #460 audit follow-up #5: vmaf_mcp_stop() SIGSEGV on its third invocation. Root cause: unconditional atomic_exchange(running, 2) mutated 0 -> 2 silently on never-started transports; the join branch guard fired for both 1 and 2, so subsequent calls invoked pthread_join() on a default-initialised or already-joined pthread_t. Fix: replace each exchange + dual-value guard with atomic_compare_exchange_strong(expected=1, desired=2) so the join branch fires exactly once per started transport. Regression test core/test/test_mcp_stop_idempotent.c. Bug fix; no ADR per CLAUDE §12 r8. Row added to Recently closed.) Updated: 2026-05-31 (T-COMPAT-PYTHON-VMAF-SCANF-LOCALE-2026-05-31 closed — two latent bugs in upstream-mirror compat/python-vmaf/ fixed: (1) tools/scanf.py::makeFormattedHandler.applyWidth inverted-guard crash on implicit-width converters + silent cap-drop on explicit-width; (2) __init__.py::ProcessRunner.run setdefault no-op when parent shell sets LANG. Fix: swap scanf branches; switch to unconditional env[...] = "C". 11 new regression tests; embedded scanf test suite improves from 7 errors to 1. ADR-0955. PR: fix/compat-python-vmaf-scanf-locale-bugs.) Updated: 2026-05-31 (T-TEST-PIXEL-FORMAT-EDGE-COVERAGE-20260531 closed — core/test/test_pixel_format_edge_coverage.c adds five end-to-end CPU extractor smoke tests covering PSNR on YUV422P 8-bit, YUV444P 10-bit, YUV420P 12-bit; SSIM on YUV422P 8-bit; CIEDE on YUV422P 8-bit. Closes Research-0912 audit gap — prior to this PR no CPU extractor was exercised end-to-end on 4:2:2 input, no extractor at 12 bpc, and the only 4:4:4+HBD smoke ran through the full VMAF model rather than isolating a single extractor. All 50 fast-suite tests green. ADR-0912.) Updated: 2026-05-31 (T-CHANGELOG-RENDERER-SPLICE-AND-DRIFT-2026-05-31 closed — scripts/release/concat-changelog-fragments.sh boundary regex fixed (^## [^[] → ^## \[) + 102 fragments normalised (drop redundant first-line ### Section headers, demote ## to ###) + 32 perf/ + performance/ fragments relocated into changelog.d/changed/perf-*.md + stderr WARNINGs for unknown subdirs / empty fragments. CHANGELOG.md regenerated from 59 757 → 15 030 lines (−44 727); --write now idempotent. ADR-0913 / Research-0913. PR: fix/changelog-renderer-and-drift.) Updated: 2026-05-30 (T-ERROR-CODE-CONSISTENCY-AUDIT-2026-05-30 closed — fork-added MS-SSIM decimate dispatcher and its three SIMD specialisations (scalar ms_ssim_decimate.c, x86/ms_ssim_decimate_avx2.c, x86/ms_ssim_decimate_avx512.c, arm64/ms_ssim_decimate_neon.c) returned bare -1 on malloc failure; converted to -ENOMEM to align with libvmaf's internal negative-errno convention. Header docstring in ms_ssim_decimate.h tightened from "non-zero on allocation failure" to "-ENOMEM on allocation failure". Wider audit of 99 suspicious returns across 35 fork-added TUs confirmed the remaining matches are framework-correct (flush()/close() "drain complete" positive signals, boolean availability predicates, qsort comparators) or live in upstream-mirror code (pdjson, predict.c, picture_cuda.c, integercuda.c). MCP transport audit deferred pending PRs #358/#359 merge to avoid file-overlap conflicts. ADR-0877. Row added to Recently closed.) Updated: 2026-05-30 (state.md drift sweep — 3 Vulkan Open rows (T-VK-1.4-BUMP, T-VK-CIEDE-F32-F64, T-VK-VIF-1.4-RESIDUAL-ARC) migrated to Recently closed with ADR-0726 supersession note; Vulkan backend dropped 2026-05-28 per ADR-0726 / PR #47, removing the entire core/src/vulkan/ + core/src/feature/vulkan/ tree and the libvmaf_vulkan.h public API, which structurally eliminates these blockers. Combined with the legacy-runner closures from the parent PR (T-LEGACY-RUNNER-ANSNR-BROKEN + T-LEGACY-RUNNER-STUB-MISSING-2026-05-29). PR: chore/state-md-drift-sweep-20260530.) Updated: 2026-05-30 (T-VMAFX-OPERATOR-ENVTEST-ETCD-2026-05-30 closed — cmd/vmafx-operator/internal/controller envtest suite was hard-failing in BeforeSuite with runtime error: invalid memory address or nil pointer dereference from controlplane.(*APIServer).Stop because the kubebuilder envtest control-plane binaries (etcd + kube-apiserver + kubectl) were not on PATH (noted in PRs #330 / #341 / #362). Fix: (1) new make setup-envtest target installs sigs.k8s.io/controller-runtime/tools/setup-envtest@latest + downloads v1.31 control-plane bundle; (2) .github/workflows/go-ci.yml installs setup-envtest + exports KUBEBUILDER_ASSETS before go test ./...; (3) suite_test.go now skips with an actionable message when KUBEBUILDER_ASSETS is unset, with a nil-testEnv bailout in AfterSuite as defense in depth. Suite goes from hard-fail to 3/3 green locally. No ADR per CLAUDE §12 r8; CI plumbing + defense-in-depth.) Updated: 2026-05-30 (Go test-coverage expansion for cmd/vmafx-controller, cmd/vmafx-controller/nodes, cmd/vmafx-server, and cmd/vmafx-mcp — no bug-status delta (test-only PR, no behavior change). Coverage deltas: controller 18.6 → 32.4 %, nodes 80.7 → 82.5 %, server 27.5 → 47.9 %, MCP 3.5 → 24.6 %. New files: cmd/vmafx-controller/main_extra_test.go, cmd/vmafx-controller/nodes/registry_edge_test.go, cmd/vmafx-server/main_extra_test.go, cmd/vmafx-mcp/impl_test.go. PR: test/go-controller-mcp-coverage.) Updated: 2026-05-30 (state.md closed-PR row sweep — 2 Open rows that cited CLOSED-not-merged PRs reconciled. (1) T-CUDA-FILTER1D-RES-DISPATCH-CONFLICT-2026-05-29 migrated to Recently closed as superseded: PR #214 (cleanup of scaffold-branch conflict markers) was closed once its base feat/cuda-resolution-dispatch-scaffold-20260529 branch was abandoned; PR #91 had already landed ADR-0753 dispatch on master via the adm_cm_device() consumer without extending into filter1d_8(), so master never carried the conflict markers. (2) T-CPP23-READ-JSON-MODEL-PENDING-2026-05-29 kept Open but de-cited — PR #215 (closed-not-merged 2026-05-30) replaced with Owner-driven; pending fresh PR per ADR-0846 Wave 8. Net Open count -1; total T-row count unchanged. PR: docs/state-md-closed-pr-row-sweep.) Updated: 2026-05-30 (T-ANSNR-SUNSET-ADR-AUTHORING-2026-05-30 closed — authored ADR-0865 "Sunset ANSNR (pre-VMAF metric)" back-dated to 2026-05-28 (PR #38 merge). Closes the ADR-0108 compliance gap caused by PR #38 citing Parent ADR-0709 (the Phase 4b distributed-platform umbrella ADR, which contains zero ANSNR content). The new ADR documents the empirical justification (Research-0733 zero feature-importance), the breaking-change migration path, the three rejected alternatives, and the historical mis-cite trail (PR #38 / #295 / #324 bodies cannot be rewritten). Tree-side grep confirmed no in-tree ADR-0709 references mis-cite ANSNR — all remaining tree-side cites correctly point at Phase 4b content. ADR-0749 (sunset legacy quality runner) now has a real parent ADR to cite. Docs-only PR; no code changes. PR: docs/author-ansnr-sunset-adr.) Updated: 2026-05-29 (state.md drift sync — 7 stale/duplicate Open rows removed: T-PYTHON-COMPARE-NO-BACKEND-PRECHECK, T-PYTHON-PERMUTATION-IMPORTANCE-HARDCODED-PATH (both closed by ADR-0613/ADR-0621 in Recently closed), duplicate T-VK-1.4-BUMP + T-VK-CIEDE-F32-F64 rows, T-PYTHON-TRAIN-TEST-STD-ZERO, T-PYTHON-ROUTINE-SWALLOWED-EXCEPTION, T-PYTHON-LOCAL-EXPLAINER-HACKY (all closed by ADR-0620 in Recently closed). Added 7 new Open rows for in-flight PRs #181, #213, #214, #215, #216 (×2), #217. PR: docs/state-md-drift-sync-20260529.) Updated: 2026-05-29 (Per-surface doc compliance audit (Research-0848 / ADR-0848): 30 PRs audited; 3 doc gaps found — T-DOC-VULKAN-STALE-POST-ADR0726, T-DOC-LEGACY-RUNNER-MISSING-DEPRECATION, PR #135 CUDA log format change. Rows added to Open bugs.) _Updated: 2026-05-29 (MT-1 + MT-2 Metal PR #117 audit findings fixed — MT-1: g_metal_features[] in dispatch_strategy.c lacked "float_ms_ssim_metal", causing vmaf_metal_dispatch_supports() to return 0 for the float MS-SSIM Metal extractor (ADR-0490 / T-VULKAN-METAL-DEAD-SCAFFOLDS-2026-05-18 wiring already landed); entry added. MT-2: vmaf_metal_state_init_external in picture_import.mm applied CFRetain + __bridge_retained (+2 retains) against a single __bridge_transfer (-1) in vmaf_metal_state_free, leaking one Obj-C reference per init/close cycle for both device and queue; CFRetain calls removed. No ADR per CLAUDE §12 r8; bug fixes. Rows added to Recently closed.) _Updated: 2026-05-29 (Research-0755 HIP backend audit completed. Findings: P0 — no extern "C" mangling bugs, no pinned-host leaks. P1 — AdmBufferHip struct passed by value (~272 bytes) in integer_adm/adm_csf.hip and adm_cm.hip kernel signatures (recommend pointer-passing, mirrors PR #93 F3). P2 — dispatch_strategy.c remains a full stub; cross-backend ULP gate runs not confirmed for newer extractors (integer_ssim_hip, integer_adm_hip, integer_cambi_hip, ssimulacra2_hip, speedhip); CAMBI HIP terminus (ADR-0345 Phase 3) confirmed landed. 20 of 20 registered extractors have real hipModuleLoadData paths under HAVE_HIPCC. See docs/research/0755-hip-backend-audit-20260529.md. Audit-only; no code changes.) _Updated: 2026-05-29 (T-CUDA-READBACK-HOST-PINNED-LEAK-20260529 closed — vmaf_cuda_kernel_readback_free in core/src/cuda/kernel_template.h now calls vmaf_cuda_buffer_host_free to release the pinned host readback buffer. Previously the helper only NULLed rb->host_pinned without calling cuMemFreeHost, leaking one cuMemHostAlloc allocation per init/close cycle across all 9 template-using feature extractors: integer_psnr, integer_ssim/float_ssim, ssim, float_psnr, float_motion, integer_ciede, integer_moment, integer_motion_v2, integer_cambi. PR #93 follow-up sweep. Bug fix; no ADR per CLAUDE §12 r8.) Updated: 2026-05-29 (T-ORT-SILENT-DISCARD-ELEM-TYPE-20260529 closed — GetTensorElementType ORT API calls during vmaf_ort_open IO-type population were silently swallowed via ort_discard_status(), leaving input_elem_types[i] / output_elem_types[i] at UNDEFINED (0) on failure. The run path would then silently emit fp32 tensors regardless of declared type, accepting a malformed model with no error. Fix: both call sites replaced with a checked path that returns -EINVAL + vmaf_log(WARNING). Two regression-lock tests added to test_ort_internals. Bug fix; no ADR per CLAUDE §12 r8.)
Updated: 2026-06-03 (T-CUDA-MS-SSIM-FLOAT-PRECISION-2026-06-03 closed — ms_ssim_vert_lcs kernel used 2.0f float literals for the L/C/S numerators and float warp/block reduction arrays. The CPU scalar reference (ssim_tools.c ssim_accumulate_default_scalar) uses 2.0 * (double literal) causing float-to-double promotion. The float accumulation caused approximately 0.004 drift over 33k pixels at scale 0, approximately 40x the places=4 tolerance. Fix: per-pixel L/C/S changed to double; warp partial shared arrays changed to double[…]; __shfl_down_sync operands changed to double; partials device/host buffers resized from sizeof(float) to sizeof(double); c1/c2/c3 in MsSsimStateCuda promoted to double. Applies the ADR-0139 pattern (previously fixed for AVX2/AVX-512) to the CUDA backend. ADR-0990. Blamed commit: 8db2715ac2.)
Updated: 2026-05-29 (Research-0755 HIP backend audit completed. Findings: P0 — no extern "C" mangling bugs, no pinned-host leaks. P1 — AdmBufferHip struct passed by value (~272 bytes) in integer_adm/adm_csf.hip and adm_cm.hip kernel signatures (recommend pointer-passing, mirrors PR #93 F3). P2 — dispatch_strategy.c remains a full stub; cross-backend ULP gate runs not confirmed for newer extractors (integer_ssim_hip, integer_adm_hip, integer_cambi_hip, ssimulacra2_hip, speed*hip); CAMBI HIP terminus (ADR-0345 Phase 3) confirmed landed. 20 of 20 registered extractors have real hipModuleLoadData paths under HAVE_HIPCC. See docs/research/0755-hip-backend-audit-20260529.md. Audit-only; no code changes.) _Updated: 2026-05-29 (T-CUDA-READBACK-HOST-PINNED-LEAK-20260529 closed — vmaf_cuda_kernel_readback_free in core/src/cuda/kernel_template.h now calls vmaf_cuda_buffer_host_free to release the pinned host readback buffer. Previously the helper only NULLed rb->host_pinned without calling cuMemFreeHost, leaking one cuMemHostAlloc allocation per init/close cycle across all 9 template-using feature extractors: integer_psnr, integer_ssim/float_ssim, ssim, float_psnr, float_motion, integer_ciede, integer_moment, integer_motion_v2, integer_cambi. PR #93 follow-up sweep. Bug fix; no ADR per CLAUDE §12 r8.)
Fork bug-status — docs/state.md¶
Updated: 2026-05-29 (T-CUDA-READBACK-HOST-PINNED-LEAK-20260529 closed — vmaf_cuda_kernel_readback_free in core/src/cuda/kernel_template.h now calls vmaf_cuda_buffer_host_free to release the pinned host readback buffer. Previously the helper only NULLed rb->host_pinned without calling cuMemFreeHost, leaking one cuMemHostAlloc allocation per init/close cycle across all 9 template-using feature extractors: integer_psnr, integer_ssim/float_ssim, ssim, float_psnr, float_motion, integer_ciede, integer_moment, integer_motion_v2, integer_cambi. PR #93 follow-up sweep. Bug fix; no ADR per CLAUDE §12 r8.) Updated: 2026-05-29 (T-CUDA-RESOLUTION-DISPATCH-EXTENDED-2026-05-29 — ADR-0753 resolution-aware dispatch extended to 3 kernels. filter1d_8_horizontal_kernel_2_17_9_no_bounds added to integer_vif/filter1d.cu; calculate_ssim_vert_combine_no_bounds added to integer_ssim/ssim_score.cu. Both wired via vmaf_cuda_workload_class() in integer_vif_cuda.c::filter1d_8() and integer_ssim_cuda.c::submit_fex_cuda(). Policy: BOUNDED at MEDIUM+LARGE, NO_BOUNDS at SMALL. ADR-0753 table + resolution_dispatch.h comment block + overview.md + AGENTS.md updated. DRAFT PR: feat/cuda-resolution-dispatch-scaffold-20260529.) _Updated: 2026-05-29 (T-CUDA-RESOLUTION-DISPATCH-SCAFFOLD-2026-05-29 closed — ADR-0753 resolution-aware CUDA kernel variant dispatch scaffolded. vmaf_cuda_workload_class(w,h) added in core/src/feature/cuda/resolution_dispatch.{h,c}; maps luma pixel count to WS_SMALL (<720p) / WS_MEDIUM (720p–4K) / WS_LARGE (>=4K). First consumer: adm_cm_device() picks adm_cm_line_kernel_8 (with launch_bounds(128,8)) at WS_MEDIUM and adm_cm_line_kernel_8_no_bounds at WS_SMALL/WS_LARGE, recovering the −9.3% 1080p gain without regressions. Policy: filter1d __ldg applies at MEDIUM+LARGE; ms_ssim_decimate smem tiling: SKIP all. DRAFT PR: feat/cuda-resolution-dispatch-scaffold-20260529. ADR-0753.) Updated: 2026-05-29 (T-CUDA-MS-SSIM-LDG-F3-20260529 closed — F3 fix (__ldg() + __restrict__ pointer extraction + __launch_bounds__(128)) applied to ms_ssim_vert_lcs (5×11 = 55 loads) and ms_ssim_horiz (2×11 = 22 loads) in core/src/feature/cuda/integer_ms_ssim/ms_ssim_score.cu. LDG.E.CONSTANT confirmed in sm_89 SASS. No bug; pure perf. ADR-0757. PR: perf/cuda-ms-ssim-vert-lcs-horiz-ldg-20260529.) Updated: 2026-05-29 (T-CPP23-ORPHAN-C-SWEEP-20260529 closed — swept core/src/ and core/src/feature/ for .c files whose .cpp companion is the active meson.build source. Found 1 true orphan: core/src/metadata_handler.c, left behind when ADR-0708 renamed the file to metadata_handler.cpp without running git rm. The other 15 candidate pairs (dict, log, mem, ref, thread_locale, opt, fex_ctx_vector, output, model, feature_name, luminance_tools, mkdirp, picture_copy, psnr_tools, cpu) all have meson.build still referencing the .c; those .cpp files are pre-prepared conversions not yet wired in. Only metadata_handler.c deleted. No ADR per CLAUDE §12 r8. PR: chore/cpp23-orphan-c-cleanup-20260529.) Updated: 2026-05-29 (T-CUDA-READBACK-HOST-PINNED-LEAK-20260529 closed — vmaf_cuda_kernel_readback_free in core/src/cuda/kernel_template.h now calls vmaf_cuda_buffer_host_free to release the pinned host readback buffer. Previously the helper only NULLed rb->host_pinned without calling cuMemFreeHost, leaking one cuMemHostAlloc allocation per init/close cycle across all 9 template-using feature extractors: integer_psnr, integer_ssim/float_ssim, ssim, float_psnr, float_motion, integer_ciede, integer_moment, integer_motion_v2, integer_cambi. PR #93 follow-up sweep. Bug fix; no ADR per CLAUDE §12 r8.) Updated: 2026-05-29 (T-HIP-ADM-BUFFER-BY-POINTER-20260529 closed — Research-0755 P1 finding resolved: AdmBufferHip (~272 bytes) passed by value in 4 HIP kernel signatures in adm_csf.hip and adm_cm.hip. Fix: kernel signatures changed to const AdmBufferHip * __restrict__ buf_ptr; device-side copy allocated once at init via hipMalloc + hipMemcpy; passed as &buf_dev in args[] arrays. Eliminates per-launch argument-buffer overhead on all 4 ADM HIP kernels. ADR-0759. PR: perf/hip-adm-buffer-by-pointer-20260529. Runtime verification pending — no AMD GPU on audit host; numerically transparent refactor.) _Updated: 2026-05-29 (T-CUDA-ADM-DECOUPLE-INLINE-LDG-F3-20260529 closed — F3 __ldg() fix applied to the active ADM path: const T *__restrict__ band-pointer extraction added to i4_adm_csf_kernel<> and adm_csf_kernel<> in adm_csf.cu, and to the six inline helpers in adm_cm.cu (inline_i4_csf_a, inline_i4_decouple_r, inline_s0_csf_a, inline_s0_decouple_r, inline_i4_csf_r, inline_s0_csf_r). All per-pixel DWT2 band reads now route through L1 read-only cache via __ldg(). CUDA vs CPU correctness: places=4 PASS, max diff = 0.00e+00 on Netflix 576×324 and 1080p checkerboard fixtures. ADR-0773. Row added to Recently closed.) Updated: 2026-05-29 (T-CUDA-CIEDE-LDG-F3-20260529 closed — F3 fix applied to calculate_ciede_kernel_8bpc and calculate_ciede_kernel_16bpc in core/src/feature/cuda/integer_ciede/ciede_score.cu. Typed __restrict__ channel pointers extracted from VmafPicture struct args before per-pixel body; all 6 indexed reads replaced with __ldg(&ptr[idx]) to route through L1 read-only texture cache. __launch_bounds__(BLOCK_X * BLOCK_Y) added to both kernels. Mirrors F3 pattern of ADR-0754 (PR #93, SSIM vert_combine). CUDA vs CPU correctness: places=4 PASS, max diff = 0.0 on Netflix 576×324. Pre-existing merge-conflict stub in integer_vif_cuda.c (from commit 24bb5daf89) resolved: HEAD side (ADR-0743 comment block) retained. ADR-0762. Row added to Recently closed.) _Updated: 2026-05-29 (Research-0755 HIP backend audit completed. Findings: P0 — no extern "C" mangling bugs, no pinned-host leaks. P1 — AdmBufferHip struct passed by value (~272 bytes) in integer_adm/adm_csf.hip and adm_cm.hip kernel signatures (recommend pointer-passing, mirrors PR #93 F3). P2 — dispatch_strategy.c remains a full stub; cross-backend ULP gate runs not confirmed for newer extractors (integer_ssim_hip, integer_adm_hip, integer_cambi_hip, ssimulacra2_hip, speed*hip); CAMBI HIP terminus (ADR-0345 Phase 3) confirmed landed. 20 of 20 registered extractors have real hipModuleLoadData paths under HAVE_HIPCC. See docs/research/0755-hip-backend-audit-20260529.md. Audit-only; no code changes.) _Updated: 2026-05-29 (T-CUDA-READBACK-HOST-PINNED-LEAK-20260529 closed — vmaf_cuda_kernel_readback_free in core/src/cuda/kernel_template.h now calls vmaf_cuda_buffer_host_free to release the pinned host readback buffer. Previously the helper only NULLed rb->host_pinned without calling cuMemFreeHost, leaking one cuMemHostAlloc allocation per init/close cycle across all 9 template-using feature extractors: integer_psnr, integer_ssim/float_ssim, ssim, float_psnr, float_motion, integer_ciede, integer_moment, integer_motion_v2, integer_cambi. PR #93 follow-up sweep. Bug fix; no ADR per CLAUDE §12 r8.) Updated: 2026-05-29 (Research-0751: 4K cross-backend baseline + PR #79 adm_cm A/B at 3840x2160. RTX 4090 CUDA medians (24f, vmaf_bench): vif 147 fps, adm 161 fps, motion 176 fps. CPU medians: vif 21 fps, adm 69 fps, motion 291 fps. CUDA/CPU speedup at 4K: vif 7.0x, adm 2.3x, motion 0.6x. PR #76 filter1d kernel is fully saturated at 4K (253 waves, 69.7% active warps -- the 0.84-wave launch-limit from 576p is gone). PR #79 adm_cm __launch_bounds__ shows zero kernel gain at 4K (-0.3%, noise) vs -9.3% at 1080p; the win is register-bound-regime-specific (8-32 waves). ms_ssim_decimate scale 0 at 4K: 88.1% active warps, 126 waves -- fully saturated, smem-tiling revert confirmed correct. Digest: Research-0751. PR: research/cross-backend-4k-baseline-20260529.) Updated: 2026-05-28 (cuDNN version audit completed — Research-0734. Verdict: fork's container and default Python env install CPU-only ORT 1.26.0; cuDNN is not a transitive dependency of any installed artifact. CUDA EP code path present in core/src/dnn/ort_backend.c but only reachable when a user manually installs onnxruntime-gpu. cuDNN 9.22.0 is the latest release; convolution memory-leak known issue (memory not freed until process exit) deferred as T-CUDNN-CONV-MEMLEAK-SERVERMODE below until a persistent inference server is shipped. No immediate action required. Digest: Research-0734. PR: docs/cudnn-version-audit-20260528.) Updated: 2026-05-28 (Research-0734 CUDA 13.3 fix-list deep audit completed — 40 "Fixed/Resolved" entries across CUDA 13.3/13.2/13.1/13.0 release notes audited against core/src/feature/cuda/ and core/src/cuda/. 37 NOT AFFECTED (cuBLAS/cuSOLVER/cuSPARSE/nvJPEG/NPP — none used). 1 LOW scope-guarded (NPP nppiCFAToRGB SSIM path [5192648] — zero call sites). 1 MEDIUM scope-guarded (cuFFT multi-GPU FP-exception [5923044] — no cuFFT usage). 1 CRITICAL confirmed (NVCC thread-reconvergence bug [6156910] present since 12.8 — dev/Containerfile and Dockerfile still pin 13.2 and must be bumped to 13.3). No new CRITICAL exposures beyond what PR #64 already scoped. Digest: docs/research/0734-cuda-13.3-fix-list-deep-audit.md. PR: docs/cuda-13.3-fix-list-deep-audit-20260528.) Updated: 2026-05-28 (Cross-backend parity baseline established — Research-0744 published. CPU vs CUDA measured on all three Netflix golden YUV pairs using vmaf_v0.6.1.json inside vmaf-dev-mcp:cuda13.3. Key findings: (1) CPU outperforms CUDA at the tested frame counts (3–48 frames) due to CUDA init overhead; (2) max pooled VMAF score delta CUDA vs CPU is −4×10⁻⁶, within established GPU tolerance; (3) integer_adm3 and integer_aim absent from CUDA pooled_metrics output — open investigation item; (4) SYCL unavailable in one-off container without Intel device node. This digest is the reference baseline for comparing against perf/cuda-vif-filter1d-ncu-driven and future perf PRs. PR: research/cuda-cross-backend-baseline-20260528. Digest: docs/research/0744-cuda-cross-backend-baseline-pre-ncu-perf.md.) Updated: 2026-05-28 (T-CUDA-HOTPATH-PROFILES-ADM-MOTION-SSIM-2026-05-28 closed — ncu --set basic hotpath profiles collected for all remaining CUDA metric families on RTX 4090 (sm_89, CUDA 13.3) at 576x324 Netflix golden pair. ADM: all 5 kernels launch-starved (< 1 wave), secondary register pressure in adm_cm_line_kernel_8 (114 regs, 33% theoretical occ). Motion: 62-64% occupancy (~5.9 waves), best-performing family. SSIM (float): calculate_ssim_vert_combine DRAM-bound at 55.8%; critical P0 bug found: integer_ssim_score.cu missing extern "C" makes int64 SSIM CUDA path crash at runtime. MS-SSIM: severe starvation at pyramid levels (0.06-0.25 waves, no shared-memory staging). Top 3 candidates: (1) fix extern "C" in integer_ssim_score.cu, (2) shared-memory tiling for ms_ssim_decimate, (3) register reduction in adm_cm_line_kernel_8. Research-0734 to 0738. PR: research/cuda-other-kernels-ncu-profile-20260528.) _Updated: 2026-05-28 (Research-0748: PR #76 filter1d_8_horizontal_kernel_2_17_9 1080p re-measurement. Verdict: +6.85 pp active warps, +3.6% end-to-end fps (checkerboard 1920×1080, 3f, median), __ldg L1-routing confirmed (+54.7% l1tex), register count 48 confirmed. Correctness: bit-identical vs baseline (delta 0.000000). PR #76 production-ready. Research: docs/research/0748-cuda-vif-filter1d-1080p-remeasure.md.) Updated: 2026-05-28 (CUDA VIF filter1d ncu-driven perf (ADR-0743) closed — filter1d_8_horizontal_kernel_2_17_9 optimized: __launch_bounds__(128, 10) reduces registers 56→48 per thread (sm_89), theoretical occupancy 75%→83.3%; __ldg() on 7 read-only tmp-channel loads routes through read-only L1 cache at ≥1080p. val_per_thread=4 evaluated and rejected (smem-limited at 37.5% occ). Correctness delta vs CPU ≤ 0.000010 (places=4 gate: PASS). PR: perf/cuda-vif-filter1d-ncu-driven-20260528. Research: docs/research/research-0743-cuda-vif-filter1d-perf-impl.md. ADR-0743.) Updated: 2026-05-28 (T-CROSS-BACKEND-BASELINE-SYCL-2026-05-28 closed — cross-backend throughput baseline extended to include SYCL on Intel Arc A380 (Research-0734). SYCL is fastest on WL1 (83 ms / 578 fps) and WL2 (71 ms). CPU is fastest on WL3 (70 ms). CUDA is slowest on all three workloads due to startup overhead at low frame counts. SYCL scores are bit-identical to CPU (Δ = 0) on all workloads; CUDA divergence 3–4e-6, within ADR-0119. Root cause of the one-off container SYCL failure documented: --device /dev/dri does not pass /dev/dri/by-path symlinks needed by Level Zero GPU ICD; fix is -v /dev/dri/by-path:/dev/dri/by-path:ro; --group-add render must be replaced with --group-add 988 (render GID). PR: research/cross-backend-baseline-with-sycl-20260528. Digest: docs/research/0734-cross-backend-baseline-with-sycl-20260528.md.) _Updated: 2026-05-28 (T-CAMBI-V0.8-SYNC-2026-05-28 closed — Research-0732 item #4 resolved: CambiFeatureExtractor Python wrapper bumped from upstream v0.5 to v0.8. The _validate_asset guard (previously inlined in _generate_result) now fires before any I/O; notyuv assets missing dis_enc_bitdepth or using an 8-bit workfile_yuv_type with a >8-bit encode are rejected with a descriptive AssertionError. CambiFullReferenceFeatureExtractor.VERSION now inherits from the base class instead of being hardcoded. Two validation tests added to python/test/cambi_test.py. C cambi.c not modified. PR: chore/cambi-python-v0.8-sync. References: Research-0732 item #4, ADR-0709 Phase 4b umbrella.) Updated: 2026-05-28 (T-SPEED-PYTHON-COMPAT-2026-05-28 closed — Research-0732 item #2: SpeedChromaFeatureExtractor, SpeedTemporalFeatureExtractor, and four QualityRunner wrappers (SpeedChromaQualityRunner, SpeedChromaUQualityRunner, SpeedChromaVQualityRunner, SpeedTemporalQualityRunner) ported from Netflix/vmaf upstream into compat/python-vmaf/. Smoke tests added to python/test/feature_extractor_test.py. Docs updated in docs/metrics/speed_qa.md. No ADR required — pure port. PR: feat/speed-python-compat-extractors.) Updated: 2026-05-28 (T-VMAFX-EBPF-RESEARCH-4B6-2026-05-28 closed — eBPF optimization target research completed (Research-0733, ADR-0709 item 4b.6). Selected target: rclone FUSE page-cache bypass via eBPF kprobe on fuse_file_read_iter. Projected 15–40% job wall-time reduction on warm-cache nodes for 1080p60 clips; 37× p50 FUSE read latency reduction. Four-phase implementation plan (4b.6.a–4b.6.d) documented. Research-only PR; no code written. PR: docs/research-vmafx-ebpf-optimization-target.) Updated: 2026-05-28 (T-VMAFX-OPERATOR-SKELETON-2026-05-28 closed — vmafx-operator kubebuilder skeleton + CRDs shipped (ADR-0714). Three CRDs in API group vmafx.dev/v1: VmafxJob (vmjob), VmafxNode (vmnode), VmafxModelTraining (vmtrain). Stage 1 stub reconcilers: Job Phase init, Node /healthz poll, ModelTraining Phase init + requeue. Helm integration: deploy/helm/vmafx/crds/ auto-installs CRDs; operator.enabled=true deploys the operator Deployment + RBAC. envtest suite verifies CRD install + reconcile triggers. Operator binary at cmd/vmafx-operator/. PR: feat/vmafx-operator-skeleton.) Updated: 2026-05-28 (Research-0733 hardware backend audit published — per-backend KEEP/DROP/DEFER table: CUDA KEEP, HIP KEEP, SYCL KEEP (Intel primary), Vulkan DROP (30 135 LOC, 3 long-standing open bugs, no k8s-native representation), Metal KEEP. No vendor loses native GPU coverage after Vulkan drop. PR: docs/hw-backend-audit-2026-05-28. Digest: docs/research/0733-hardware-backend-audit-2026-05-28.md.) Updated: 2026-05-28 (T-VMAFX-RUST-PILOT-TAD-2026-05-28 closed — TAD (Temporal Absolute Difference) feature extractor implemented in Rust and wired into libvmaf.so via cbindgen + Meson custom_target. Proves the Phase 4 Rust-in-libvmaf integration story end-to-end. New --feature tad signal available; does not affect existing VMAF scores. ADR-0707. PR: feat/tad-rust-pilot.) Updated: 2026-05-28 (T-CPP23-INTERNALS-PILOT-2026-05-28 closed — core/src/metadata_handler.c converted to C++20 (metadata_handler.cpp) as the Phase 4 language-modernization pilot (ADR-0708, Research-0732). vmaf_metadata_destroy now uses std::unique_ptr<VmafCallbackList> with a CallbackListDeleter that walks and frees the linked-list chain, replacing the manual traversal loop. Public C API unchanged; extern "C" guards added to metadata_handler.h. Netflix golden gate verified: 76.6678 (places=4 pass). PR: refactor/cpp23-pilot-metadata-handler. No bug; pure refactor.) Updated: 2026-05-28 (T-NETFLIX-PIPELINE-BACKLOG-AUDIT-2026-05-28 closed — comprehensive audit of 2,251 Netflix/vmaf upstream commits not in the fork. Result: most C-side extractors, Python harness changes, and model catalog items are already ported. 14 actionable backlog items identified: motion_v2 five-frame window unblock (rank 1), SpEED Python compat extractors (rank 2), picture-pool/batch-threading unconditional enable (rank 3), CAMBI Python v0.8 (rank 4), VIF reflect_101 boundary fix (rank 5). Full inventory in Research-0732. Quarterly re-audit cadence established. PR: docs/research-netflix-pipeline-backlog-audit.) Updated: 2026-05-28 (T-VMAFX-NODE-FFMPEG-LATEST-2026-05-28 closed — docker/Dockerfile.node ships vmafx-node worker images with ffmpeg n8.2 compiled from source with all 15 ffmpeg-patches/ patches applied. Four targets: node-cpu, node-cuda, node-rocm, node-sycl. Codec inventory: libx264, libx265, libvpx-vp9, libsvtav1, libdav1d. cmd/vmafx-node Go binary with startup encoder probe. ADR-0717 / Phase 4b.4. No bug; new feature surface. PR: feat/vmafx-node-ffmpeg-latest.) Updated: 2026-05-28 (T-VMAFX-PHASE4B-ADR-0709-2026-05-28 — umbrella ADR-0709 filed for VMAFX Phase 4b distributed platform. Locks the controller/node/operator architecture, ffmpeg worker integration, rclone zero-copy storage, eBPF research path, Go ONNX Runtime AI inference in the node, Python sidecar continuous training (v1), C ABI break with ffmpeg-patches update, and native build sunset (Docker images + Helm chart only). Nine-phase implementation plan (4b.1–4b.9) established. No bug; architectural decision record only. PR: feat/vmafx-phase4b-distributed-platform-adr-0709. ADR-0709.) Updated: 2026-05-28 (T-VMAFX-MCP-GO-PORT-2026-05-28 delivered — Go implementation of the VMAFX MCP server (cmd/vmafx-mcp/) shipped in PR feat/vmafx-mcp-go-port. All 15 Python tools ported with byte-for-byte schema parity; pkg/libvmaf/ shared path helpers created. Python server preserved. ADR-0704.) Updated: 2026-05-28 (T-VMAFX-RUST-SYS-BINDINGS-2026-05-28 closed — vmafx-sys Rust FFI crate shipped as bindings/rust/vmafx-sys (ADR-0706). Provides bindgen-generated raw bindings to libvmaf plus a thin safe Rust wrapper (VmafContext, VmafModel, picture helpers, VmafxError). Root Cargo.toml Rust workspace member entry added. CI gate rust-ci.yml runs cargo fmt --check + cargo clippy -D warnings + cargo test + Netflix golden smoke test on all bindings/rust/ PRs. PR: feat/bindings-rust-vmafx-sys. No bug; new surface.) Updated: 2026-05-28 (T-VMAFX-PHASE4-FOUNDATION-2026-05-28 closed — Go and Rust workspace skeletons added at repo root (go.mod, Cargo.toml). pkg/version smoke package proves the Go workspace compiles; go-ci.yml and rust-ci.yml GitHub Actions workflows gate PRs. Makefile targets go-build, go-test, rust-build, rust-test added. Multi-language policy documented in docs/principles.md §8 and docs/development/languages.md. ADR-0702 filed. No bug; pure foundation scaffolding. PR: feat/vmafx-phase4-language-modernization-foundation.) Updated: 2026-05-28 (T-POST-CUTOVER-URL-SWEEP-2026-05-28 closed — all in-tree references to lusoris/vmaf updated to VMAFx/vmafx following the GitHub org cutover. GHCR paths updated to vmafx/vmafx (lowercase OCI convention). 113 files, 279 replacements. PR: chore/post-cutover-url-sweep. No bug; pure maintenance change. Closes T-POST-CUTOVER-URL-SWEEP.) Updated: 2026-05-28 (T-CI-PATH-DRIFT-POST-ADR0700-2026-05-28 closed — post-ADR-0700 rename left stale libvmaf/ source-directory references in five workflow files (docker-image.yml, ffmpeg-integration.yml, supply-chain.yml, nightly.yml, tests-and-quality-gates.yml), blocking all merges. Fixed by replacing stale paths with core/. Also: gitleaks-action v2.3.9 fails on org repos without GITLEAKS_LICENSE; replaced with direct gitleaks CLI binary install (free, Apache-2.0). Dependency graph enabled via PATCH API. PR: fix/ci-paths-libvmaf-to-core-20260528.) Updated: 2026-05-28 (T-VMAFX-REPO-LAYOUT-2026-05-28 closed — libvmaf/ renamed to core/ and python/vmaf/ moved to compat/python-vmaf/ as part of the VMAFX rebrand (ADR-0700). Output artifacts (libvmaf.so, install headers, public C symbols) unchanged. A compat/vmaf symlink and python/vmaf/__init__.py shim preserve import vmaf. All CI workflows, Makefile, scripts, and ~676 doc files updated. No bug; pure layout change. PR: refactor/vmafx-repo-layout.) Updated: 2026-05-22 (T-CI-DRAFT-AUTOMERGE-GATE-2026-05-22 closed by PR #1503 — draft PRs could satisfy the single branch-protection context because Required Checks Aggregator was skipped on drafts and GitHub treats skipped required checks as successful. ADR-0679 makes the aggregator fail drafts explicitly, ignore stale draft-era sibling check runs during ready-for-review aggregation, and compares ADR collision phase 1 against the PR base SHA to avoid post-merge self-collisions.) Updated: 2026-05-20 (T-VULKAN-MOTION-LAVAPIPE-INIT closed — lavapipe motion parity is restored by routing --feature motion --backend vulkan through the stable integer_motion_vulkan extractor, enabling its raw integer_motion debug metric by default, correcting CUDA/SYCL/Vulkan motion_v2 high-edge mirror padding to CPU reflect-101 (2 * size - idx - 2), and adding motion / motion_v2 back to the lavapipe GPU-parity matrix. ADR-0662; row moved to Recently closed.) Updated: 2026-05-20 (T-AI-FR-REGRESSOR-V1-REFRESH-2026-05-20 closed — fr_regressor_v1 was retrained from runs/full_features_netflix_refresh_20260520.parquet after the May feature-extraction/default-path refresh wave. The ADR-0249 recipe and PLCC ship gate are unchanged. New LOSO: PLCC 0.9982 ± 0.0014, SROCC 0.9567 ± 0.1234, RMSE 2.194 ± 1.049; ONNX sha256 b57dee2509290d77c7980f8f23aa1380f64937c485d1b1d1e5f78c13a3a54c63. ADR-0647; row added to Recently closed. Aggregate models, codec-aware regressors, MOS/HDR heads, and encoder predictors remain under T-AI-REFRESH-ALL-DERIVED-ARTIFACTS-2026-05-20.) Updated: 2026-05-20 (T-DNN-MULTI-OUTPUT closed — attached tiny-AI rank-2 and rank-4 models now route through vmaf_ort_run() and append every scalar ONNX output to the feature collector. Single-output models preserve their historical key; multi-output keys derive from count-matched sidecar output_names[] or ONNX output names with deterministic sanitised fallbacks. ADR-0646; row moved to Recently closed.) Updated: 2026-05-20 (T-INTEGER-ADM-P-NORM-SIMD-GAP closed — integer_adm:adm_p_norm now threads through scalar, AVX2, and AVX-512 adm_cm / i4_adm_cm callbacks instead of leaving x86 SIMD on the hard-coded 3.0 exponent. ADR-0645; row moved to Recently closed. T-RULE-ENFORCEMENT-READY-FOR-REVIEW-TRIGGER-2026-05-20 closed — the rule-enforcement workflow now listens to ready_for_review, matching ADR-0331 and preventing draft-time skipped gates from lingering when a PR is activated. T-TEST-FEATURE-COLLECTOR-VCS-HEADER-RACE-2026-05-20 closed — the direct libvmaf.c test target now depends on generated vcs_version.h.) Updated: 2026-05-20 (T-DEV-CONTAINER-V16-ENCODER-PROBE-HARDENING-2026-05-20 closed — BBB v16 container probe/debug pass found QSV pointed at NVIDIA /dev/dri/renderD128, the image missing libmfx-gen.so oneVPL GPU runtime, AMF failure diagnostics hiding the missing libamfrt64.so.1 line behind muxer noise, compose health checking a UDS socket the stdio entrypoint does not create, and compare --format markdown producing raw tables instead of profile-card reports. Fix is ADR-0641: QSV auto-selects the Intel render node, dev/Containerfile builds pinned intel/vpl-gpu-rt, probe error extraction prefers actionable runtime lines, compose health checks vmaf --version, compare can emit html/both profile cards directly, shared-ref bisects use 1.1× raw-source disk headroom, and default CPU compare encoders shrink to libx265,libsvtav1. T-SVTAV1-HDR-ADAPTER-2026-05-20 opened for the juliobbv-p/svt-av1-hdr runtime gap. T-VMAFTUNE-PROFILE-REPORT-AUDIT-2026-05-20 opened for the next deep audit of profile-card report graphs/layout/artifacts.) Updated: 2026-05-20 (T-MASTER-CI-2026-05-20 closed — PR #1437 repair cluster for master CI: CLI EOF/error paths leaked preallocated picture-pool slots and hung in vmaf_close() after writing output; lavapipe parity invoked retired adm_vulkan after ADR-0586 renamed the extractor to integer_adm_vulkan; AVX-512 float convolution used 64-byte aligned memory ops on buffers only guaranteed 32-byte aligned, crashing float_vif on AVX-512 runners; Python run_test_on_dataset() unconditionally requested bootstrap score keys from normal VMAF / PSNR runners in macOS tox; Python doctests relied on NumPy / Python repr details. Row added to Recently closed.) Updated: 2026-05-19 (ADR-0624 fast NR pre-scoring landed — --fast-nr flag wired into tune-per-shot and compare; NRProxyBackend/NRProxyBackendError added to score_backend.py; NR early-elimination sentinel + telemetry counters (fr_calls_total/fr_calls_saved) added to bisect.py. T-NR-PROXY-CALIBRATION-RUN filed in Deferred until corpus calibration sweep runs. PR: feat/fast-nr-prescoring.) Updated: 2026-05-19 (T-PYTHON-ROUTINE-SWALLOWED-EXCEPTION / T-PYTHON-TRAIN-TEST-STD-ZERO / T-PYTHON-LOCAL-EXPLAINER-HACKY closed — scaffold-audit P0 silent-correctness fixes (ADR-0620 / PR fix/scaffold-audit-p0-silent-correctness): (1) routine.py:604 except Exception: print/fallback replaced with raise CalibrationError(...) from exc; allow_uncalibrated=False default. (2) train_test_model.py:354 np.zeros substitution replaced with raise MissingLabelStddevError; assume_unit_stddev=True opt-in. (3) local_explainer.py:121 silent model[0] pick replaced with raise EnsembleNotSupportedError for len(model) > 1. 16 regression tests. Three rows moved to Recently closed.) Updated: 2026-05-19 (T-MACOS-SIGSEGV-UNRESOLVED-2026-05-19 opened — macOS SIGSEGV persists after PRs #1355/#1403/#1412; tmate SSH debug step added to CI (ADR-0626, PR feat/ci-tmate-debug-macos-on-failure) to enable live lldb on the next workflow_dispatch run. Row added to Open.) Updated: 2026-05-19 (ADR-0613 scaffold-audit P1 landed — T-PYTHON-COMPARE-NO-BACKEND-PRECHECK closed (select_backend() pre-check wired into _run_compare and _run_tune_per_shot); T-HIP-PICTURE-ALLOC-ENOSYS closed (hipMalloc-backed alloc/free replaces ENOSYS stubs); T-MOBILESAL-BPC-EARLY-REJECT-UNDOCUMENTED closed (actionable error message + docs/metrics/mobilesal.md §Known limitations updated); T-DNN-MULTI-OUTPUT-UNDOCUMENTED closed (code comments + docs/api/dnn.md §Known limitations added); T-DNN-MULTI-OUTPUT promoted to Open for the full multi-output follow-up. PR: fix/scaffold-audit-p1-feature-plumbing.) Updated: 2026-05-19 (ADR-0623 scaffold-audit P2 landed — T-SYCL-CLANG-TIDY-DISABLED / T-DOCKER-SMOKE / T-GPU-COVERAGE-STABLE-WEEKS / T-INTEGER-ADM-P-NORM-SIMD-GAP opened. adm_p_norm exposed on integer ADM extractor; float_vif_hip auto-dispatch gated behind enable_float_vif_hip_autodispatch Meson option. PR: fix/scaffold-audit-p2-half-finished.) Updated: 2026-05-19 (T-BBB-V14-QUADRATIC-REDECODE-2026-05-19 closed — vmaf-tune compare with 56 concurrent workers (14 encoders × 4 targets) ran for ~9.7 hours without converging on v14 BBB 1080p. Root cause: each bisect worker's finally block (ADR-0577 / PR #1354) deleted the 118 GB shared reference YUV on completion; the next worker re-decoded it through the --max-concurrent-decodes 1 semaphore (~3 min). With 56 workers × ~7 iterations, this produced up to 392 re-decodes. Fix (ADR-0612): decode the reference once in _run_compare before opening the thread pool; pass the pre-decoded .yuv path to all workers via pre_decoded_ref on compare_codecs/compare_codecs_sweep; delete in a try/finally block after pool shutdown. Workers see src_is_container=False and skip the per-bisect decode. Row added to Recently closed.) Updated: 2026-05-19 (T-BBB-V14-QUADRATIC-REDECODE-2026-05-19 closed — vmaf-tune compare with 56 concurrent workers (14 encoders × 4 targets) ran for ~9.7 hours without converging on v14 BBB 1080p. Root cause: each bisect worker's finally block (ADR-0577 / PR #1354) deleted the 118 GB shared reference YUV on completion; the next worker re-decoded it through the --max-concurrent-decodes 1 semaphore (~3 min). With 56 workers × ~7 iterations, this produced up to 392 re-decodes. Fix (ADR-0607): decode the reference once in _run_compare before opening the thread pool; pass the pre-decoded .yuv path to all workers via pre_decoded_ref on compare_codecs/compare_codecs_sweep; delete in a try/finally block after pool shutdown. Workers see src_is_container=False and skip the per-bisect decode. Row added to Recently closed.) Updated: 2026-05-19 (T-MACOS-VMAF-WRITE-OUTPUT-SEGV-DEEP-2026-05-19 closed — PR #1403 (ADR-0602) fixed the pic_cnt - 1 unsigned underflow and NULL guards, but macOS CI was cancelled and the fix was never verified. CI run 26065652545 (job 76635756665) confirmed the same two tests still SIGSEGV on macOS clang after #1403. Deep-fix (ADR-0606): four additional bugs addressed: (1) seven i > capacity off-by-one checks corrected to i >= capacity — index capacity is one past the allocated score array and is a heap buffer overread that MALLOC_PERTURB_=198 surfaces; (2) fps computation 0.0/0.0 = NaN guarded with an explicit zero check before division; (3) json_write_pool_score comma-placement corrected from j > 1 (enum-value heuristic) to bool *first flag; (4) json_write_frames separator corrected from i > 0 (frame-index heuristic) to bool first_frame flag. ADR-0606; row updated in Recently closed.) Updated: 2026-05-19 (T-MACOS-VMAF-WRITE-OUTPUT-SEGV-2026-05-19 closed — Build — macOS clang (CPU) SIGSEGV in test_write_output_json_path and test_vmaf_write_output after PR #1355 merged. Two root causes: (1) pic_cnt - 1 unsigned underflow to UINT_MAX when vmaf->pic_cnt == 0 (scores injected via vmaf_import_feature_score, bypassing vmaf_read_pictures) passed vmaf_feature_score_pooled's index_low > index_high guard and entered an ostensibly-infinite loop that Apple Clang did not reliably exit before a guard-page hit; (2) missing NULL guard for the vmaf context itself in vmaf_write_output_with_format. Fix: pic_cnt > 0 guard in json_write_pooled_entry and xml_write_one_metric_pools before the pic_cnt - 1 expression; NULL guards for vmaf and vmaf->feature_collector at the top of vmaf_write_output_with_format; matching NULL guards in vmaf_write_output_json; regression test test_write_output_pic_cnt_zero added. ADR-0602; follow-up deep-fix in ADR-0606 (macOS CI was cancelled for PR #1403 — the same SIGSEGV persisted after merge). Row updated in Recently closed by ADR-0606.) Updated: 2026-05-18 (T-BBB-V14-HW-ENCODER-PROBE-QSV-INIT-2026-05-18 closed — three bugs in vmaf-tune compare blocked the BBB v14 hardware-encoder run. (V14-A) probe_encoder_available() issued its dummy encode against a 64×64 source, which NVENC rejects with EINVAL (hardware minimum ~145×49) and QSV rejects (minimum ~128×96), causing every hardware encoder to fail the probe even on hosts with a working GPU. Fixed by changing the source to 320×240 @ 24 fps / 0.5 s. (V14-B) QSV encodes lacked the VA-API device-init chain required by FFmpeg's QSV bridge on Linux (-init_hw_device vaapi=va:… -init_hw_device qsv=qsv_dev@va -filter_hw_device va + -vf format=nv12,hwupload=extra_hw_frames=64); every QSV encode returned -22 Invalid argument. Fixed by injecting the chain via _hw_init_args_for_encoder() in compare.py. VA-API render node exposed as --vaapi-device PATH / VMAFTUNE_VAAPI_DEVICE env var (default /dev/dri/renderD128). BaseQsvAdapter.qsv_hw_init_args() static helper added for callers building production encode commands. (V14-C) The AMD Raphael / Phoenix APU iGPU (gfx1036) has no VCE encode block — AMF encoding fails with AMF_NOT_SUPPORTED at the silicon level. Documented as a hardware limitation in _amf_common.py; probe correctly surfaces (False, "dummy encode failed") without aborting the sweep. ADR-0601; row added to Recently closed.) Updated: 2026-05-18 (T-WINDOWS-STAT-COMPAT-INCLUDE-ORDER-2026-05-18 closed — Build — Windows MinGW64 (CPU) and Build — Windows MSVC + CUDA CI legs broken by the ADR-0521 stat compat macros. Two defects: (1) macros #define stat __stat64 etc. placed before #include <sys/stat.h> caused the preprocessor to macro-expand tokens inside the system header — under MinGW64 this redefined struct _stat64 from _mingw_stat64.h; under MSVC with SDK 10.0.26100.0 + NVCC it triggered cascading C2059/C2143 errors. (2) the guard was #ifdef _WIN32 which fires for MinGW64 too, but MinGW already provides POSIX stat/fstat/S_ISREG natively. Fix (ADR-0575): move #include <sys/stat.h> before the macro block; change guard to #ifdef _MSC_VER. ADR-0575; row added to Recently closed.) Updated: 2026-05-18 (T-INTEGER-SSIM-GPU-WRONG-METRIC-2026-05-18 closed — all three GPU backends (CUDA, HIP, SYCL) silently returned float_ssim scores when the caller requested the "ssim" feature (integer_ssim). Root cause: integer_ssim_cuda.c provided "float_ssim" (11-tap float Gaussian) and was misleadingly named; integer_ssim_hip.c used float intermediates instead of int64; there was no vmaf_fex_integer_ssim_sycl at all. Fix (ADR-0564): new CUDA kernel integer_ssim_score.cu + host glue ssim_cuda.c implement the real 9-tap int64 algorithm; integer_ssim_hip.c rewritten to use int64 device buffers matching the pre-existing HIP kernel; integer_ssim_sycl.cpp gets a new extractor with int64 moments and float32 SSIM formula (fp64-free constraint, ADR-0220). CUDA confirmed bit-exact (diff=0) vs CPU on Netflix golden 576x324 8bpc pair. ADR-0564; row added to Recently closed.) Updated: 2026-05-18 (T-CUDA-AIM-ADM3-2026-05-18 closed — float_adm_cuda lacked VMAF_feature_aim_score and VMAF_feature_adm3_score, causing --backend cuda to silently fall back to CPU for HDR VMAF model features. Two new kernel stages added to float_adm_score.cu (float_adm_csf_r stage 2b, float_adm_aim_cm stage 3b); FADM_ACCUM_SLOTS extended 6→9; host-side collect derives both scores. Parity with CPU held to places=4 on Netflix golden fixture. ADR-0574; row added to Recently closed. Branch: feat/hdr-features-cuda-twins.)
Updated: 2026-05-18 (T-VMAF-TUNE-ENOSPC-RC228-2026-05-18 closed — vmaf-tune compare (and ladder, tune-per-shot) failed all rows with rc=228 (ENOSPC; unsigned 8-bit of −28) when decoding the reference source to raw YUV on the dev-mcp container's 8 GB /tmp tmpfs. A 634-second 1080p60 BBB source decodes to approximately 118 GB of raw YUV420p, exhausting /tmp. Fix: disk-space preflight in bisect.py estimates required bytes (_estimate_yuv_bytes) and checks shutil.disk_usage(workdir).free < estimate × 1.1; if insufficient, bisect_target_vmaf returns BisectResult(ok=False, error=<human-readable message with --workdir / VMAFTUNE_WORKDIR hint>) before touching ffmpeg. VMAFTUNE_WORKDIR env var routes all scratch I/O to the specified volume. --workdir PATH CLI flag added to compare, tune-per-shot, and ladder. Container sets ENV VMAFTUNE_WORKDIR=/probes/vmaftune-work (435 GB bind-mount). ADR-0549; row added to Recently closed.)
Updated: 2026-05-18 (Audit cleanup bundle 2 applied — five P3 housekeeping items resolved: (1) Naming-01: one-line upstream-mirror-filename comment added to 8 CUDA/SYCL TUs; (2) Container-01: apt-mark showmanual post-install checks added for NEO, ROCm, and mesa-vulkan-drivers sets; (3) Docs-01: stale 2026-05-15 Updated note corrected (T-VK-T7-29-PART-2-IMPORT-NOT-IMPL and T-CAMBI-HIP-NOT-STARTED are in Recently-closed); (4) Python-03: stale inline baseline comment # 88.032956 deleted from vmafexec_test.py:1294; (5) gitignore: .claude/worktrees/ added. ADR-0549.)
Updated: 2026-05-18 (T-HIP-CUDA-ORPHAN-TU-CLEANUP-2026-05-18 closed — deep audit identified 6 orphan/dead translation units: integer_ciede_hip.c and integer_moment_hip.c (HIP duplicate-symbol orphans not in hip/meson.build), float_ssim_cuda.c (CUDA duplicate-symbol orphan not in core/src/meson.build), and adm_hip.c / motion_hip.c / vif_hip.c (HIP plumbing stubs compiled but with zero callers and a misleading init=0/run=-ENOSYS posture, no VmafFeatureExtractor registration). Also removed the now-unused feature_hip.h forward-declaration header. Total: ~1444 LOC removed. No callers of any deleted symbol found anywhere in libvmaf/, tools/, python/, ai/, mcp-server/. ADR-0546; row added to Recently closed.)
Updated: 2026-05-18 (T-HIP-05-AUDIT-FINAL-VERIFY-2026-05-18 closed — static-source audit of the 9 remaining HIP extractors listed by the HIP-05 audit as scaffold-ENOSYS: ciede_hip, float_moment_hip, float_ansnr_hip, integer_motion_v2_hip, float_motion_hip, float_adm_hip, ssimulacra2_hip, integer_adm_hip, float_vif_hip. All 9 are already real as of master 64f37a66d — the audit was stale after the kernel-promotion wave (PRs #1303–#1307, ADR-0372/0373/0375/0377/0468/0539). Every extractor has a .hip kernel source registered in hip_kernel_sources and a real #ifdef HAVE_HIPCC init/submit/collect path. hip_hsaco_stubs.c is now effectively empty (all weak stubs removed). No porting work is required. ADR-0563; row added to Recently closed.)
Updated: 2026-05-18 (T-CROSS-BACKEND-PARITY-MATRIX-2026-05-18 closed — systematic one-pass audit of all 18 CPU feature extractors across SYCL (Intel Arc A380), CUDA (RTX 4090), and Vulkan on the Netflix golden 576x324 pair. All 18 extractors are bit-exact vs CPU at IEEE-754 double precision (--precision max). No P0 or P1 divergence found. The previously documented 3.1e-5 ADM-scale1 delta is no longer observed (closed by the ADR-0178 + ADR-0545 kernel hardening wave). HIP parity deferred -- no discrete AMD GPU on audit host; scaffold extractors return -EINVAL. Registration coherence gaps noted for speed_chroma, speed_temporal, and integer ssim (no GPU twins). ADR-0550; Research-0550.) Updated: 2026-05-18 (T-VCQ-223-LOCAL-EXPLAINER-HANG-2026-05-18 closed — root cause confirmed as CPU-bound libsvm sampling, not a deadlock. Fix: default the fallback to neighbor_samples=100 — matching the passing sibling test_explain_vmaf_results. @unittest.skip("[VCQ-223]") removed from local_explainer_test.py:252; score assertions updated to the neighbor_samples=100 calibration. Wall time on dev machine: ~78 s. ADR-0562; PR fix/vcq-223-local-explainer-hang.) Updated: 2026-05-18 (T-DEV-CONTAINER-SYCL-HIP-RUNTIME-2026-05-18 closed — vmaf-dev-mcp vmaf --backend sycl and vmaf --backend hip silently fell back to CPU on the dev host (Linux 7.0.8-cachyos + Arc A380 + AMD gfx1036): NEO 25.18 from Intel's noble/unified APT repo did not understand kernel-7.x i915/xe UAPI (zeInit returned ZE_RESULT_ERROR_UNINITIALIZED, sycl-ls Platforms: 0), and ROCm 6.4 did not understand the kernel-7.x KFD ioctls (rocminfo failed with Unable to open /dev/kfd read-write: Invalid argument). Fix: pin Intel NEO 26.18.38308.1 via GitHub releases (Intel APT repo's newest is too old as of 2026-05-18; ARG-pinned IGC 2.34.4 + gmmlib 22.10.0 follow the NEO release-notes manifest), bump ROCm 6.4 → 7.2.3 (matches host) via the existing AMD APT repo, add /opt/intel/oneapi/tbb/latest/lib to LD_LIBRARY_PATH so Intel CPU OpenCL ICD loads, and add a runtime-visibility probe in dev/scripts/dev-mcp-entrypoint.sh that emits a WARN on container start when SYCL level_zero:gpu or HIP HSA agents are missing. ADR-0543; row added to Recently closed.) Updated: 2026-05-18 (T-HIP-SSIMULACRA2-BLUR-FMAD-2026-05-18 closed — vmaf --backend hip --feature ssimulacra2 was at risk of drifting past the places=2 cross-backend parity gate (ADR-0214) because the ssimulacra2_blur.hip HSACO was being compiled with hipcc's default -ffp-contract=fast. hipcc / amdclang++ then silently FMA-fused the recursive Gaussian IIR step (n2 * sum - d1 * prev) on the device side, shifting the pole cascade past CPU within a handful of pyramid levels. The CUDA twin already disables this via cuda_cu_extra_flags : ['--fmad=false', '-Xcompiler=-ffp-contract=off']; the HIP scaffolding had no equivalent dispatch — every kernel got the same flat command line. Fix (ADR-0539): introduce a hip_cu_extra_flags dict in core/src/meson.build mirroring the CUDA pattern, with first and only entry 'ssimulacra2_blur' : ['-ffp-contract=off']. Fall-through (.get(name, [])) is byte-identical for every other kernel. Verified end-to-end on AMD gfx1036 (iGPU) inside vmaf-dev-mcp: on the Netflix golden 576×324 pair, ssimulacra2 / integer_ssim / integer_ms_ssim all match CPU to display precision (6 decimal places) — well inside the places=2 gate. ADR-0539; row added to Recently closed.)
Updated: 2026-05-18 (T-AI-SCRIPT-ENV-VARS-2026-05-18 closed — 15 scripts under ai/scripts/*.py hard-coded the maintainer's .workingdir2/<corpus>/ planning directory as the default for corpus-root / data-root / clips-dir arguments; on the dev-mcp container and non-maintainer machines the path was absent and every first invocation died with FileNotFoundError. Fix (ADR-0547): layer os.environ.get("VMAF_<NAME>_DIR", "<old default>") on every affected constant — maintainer defaults unchanged; operators set one env var per corpus. Also delete untracked 142 KB tools/vmaf-tune/src/vmaftune/cli.py.bak editor backup and add *.bak/*.orig to .gitignore. Split from ADR-0546 bundle because the HIP parity claim in the original PR did not meet the places=4 bar (ADR-0214); HIP investigation is on investigate/hip-gfx1036-precision. ADR-0547; row added to Recently closed.) Updated: 2026-05-18 (T-HIP-INTEGER-VIF-KERNEL-CRASH-2026-05-18 closed — closes the ADR-0530 follow-up. The HIP integer VIF extractor previously crashed with a GPU memory access fault on the first frame whenever the model-driven dispatch picked it (PR #1287 un-flagged vmaf_fex_integer_vif_hip for exactly this reason). Four defects fixed: (1) the static 4×18 vif_filter1d_table is uploaded to a device buffer at init (pre-fix passed a host pointer into hipModuleLaunchKernel); (2) filter half-widths corrected from {9,5,3,0} (parsed from the wrong number in the kernel-name suffix) to {8,4,2,1} (= vif_filter1d_width[scale]/2); (3) the rd-filter downsample-write path now writes the half-resolution ref/dis planes scales 1–3 consume (pre-fix left them uninitialised); (4) per-frame hipMemcpy2DAsync stages the host VmafPicture Y plane into device memory before the scale-0 kernel reads it. VMAF_FEATURE_EXTRACTOR_HIP re-enabled on vmaf_fex_integer_vif_hip. End-to-end on AMD gfx1036: vmaf --backend hip --feature vif_hip reports VIF scores 0.5047 / 0.8764 / 0.9365 / 0.9634 on the Netflix golden 576×324 pair, within places=3 of CPU 0.5057 / 0.8791 / 0.9379 / 0.9643; places=4 tightening is a tracked follow-up. ADR-0538 (closes the ADR-0530 follow-up); row added to Recently closed.) Updated: 2026-05-18 (T-HIP-INTEGER-MOTION-FLAG-PROMOTION-2026-05-18 closed — extends ADR-0523 (PR #1283, registration fix). vmaf --backend hip --feature integer_motion now actually dispatches the HIP kernel calculate_motion_score_kernel_8bpc instead of silently falling back to the CPU twin. Promotes VMAF_FEATURE_EXTRACTOR_HIP on vmaf_fex_integer_motion_hip, adds VMAF_PICTURE_BUFFER_TYPE_HIP_DEVICE enum entry, wires compute_fex_flags() HIP slot, adds a CPU-twin fallback pass in vmaf_get_feature_extractor_by_feature_name(), drains HIP gpu_pending final-frame collect in flush_context_serial(), and routes integer_motion_hip writes through feature_name_dict. Verified on AMD gfx1036: VMAF=76.7125 (CPU 76.6678 — places=4 cross-backend gate passes); 48 HSACO kernel launches per 48-frame clip confirm real GPU dispatch. vmaf_fex_integer_vif_hip was un-flagged in the same PR (it crashes with a GPU memory access fault when the dispatch picks it; needs its own kernel-level fix). ADR-0530 extends ADR-0519 + ADR-0523; row added to Recently closed.) Updated: 2026-05-18 (T-DEV-CONTAINER-DRI-BIND-RACE-2026-05-18 closed — the vmaf-dev-mcp container failed to start after any PCI re-enumeration (reboot, suspend/resume, GPU hotplug) because dev/docker-compose.yml bind-mounted /dev/dri/by-path, whose symlink names (e.g. pci-0000:01:00.0-card) change whenever the kernel re-enumerates PCI devices; the OCI hook then tried to mount a no-longer-existing path and exited before the entrypoint ran. Discovered during BBB 4K v10 report regeneration. Fix: bind-mount the whole /dev/dri directory (stable devtmpfs path) instead; this subsumes the devices: /dev/dri:/dev/dri entry. ADR-0543; row added to Recently closed.) Updated: 2026-05-18 (T-PER-SHOT-BITRATE-AND-CHART-2026-05-18 closed — two display bugs in the BBB v10 4K per-shot report were fixed. Bug A: the Bitrate column showed "—" for every shot because _build_per_shot_bisect_predicate discarded result.bitrate_kbps; fix introduces a bitrate_sidecar dict populated by the predicate closure and consumed by _run_tune_per_shot to annotate each ShotRecommendation via dataclasses.replace, then emitted as bitrate_kbps in the plan JSON. Bug B: the per-shot timeline chart's last-shot CRF band was invisible because the x-axis right bound was exactly last_end, causing matplotlib's clip rectangle to trim the hlines artist; fix uses asymmetric padding (left 2 %, right 5 %) so the final band is fully visible. ShotRecommendation gains an optional bitrate_kbps field (default NaN). Three new tests. ADR-0531; row added to Recently closed.) Updated: 2026-05-18 (T-PER-SHOT-READONLY-CWD-2026-05-18 closed — vmaf-tune tune-per-shot exited 1 when the working directory was read-only (e.g. /workspace bind-mounted RO in the dev container). The default segment-dir resolved to Path("segments") relative to CWD, so write_concat_listing's mkdir call raised PermissionError after the plan JSON had already been fully written. Two-part fix: (1) segment-dir derivation now prefers plan_out.parent/segments over output.parent/segments when --plan-out is set; (2) OSError from write_concat_listing is caught, a WARN is emitted to stderr, and the command exits 0. Two regression tests added. ADR-0532; row added to Recently closed.) Updated: 2026-05-18 (T-PER-SHOT-BITRATE-AND-CHART-2026-05-18 closed — two display bugs in the BBB v10 4K per-shot report were fixed. Bug A: the Bitrate column showed "—" for every shot because _build_per_shot_bisect_predicate discarded result.bitrate_kbps; fix introduces a bitrate_sidecar dict populated by the predicate closure and consumed by _run_tune_per_shot to annotate each ShotRecommendation via dataclasses.replace, then emitted as bitrate_kbps in the plan JSON. Bug B: the per-shot timeline chart's last-shot CRF band was invisible because the x-axis right bound was exactly last_end, causing matplotlib's clip rectangle to trim the hlines artist; fix uses asymmetric padding (left 2 %, right 5 %) so the final band is fully visible. ShotRecommendation gains an optional bitrate_kbps field (default NaN). Three new tests. ADR-0531; row added to Recently closed.) Updated: 2026-05-18 (T-PER-SHOT-READONLY-CWD-2026-05-18 closed — vmaf-tune tune-per-shot exited 1 when the working directory was read-only (e.g. /workspace bind-mounted RO in the dev container). The default segment-dir resolved to Path("segments") relative to CWD, so write_concat_listing's mkdir call raised PermissionError after the plan JSON had already been fully written. Two-part fix: (1) segment-dir derivation now prefers plan_out.parent/segments over output.parent/segments when --plan-out is set; (2) OSError from write_concat_listing is caught, a WARN is emitted to stderr, and the command exits 0. Two regression tests added. ADR-0530; row added to Recently closed.) Updated: 2026-05-18 (T-DEV-CONTAINER-DRI-BIND-RACE-2026-05-18 closed — the vmaf-dev-mcp container failed to start after any PCI re-enumeration (reboot, suspend/resume, GPU hotplug) because dev/docker-compose.yml bind-mounted /dev/dri/by-path, whose symlink names (e.g. pci-0000:01:00.0-card) change whenever the kernel re-enumerates PCI devices; the OCI hook then tried to mount a no-longer-existing path and exited before the entrypoint ran. Discovered during BBB 4K v10 report regeneration. Fix: bind-mount the whole /dev/dri directory (stable devtmpfs path) instead; this subsumes the devices: /dev/dri:/dev/dri entry. ADR-0528; row added to Recently closed.) Updated: 2026-05-18 (T-HIP-DEAD-CODE-EXTRACTORS-2026-05-18 closed — generalisation of T-HIP-INTEGER-MOTION-UNREGISTERED to the rest of the HIP feature TUs. Six more extractors (float_vif_hip, integer_adm_hip (registers as adm_hip), integer_ms_ssim_hip, psnr_hvs_hip, integer_ssim_hip, ssimulacra2_hip) shipped a VmafFeatureExtractor symbol under core/src/feature/hip/*.c and mirrored their CUDA twins but were missing from core/src/hip/meson.build's hip_sources list and from the extern + registry blocks in core/src/feature/feature_extractor.c. vmaf_get_feature_extractor_by_name(<name>) returned NULL for all six. Fix: add the files to hip_sources and add the matching extern + &vmaf_fex_* rows inside the #if HAVE_HIP blocks; pin the registration in core/test/test_hip_smoke.c with one assertion per extractor. Scaffold-init posture preserved — init() returns -ENOSYS unless enable_hipcc=true. ADR-0533; row added to Recently closed.) Updated: 2026-05-18 (T-VMAF-TUNE-COMPARE-RATE-QUALITY-CHART-FROM-BISECT-SAMPLES-2026-05-18 closed — vmaf-tune compare rate-quality chart now renders genuine per-codec R-Q curves from every probe the underlying target-VMAF bisect computed, instead of connecting picked-CRF cells across (codec, target) pairs (which produced physically impossible downward dips when per-target overshoot varied). Default --target-vmafs flipped from premium-archival 85,90,92,95 to realistic-streaming 75,80,85,90,93. v2 schema gains an optional additive bisect_samples row field; old v2 dumps without it render via the legacy connect-the-dots chart with a caveat note. v1 single-target back-compat preserved via _TrackedDefaultAction sentinel — --target-vmaf NN alone (no --target-vmafs) still emits v1. ADR-0534; row added to Recently closed.) Updated: 2026-05-18 (T-ADR-PARALLEL-RENUMBER-STORM-2026-05-18 closed — scripts/adr/next-free.sh extended with --claim <slug> mode: atomically reserves the next free ADR number by writing a stub file under a POSIX-mkdir lock, so parallel agents on the same host always get distinct numbers. Remote-branch awareness added via git ls-remote --heads + git ls-tree scan to close the cross-branch race window. Smoke test suite at scripts/adr/test-next-free.sh. ADR-0535; row added to Recently closed.) Updated: 2026-05-18 (T-PER-SHOT-BITRATE-PREDICATE-CHAIN-2026-05-18 closed — PR #1290 (ADR-0531) introduced ShotRecommendation.bitrate_kbps and documented the bitrate_sidecar wiring, but left _build_per_shot_bisect_predicate returning only the predicate closure (discarding result.bitrate_kbps). All 12 shots in the BBB v11 4K plan still showed bitrate_kbps: null. Fix (ADR-0538): _build_per_shot_bisect_predicate now returns (predicate, bitrate_sidecar) where the closure populates the sidecar dict keyed by (start_frame, end_frame) as each shot's bisect completes; _run_tune_per_shot unpacks the tuple and patches each ShotRecommendation via dataclasses.replace before serialising the plan JSON. PredicateFn type alias unchanged — no blast radius on --predicate-module callers. +1 regression test. ADR-0538; row added to Recently closed.) Updated: 2026-05-18 (T-HIP-INTEGER-MOTION-UNREGISTERED-2026-05-18 closed — vmaf_fex_integer_motion_hip was defined in feature/hip/integer_motion_hip.c but its extern declaration and feature_extractor_list[] entry were never added to feature_extractor.c, making every vmaf_use_feature("motion_hip", NULL) call silently return an error since PR #1167. test_hip_motion3_parity has been failing at that lookup since it was added. Fix: add the extern declaration and list entry inside #if HAVE_HIP, mirroring the CUDA twin. ADR-0523; row added to Recently closed.) Updated: 2026-05-18 (T-HIP-IMPORT-STATE-ENOSYS-2026-05-18 closed — vmaf_hip_import_state was returning -ENOSYS and breaking vmaf --backend hip on every AMD ROCm host. Moved the function from core/src/hip/common.c into core/src/libvmaf.c and implemented it as a SYCL / Vulkan / Metal-style "stash the borrowed state pointer on the VmafContext and return 0" wrapper. End-to-end verified on AMD gfx1036 inside vmaf-dev-mcp: VMAF = 76.66783, bit-exact match against CPU. The HIP-flagged extractors keep VMAF_FEATURE_EXTRACTOR_HIP cleared for now (dispatch routes through their CPU twins, which is why scores match CPU exactly); the flag-bit promotion + VMAF_PICTURE_BUFFER_TYPE_HIP_DEVICE plumbing is a separate follow-up. Closes the last Open bug from the post-ADR-0514 container-backend-exposure investigation. ADR-0519; row added to Recently closed.) Updated: 2026-05-18 (T-WINDOWS-MSVC-UNISTD-H-2026-05-18 closed — Build — Windows MSVC + CUDA and Build — Windows MSVC + oneAPI SYCL matrix legs fixed. Two portability gaps remained after PR #1274 (ADR-0515): (1) vif_avx512.c used __attribute__((noinline, noclone)) (GCC/Clang-only syntax) on the ADR-0503 noinline helpers, causing fatal C2143 on MSVC cl.exe — fixed by the VMAF_NOINLINE_NOCLONE portability macro; (2) yuv_input.c::yuv_check_file_size called fstat() + S_ISREG() (POSIX names absent from MSVC <sys/stat.h>) — fixed by _WIN32 shims mapping them to _fstat64/struct __stat64/_S_IFREG. ADR-0521; row added to Recently closed.) Updated: 2026-05-18 (T-ADR-0486-TRIPLICATE-COLLISION cleaned up — three files shared the 0486- prefix in docs/adr/: 0486-context-api-contract-doc.md (first committed in PR #1090 at 10:24 CEST), 0486-aiutils-subprocess-dedup.md, and 0486-ms-ssim-sycl-enable-lcs-parity.md (both landed later in the same squash commit). The original 0486-context-api-contract-doc.md retains its number per ADR-0028 (keep earliest). The two later duplicates are renumbered to ADR-0524 (aiutils-subprocess-dedup) and ADR-0525 (ms-ssim-sycl-enable-lcs-parity); cross-references in ADR-0487, ADR-0490, and the two changelog fragments updated accordingly. scripts/ci/check-adr-numbering.sh now passes clean for the 0486 prefix. Row added to Recently closed per ADR-0165.) Updated: 2026-05-18 (T-TINY-MODEL-LOADER-FEATURE-RANK-2026-05-18 closed — vmaf --tiny-model now loads and runs the three shipped FR-regressor tiny models (fr_regressor_v1, fr_regressor_v2, vmaf_tiny_v4). Before the fix all three failed at attach time with errno -95 (ENOTSUP): the C-side vmaf_ctx_dnn_attach rejected any input rank other than 4 (NCHW image), but every shipped FR-regressor is rank-2 feature-vector. The loader now branches on rank, materialises the canonical-6 features from the live feature collector at inference time, applies the sidecar's optional StandardScaler, and pre-seeds the optional codec block (e.g. fr_regressor_v2's 14-D codec input) to the "unknown encoder" one-hot. External-data ONNX (sibling .onnx.data files) needs no extra wiring — ONNX Runtime auto-resolves siblings when handed the absolute model path. ADR-0518; row added to Recently closed.) Updated: 2026-05-18 (T-VMAF-TUNE-COMPARE-RATE-QUALITY-SWEEP-2026-05-18 closed — vmaf-tune compare gains --target-vmafs 85,90,92,95 and a v2 JSON schema (schema_version: 2, rows keyed by (codec, target_vmaf)). vmaf-tune report detects the schema discriminator and renders a per-codec rate-quality line chart (log-bitrate / VMAF axes) with the pareto frontier highlighted as a heavier dashed overlay, plus a per-codec / per-target summary table. New compare_codecs_sweep() API flat-dispatches the (codec x target) cross-product to the thread pool. New probe_encoder_available() two-stage probe (ffmpeg -encoders listing grep + 1-frame lavfi dummy encode) flags hardware-encoder rows that the host can't run with a stable hardware encoder not available: … error string so the sweep does not abort. Default --encoders is now the CPU set libx264,libx265,libsvtav1,libvpx-vp9. v1 compare JSONs continue to render the legacy bar+dot chart. ADR-0516; row added to Recently closed.) Updated: 2026-05-18 (T-BBB-E2E-V7-1-COMPARE-FRAMERATE-PROBE-2026-05-18 closed — vmaf-tune compare --src container.mp4 now auto-probes the source via vmaftune.report.probe_source and substitutes the probed framerate / duration when the user left those flags at their argparse defaults. Before the fix, the per-iteration frame_skip_ref / frame_cnt derived from the default --framerate=24.0 mis-indexed the reference YUV against any container whose native rate was not 24 fps (e.g. BBB 60 fps reported libx264 CRF=6 / VMAF=90.43, physically impossible). Sister fix to ADR-0505; the compare encode plumbing already passed source_is_container=True correctly. ADR-0509; row added to Recently closed.) Updated: 2026-05-18 (T-PER-SHOT-SCENE-THRESHOLD-2026-05-18 + T-PER-SHOT-REPORT-1-SHOT-CHART-2026-05-18 closed — vmaf-tune tune-per-shot on 5 s BBB 4K returned a single shot covering the whole clip because the vmaf-perShot luma-delta heuristic uses a compiled-in default cutoff of 12.0 (too conservative for short content) and the Python wrapper had no override + no fallback. The HTML report's per-shot timeline was also blank when the source resolved to 1 shot because ax.step on a single point emits a zero-length path the SVG backend drops. Bundled fix: --scene-threshold flag forwards to vmaf-perShot --diff-threshold; new --max-shot-duration flag (default 2.0 s) uniformly slices any shot longer than the window into equal-length sub-shots so 5 s clips always produce >= 2 shots; report's _shot_plot_fn renders ax.hlines bands over each shot's frame range with explicit axis bounds. +6 regression tests across test_per_shot.py + test_report.py. ADR-0513; rows added to Recently closed.) Previously: 2026-05-18 T-CHUG-EXTRACT-VMAF-ALIGNMENT-2026-05-18 closed — ai/scripts/extract_k150k_features.py was silently producing identity-pair feature dumps (VMAF~99 on every CHUG bitrate-ladder rung including 360p @ 0.2 Mbps) when pointed at a CHUG sidecar, because the K150K script is an FR-from-NR adapter (ref==distorted) intended for KoNViD-150k-A only. Root cause was operator-level (wrong script for the corpus), but the misuse was completely silent — no exit code, no warning, no per-row provenance flag. New detect_fr_corpus_misuse() helper inspects the --metadata-jsonl sidecar for the FR signature (any chug_content_name group with both chug_ref==1 and chug_ref==0 rows); main() exits 2 before spawning any worker and points the operator at ai/scripts/chug_extract_features.py. --allow-fr-from-nr opt-in preserves the identity-pair study workflow. +3 detector unit tests + 2 pairing regression tests on chug_extract_features.py (ref_path != dis_path invariant; orphan distorted rows are dropped) + a synthetic-YUV end-to-end smoke asserting adm2_mean < 0.95 on a deliberately destroyed distorted clip. Same-family precedent: ADR-0503 (BBB v5 source-is-container propagation). ADR-0510; row added to Recently closed.) Updated: 2026-05-18 (T-DEV-MCP-BACKEND-EXPOSURE-2026-05-18 closed — vmaf-dev-mcp container now exposes every host GPU backend (CUDA + SYCL + Vulkan + HIP) on multi-GPU hosts; dev/Containerfile adds ${ONEAPI_ROOT}/tcm/latest/lib to LD_LIBRARY_PATH for the level-zero UR adapter's libhwloc.so.15 dlopen, clears the bogus VK_ICD_FILENAMES=lvp_icd.x86_64.json pin, and adds a build-time backend probe scanning for "built without X support" regressions; dev/docker-compose.yml bind-mounts /dev/dri/by-path so the Intel compute-runtime can resolve Arc via its udev pci symlink. ADR-0514; row added to Recently closed.) Updated: 2026-05-18 (T-CI-MINGW64-TEST-PUBLIC-API-SCORE-MKSTEMP-2026-05-18 closed — Build — Windows MinGW64 (CPU) matrix leg was perpetually red on master because core/test/test_public_api_score.c::test_vmaf_write_output hardcoded /tmp/vmaf_test_output_XXXXXX + mkstemp(3); MSYS2/MinGW64 inside the GitHub Actions windows-latest runner does not expose a usable /tmp from the MINGW64 shell, so mkstemp failed with ENOENT. Fix: factor the temp-path setup into make_temp_output_path() — GetTempPathA() + <pid>-suffixed filename on #ifdef _WIN32, mkstemp on POSIX; unlink → remove for Win32 portability. Mirrors the precedent in core/test/dnn/test_model_loader.c::test_sidecar_parses. ADR-0515; row added to Recently closed.) Updated: 2026-05-18 (T-BBB-E2E-V8A-LADDER-PASS1-DURATION-2026-05-18 closed — build_pass1_stats_command now mirrors the ADR-0506 V6-1 fallback and emits input-side -t req.duration_s when the caller bound --duration N without sample-clip mode. Before the fix, libx264 (and any codec adapter with supports_encoder_stats=True) routed through run_encode_with_stats whose pass-1 invocation still re-encoded the full source — so ladder --duration 5 against a 10-minute BBB source burned ~10 min wall time per cell on pass 1 before pass 2 ran on the requested 5-second window. ADR-0508; row added to Recently closed.) Updated: 2026-05-18 (T-BBB-E2E-V6-CLUSTER-2026-05-18 closed — ladder --duration N now actually clips the ffmpeg encode pipe (V6-1; previously the flag was metadata-only and a 10-second smoke run re-encoded the full 9-minute source); _decode_source_to_yuv synthesises the demuxer-side -f rawvideo -s WxH -r FR block when the source is raw YUV (V6-2; cross-res rungs against raw sources now score successfully); _run_ladder wraps build_and_emit in try/except and returns 2 on RuntimeError/ValueError/OSError (V6-3); row added to Recently closed.) Updated: 2026-05-18 (T-BBB-E2E-V5-CLUSTER-2026-05-18 closed — corpus.iter_rows now sets EncodeRequest.source_is_container=True for non-raw sources so ffmpeg auto-detects container shape instead of re-interpreting compressed bytes as planar YUV (V5-2 — fixes uniformly-bogus ~50 Mbps encodes and VMAF 4-9 on BBB MP4 sources); make_default_sampler gains a cloud_sink kwarg that captures the full per-CRF sweep, threaded into build_and_emit via extra_samples so the JSON samples[] array carries every encoded CRF row (V5-3 dedup'd by (width, height, crf)); V5-1 (vmaf --backend vulkan exit-byte propagation) re-pinned via a hardened integration test that probes $VMAF_BIN_FOR_TESTS, $PATH, and build/tools/vmaf; row added to Recently closed.) Updated: 2026-05-18 (T-BBB-E2E-V4-CLUSTER-2026-05-18 closed — _maybe_decode_reference now scales the reference leg to the rung target on cross-resolution sweeps (V4-B); vmaf-tune-ladder/v1 JSON emits a top-level samples[] array; vmaf-tune report distinguishes encoder unavailable rows from real encode failures via a new degraded=true flag (V4-C); ADR-0498 strict-mode non-zero exit pinned by Python integration test (V4-A regression guard); row added to Recently closed.) Updated: 2026-05-17 (T-VK-VIF-FP32-PRECISION-GAP closed — Vulkan VIF shader g/sv_sq promoted to double via GL_EXT_shader_explicit_arithmetic_types_float64; eliminates ~7 ULP/px fp32 bias and passes ADR-0214 places=4 gate; row added to Recently closed.) Updated: 2026-05-17 (T-CUDA-KERNEL-LIFECYCLE-HELPERS-CASCADE -- confirmed VmafCudaKernelLifecycle / VmafCudaKernelReadback helpers are intact in kernel_template.h; cascade regressions in dev-mcp container build (#1192-#1203) were nv-codec-headers missing, wrong meson setup path, SYCL std::powf / duplicate-field errors, and CUDA vif missing field -- all now resolved; row added to Confirmed not-affected.) Updated: 2026-05-17 (integer_adm CUDA and Vulkan backends now honour adm_skip_scale0; the option was registered on the CPU extractor but absent from both GPU paths, so scale-0 was always accumulated; row added to Recently closed.) Updated: 2026-05-17 (test_cambi UBSan deselect and test_framesync TSan deselect retired — test_cambi is clean after PR #761 (AVX2 runtime gate, 2026-05-11); test_framesync is clean after PR #548 (mutex-domain fix, 2026-05-09, nightly TSan green 2026-05-09/10); both removed from the sanitizer EXCLUDE regexes.) Updated: 2026-05-16 (Staleness sweep — 5 stale verified-notes corrected: PR #512 Phase-3b MERGED (T-VK-VIF-1.4-RESIDUAL-ARC); PR #469 MERGED (T6-2a-followup' path B, two rows); PR #497 MERGED 2026-05-09 — Research-0090 deferred trigger fired, now actionable; PR #443 + #444 CLOSED without merge, stale cross-refs struck.) Updated: 2026-05-16 (T-CAMBI-CUDA-HOST-PREPROCESSING-SEGV closed — cambi_cuda SIGSEGV on every frame fixed by downloading dist_pic GPU→host before host preprocessing; row added to Recently closed.) Updated: 2026-05-16 (PR fix/sycl-motion-fps-weight-vulkan-import-status-2026-05-16 — closed T-VK-T7-29-PART-2-IMPORT-NOT-IMPL; all three Vulkan import entry points fully implemented, stale -ENOSYS header comments removed. SYCL motion_v2 gains motion_fps_weight option.) Updated: 2026-05-16 (Audit findings #7, #8, #10 fixed — pthread_*_init return checks in thread_pool.c, NULL-guard + return-error in adm_dwt2_* in adm_tools.c, w/h overflow guard in vmaf_picture_alloc; rows added to Recently closed.) _Updated: 2026-05-16 (MS-SSIM GPU option-parity bug fixed — CUDA float_ms_ssim extractor now honours enable_db and clip_db; SYCL extractor now honours enable_lcs, enable_db, and clip_db; previously all were silently dropped; ADR-0460 / Research-0137; row added to Recently closed.) Updated: 2026-05-16 (GPU PSNR enable_chroma option-parity bug fixed — psnr_cuda, psnr_sycl, psnr_vulkan now honour enable_chroma=false; previously the option was silently dropped and GPU extractors emitted full chroma on non-YUV400 sources regardless of the flag; ADR-0453 / Research-0136; row added to Recently closed.) Updated: 2026-05-16 (Issue #857 closed — cambi_cuda SIGSEGV fixed; wrong kernel parameter type in cuLaunchKernel dispatch helpers; row added to Recently closed.) Updated: 2026-05-18 (Vulkan VIF two-variant compute shader: ADR-0492's hard shaderFloat64 refusal replaced by runtime fp32/fp64 auto-pick — vmaf --backend vulkan now runs on Intel Arc / AMD iGPU / older NVIDIA. New T-VK-NO-SHADERFLOAT64-REFUSAL-2026-05-18 row added to Recently closed; T-VK-VIF-FP32-PRECISION-GAP row updated to note the supersession. ADR-0512.) Updated: 2026-05-15 (Batch 6 code-quality cleanups — added Open row T-VCQ-223-LOCAL-EXPLAINER-HANG; T-VK-T7-29-PART-2-IMPORT-NOT-IMPL and T-CAMBI-HIP-NOT-STARTED are now in Recently closed.) Updated: 2026-05-15 (vmaf CLI -c short option handler added and atoi() in vmaf_bench.c replaced — audit slice A findings F1 and F2 closed; ADR-0438; --tiny-model-verify doc corrected to boolean; rows added to Recently closed.) Updated: 2026-05-15 (saliency_student_v2 promoted to production default (IoU 0.7105 vs v1 0.6558, +8.3 %); registry CI job confirmed wired; learned_filter_v1 and nr_metric_v1 model cards added — ADR-0444.) Updated: 2026-05-15 (test_cli_parse and test_predict sanitizer deselects retired — current master passes both tests under ASan+LSan, UBSan, and TSan, so the sanitizer matrix now runs them again.) Updated: 2026-05-15 (vmaf-tune HDR dispatch coverage widened — HDR cells for AV1 NVENC, HEVC/AV1 QSV, HEVC/AV1 AMF, HEVC VideoToolbox, and libaom-av1 now receive central hdr_codec_args() color signaling instead of empty HDR args; HDR model weights remain gated on CHUG / upstream model availability.) Updated: 2026-05-14 (tiny-AI training discovery synthesis added — committed sidecars/cards now produce a reproducible discovery report; CHUG UGC-HDR ingestion and reference-aligned feature materialisation are wired as local-only .workingdir2/chug/ pipeline steps for HDR unlock work.) Updated: 2026-05-14 (vmaf-tune ladder --with-uncertainty scaffold gap closed — corpus rows with vmaf_interval now flow through the CLI, and point-only rows use the active wide-interval threshold as a conservative fallback before the ADR-0279 prune/insert rung recipe runs.) Updated: 2026-05-14 (vmaf-tune recommend-saliency libaom-av1 ROI gap closed — the dispatcher now writes the shared 16×16 qpfile and passes it via the patched FFmpeg -qpfile bridge instead of falling back to a plain encode.) Updated: 2026-05-14 (Metal dispatch support scaffold closed — vmaf_metal_dispatch_supports() now recognises the eight landed Metal extractor names and provided feature keys instead of returning 0 unconditionally; smoke coverage and Metal backend docs updated.) Updated: 2026-05-14 (Tiny-AI bisect-cache real-feature bridge added — ai/scripts/build_bisect_cache.py now accepts a DMOS/MOS-aligned parquet via --source-features / --target-column, normalises the target to mos, and fits the deterministic ONNX timeline from those real rows while preserving the synthetic default for CI.) Updated: 2026-05-14 (Tiny-AI frame-loader colour-pixfmt gap closed — ai/src/vmaf_train/data/frame_loader.py now decodes rgb24, bgr24, rgba, and bgra packed frames as HxWxC arrays instead of raising the old gray-only NotImplementedError.) Updated: 2026-05-14 (JSON model loader fixed-size parser cap removed — read_json_model.c now grows feature and score-transform knot arrays from the payload, so external models are no longer rejected at 65 features or 11 knots solely because of the old MAX_FEATURE_COUNT / MAX_KNOT_COUNT constants.) Updated: 2026-05-14 (vmaf-tune auto non-smoke scaffold gap narrowed — emitted cells now use the existing predictor path to choose codec-specific CRFs and predictor bitrate / VMAF estimates instead of the old fixed CRF-23 placeholder.) Updated: 2026-05-14 (MCP scaffold-doc cleanup — docs/mcp/index.md, docs/mcp/embedded.md, and docs/mcp/release-channel.md now describe the live embedded stdio / UDS / SSE runtime instead of the retired T5-2 scaffold / stub state.) Updated: 2026-05-14 (Vulkan VIF API-1.4 NVIDIA residual closed — vif.comp now avoids NVIDIA driver 595.71's non-deterministic subgroupAdd(int64_t) path by reducing int64 accumulator fields with an explicit subgroupShuffleXor butterfly; NVIDIA, Arc, and RADV all gate 0/48 at places=4 locally.) Updated: 2026-05-14 (test_pic_preallocation sanitizer deselect retired — current master passes the test under ASan+LSan, UBSan, and TSan, so the sanitizer matrix now runs it again; the other T-SANITIZER-DEFECTS-REVEALED-758 exclusions remain tracked.) Updated: 2026-05-14 (test_feature_collector sanitizer deselect retired — current master passes the test under ASan+LSan, UBSan, and TSan, so the sanitizer matrix now runs it again; the other T-SANITIZER-DEFECTS-REVEALED-758 exclusions remain tracked.) Updated: 2026-05-14 (test_score_pooled_eagain sanitizer deselect retired after fixing the AVX2 ADM direct-LUT UBSan path exposed by the re-enabled test; ASan+LSan, UBSan, and TSan now pass locally, while the other T-SANITIZER-DEFECTS-REVEALED-758 exclusions remain tracked.) Updated: 2026-05-14 (public-doc stub sweep — removed stale Research-0086 stub banners from accepted/proposed/deferred docs, replaced the vmaf-tune --resolution-aware placeholder page with real operator docs, and corrected stale Phase-D ADR links to ADR-0392.) Updated: 2026-05-14 (vmaf-tune Phase F x264 two-pass gap closed — X264Adapter now declares supports_two_pass = True and emits FFmpeg-native -pass / -passlogfile argv; docs updated from "libx265 only" to libx264 + libx265.) Updated: 2026-05-14 (vmaf-tune tune-per-shot scaffold gap closed — CLI now extracts each detected shot and runs the real Phase-B bisect backend by default; --predicate-module remains the custom/test hook.) Updated: 2026-05-14 (vmaf-tune recommend --from-corpus CLI filter bug fixed — failed rows, non-finite VMAF rows, and non-matching encoder / preset rows now follow the same filtering contract as the library API.) Updated: 2026-05-14 (scaffold-state audit on synced origin/master: removed stale Deferred rows for T-HDR-ITER-ROWS and Tiny-AI C1 baseline because both already have Recently closed entries; aligned the fr_regressor_v2_ensemble_v1_seed{0..4} registry rows with ADR-0321's production flip; fixed vmaf-tune auto HDR dispatch + recipe-threshold use.) Updated: 2026-05-14 (vmaf-tune predictor scaffold gap partially closed — six hardware predictor models h264/hevc/av1_{nvenc,qsv} retrained from the real Phase-A hardware corpus; software + AMF predictors remain synthetic stubs until matching corpora exist.) Updated: 2026-05-14 (vmaf-tune compare scaffold gap closed — CLI now binds the real Phase-B bisect backend from explicit source geometry flags by default; --predicate-module remains as the custom/test backend hook.) Updated: 2026-05-14 (vmaf-tune usage-doc scaffold labels retired for coarse-to-fine, Phase E ladder, ladder default sampler, saliency-aware encode, and fast recommend; those docs now describe the wired implementations and remaining production limits.) Updated: 2026-05-14 (vmaf-tune fast --time-budget-s scaffold gap closed — the flag now feeds Optuna's timeout and the JSON n_trials reports completed trials; the standalone fast-path usage page now documents the production surface instead of the old stub placeholder.) Updated: 2026-05-13 (SAN-FLOAT-MS-SSIM-MIN-DIM-LEAK closed — invoke_init already calls fex->close(fex) + free(priv) on every code path; re-verified clean under ASAN_OPTIONS=detect_leaks=1; test_float_ms_ssim_min_dim removed from ASan deselect list in .github/workflows/tests-and-quality-gates.yml; row moved to Recently closed. Duplicate T-VK-1.4-BUMP + T-VK-CIEDE-F32-F64 rows removed from Open section.) Updated: 2026-05-13 (staleness sweep: three Open rows dropped — T-NIGHTLY-TSAN-ADM-INIT and T-CUDA-FEATURE-EXTRACTOR-DOUBLE-WRITE were already in Recently closed (PR #548 + PR #742 / ADR-0385) but the matching Open rows had not been cleaned up; T8-1b Metal runtime "CLOSED" row was misfiled in Open and is now properly in Recently closed (PR #764 / ADR-0420 + layered #765/#766/#767 follow-ups + tap flip). Also removed two duplicate T-VK-1.4-BUMP + T-VK-CIEDE-F32-F64 rows from the Open section.) Updated: 2026-05-11 (T-MACOS-ARM-SVE2-PROBE-FALSE-POSITIVE fixed — meson SVE2 probe now short-circuits is_sve2_supported = false on Darwin to mirror the runtime __linux__ gate; Apple Clang's declarations-only <arm_sve.h> was making cc.compiles() pass on Apple Silicon while the real SVE2 TU fails; ADR-0419; row added to Recently closed.) Updated: 2026-05-10 (T-ROUND9-THREAD-POOL-PTHREAD-CREATE fixed — vmaf_thread_pool_create now checks pthread_create return and handles partial/total thread-spawn failure; n_workers_created field added to fix racy read in destroy; Research-0097; row added to Recently closed.) Updated: 2026-05-10 (T-ROUND8-MCP-TMPDIR-LEAK fixed — describe_worst_frames MCP tool now clears prior-call PNGs at start of each invocation via shutil.rmtree; state.md row added to Recently closed.) Updated: 2026-05-10 (T-ROUND8-OPT-NAN-BYPASS fixed — set_option_double in opt.c now rejects NaN explicitly before bounds check via isnan(); state.md row added to Recently closed.) Updated: 2026-05-10 (T-CUDA-FEATURE-EXTRACTOR-DOUBLE-WRITE fixed — feature_extractor_vector_append() now deduplicates by provided-feature names; ADR-0385; row added to Recently closed). Updated: 2026-05-10 (T-FUZZ-Y4M-NEG-WIDTH-SEGV fixed — y4m_input_open_impl rejects pic_w <= 0 / pic_h <= 0 before allocation; ADR-0382; reproducer seed y4m_neg_width_null_deref.y4m added to y4m_input_known_crashes/; state.md row moved to Recently closed). Updated: 2026-05-10 (state.md staleness sweep: four SAN-* Open rows removed — already in Recently closed via PR #548 (fix/sanitizer-real-bugs-2026-05-09, 2026-05-09); T-PY-FEXT-ATOM-SYNC added to Recently closed — closed by PRs #731 + #732 + #733; T-PYPSNR-AST-EVAL already in Recently closed via PR #724). Updated: 2026-05-10 (vf_libvmaf HIP AVERROR mis-mapping in patch 0011 fixed — AVERROR(EINVAL) → AVERROR(-err) at both HIP init error sites; state.md row added to Recently closed). Updated: 2026-05-10 (PyPsnrFeatureExtractor import error fixed — PR fixes feature_extractor.py class hierarchy; ast.literal_eval + numpy 2.x latent bug newly exposed — tracked as T-PYPSNR-AST-EVAL below). Updated: 2026-05-10 (FFmpeg HIP integration gap closed — patch 0011 hip_device selector shipped, ADR-0380; dedicated libvmaf_hip filter deferred to Deferred section pending FFmpeg ROCm hwdec path). Updated: 2026-05-10 (integer_motion_v2 flush dict leak fixed — round-7 stability audit, Research-0094). Updated: 2026-05-10 (Vulkan VIF scale 2/3 saturation fixed — float_vif.comp SPIR-V optimizer + rd-buffer overflow; ADR-0381, PR #718). Updated: 2026-05-10 (vmaf_cuda_picture_alloc_pinned null-deref CWE-476 cross-PR seam fixed — round-6 audit). Updated: 2026-05-10 (-fsanitize=integer narrowing/overflow defects fixed in picture.c, libvmaf.c, dnn/tensor_io.c; state.md row added to Recently closed). Updated: 2026-05-10 (CUDA motion sub-4K perf root cause fixed — PR #702, ADR-0378). Updated: 2026-05-10 (CUDA vmaf_cuda_buffer_upload/download_async inverted stream-select ternary fixed — c_stream == 0 → c_stream != 0 in common.c:388,416; state.md row added to Recently closed). Updated: 2026-05-10 (Vulkan GCC 16 build-break closed — ADR-0376 static void → static int buffer-invalidate fix; state.md row added to Recently closed). Updated: 2026-05-10 (Phase-A DNN ENOSYS audit finding resolved — confirmed intentional per ADR-0374; state.md row added to Confirmed not-affected). Updated: 2026-05-10 (session backfill: 10 PRs #661–#678 added to Recently closed; duplicate CUDA framesync row removed). Updated: 2026-05-10 (T-NIGHTLY-TSAN-ADM-INIT, SAN-INTEGER-ADM-DIV-LOOKUP-RACE, SAN-FRAMESYNC-MUTEX-DOMAIN moved to Recently closed — all fixed by PR #548 (fix/sanitizer-real-bugs-2026-05-09, merged 2026-05-09); nightly TSan job green 2026-05-09 and 2026-05-10). Updated: 2026-05-09 (comprehensive verify-every-row audit; Research-0090). Updated: 2026-05-09.
Updated: 2026-05-06.
Updated: 2026-05-09.
The tracked, in-tree register of bug status for this fork. Per ADR-0165 and CLAUDE.md §12 rule 13, every PR that closes, opens, or rules out a bug updates this file in the same PR. The goal is to prevent re-investigation of already-closed bugs across session resets.
Scope split:
- This file — bug status only (Open / Recently closed / Confirmed not-affected / Deferred).
docs/adr/— architectural and policy decisions (one file per non-trivial choice; immutable once Accepted)..workingdir2/BACKLOG.md— local planning dossier (T-numbered backlog items, gitignored).
Netflix#N = upstream issue / PR; #N = fork PR.
Open bugs¶
Bugs known to affect the fork or the user-visible surface, with no landed fix yet.
| Bug | Summary | Reproducer | Owner | Target |
|---|---|---|---|---|
| T-SYCL-INIT-LEAKS-EXC-2026-06-19 | RC-audit finding: SYCL error paths leaked USM device memory and let C++ exceptions escape through the C dispatch frame. init_fex_sycl in integer_adm_sycl.cpp / integer_vif_sycl.cpp had bare -ENOMEM returns (device-buffer null-check + feature_name_dict failure) that skipped the NULL-safe close_fex_sycl(fex), leaking already-allocated USM buffers; the ADM null-check also omitted the 11 band/CSF buffers (d_ref_band/d_dis_band/d_csf_f) so a NULL would page-fault a device kernel instead of returning -ENOMEM; both files discarded the vmaf_sycl_memcpy_h2d LUT-upload return (uninitialised on-device LUT after -EIO). common.cpp::vmaf_sycl_graph_submit called record_combined_graphs() (throwing SYCL graph APIs) outside the surrounding try, and dmabuf_import.cpp::vmaf_sycl_import_va_surface_readback ran the VA readback q->memcpy/q->wait() without try/catch, leaking VAImage/mapped buffer and risking std::terminate on a synchronous sycl::exception. | SYCL build + cross-backend run on Intel Arc; error paths exercised via forced USM-alloc failure / corrupt VA surface. CI SYCL leg (clang-tidy-sycl + SYCL build) verifies compilation. | fix/sycl-init-leaks-exception-safety (bug fix, no ADR). | Closed on merge: all SYCL init -ENOMEM/-EIO returns run close_fex_sycl, the ADM null-check covers every USM buffer, both LUT uploads are error-checked, and the graph-record + VA readback are exception-bounded. |
| T-HIP-PSNR-CHROMA-MCP-PARITY-2026-06-20 | RC-audit fixes (closed-on-merge): (1) HIP psnr_hip options[] was empty so enable_chroma was stuck false and psnr_cb/psnr_cr were never emitted despite being advertised — added the option (default true, mirrors CPU/CUDA ADR-0453/0471). (2) MCP Go/Python parity: removed the dead vulkan backend value (ADR-0726), and run_tune_per_shot no longer passes the unsupported --format to vmaf-tune tune-per-shot. (3) Python probe_backend raises ValueError (not KeyError) on a missing backend arg. | Go: go test ./cmd/vmafx-mcp/... green. HIP: option matches the CUDA twin; CI HIP leg builds. Python guard verified in a clean install (CI). | Bug fix (no ADR); RC independent audit. | Closed on merge. |
| T-CI-TOX-PY311-SCIPY-118-2026-06-20 | The (non-required) macOS Build job's tox step reddened on every PR: python/tox.ini envlist was pinned to py311, but the fork's Python deps now require ≥3.12 (scipy>=1.18.0 is Python-3.12+-only; also numpy>=2.4.6, pandas>=3.0.3), so pip install failed with "No matching distribution found for scipy>=1.18.0" under 3.11. Bumped the tox env to py314 to match the CI Python (setup-python 3.14.5). The required Netflix golden gate runs pytest directly (not via tox) and was unaffected. Non-gating, so it did not block merges. | pip install --dry-run 'scipy>=1.18.0' under Python 3.14 → "Would install scipy-1.18.0" (fails under 3.11 with "No matching distribution"). | CI infra. | Closed on merge; the macOS tox step goes green. |
| T-NEON-FMA-FLOAT-ADM-DWT2-2026-06-06 | test_float_adm_dwt2_bitexact fails on ARM CI with a 1-ULP FMA gap after PR #685 wired the AdmSimdDispatch table so float-ADM NEON kernels were dispatched at runtime. The NEON DWT2 uses hardware FMA by default; the scalar reference does not. A follow-up #pragma clang fp contract(off) carve-out (PR #690) was insufficient across all ARM toolchain configs. PR #695 reverted PR #685 in full — the float-ADM SIMD kernels remain compiled but are no longer dispatched at runtime. Follow-up required: rewire dispatch with an FMA-safe NEON DWT2 path. | meson test -C build-arm64 --suite=fast test_float_adm_dwt2_bitexact on macOS arm64 or aarch64 Linux — fails on any toolchain that enables hardware FMA by default. | Owner-driven; see ADR-1057 for the FMA-contract semantics analysis. | Closes when a PR adds a #pragma clang fp contract(off) + GCC-equivalent carve-out to the NEON DWT2 kernel that passes test_float_adm_dwt2_bitexact on all ARM CI legs. |
| T-SYCL-ARC-SSIMULACRA2-PARITY-2026-06-03 | ssimulacra2 SYCL parity gate fails on Intel Arc A380 with max_abs_diff=8.72e-2 (~17× over the 5e-3 places=2 contract). Arc A380 lacks native fp64; the 3-pole IIR Charalampidis blur in ssimulacra2_sycl.cpp::launch_blur accumulates fp32 rounding over up to 329 rows of recurrence state per scale, and over 6 pyramid scales. The XYB and pooling stages already run on host (ADR-0201 precision fix). RTX 4090 passes places=2. The divergence cannot be validated or fixed without hardware access to Arc A380. A device-calibration entry at places=1 (5e-2) minimum is the correct short-term path; a longer-term fix would require Kahan-compensated IIR or integer-accumulator blur, which cannot be validated without the hardware. Same failure observed on Vulkan (max_abs_diff=5.48e-2). Partial fix (branch fix/sycl-arc-ssimulacra2-calibration-short-path): gpu_ulp_calibration.yaml gains a named DG2-G10 SYCL entry (sycl:0x8086:0x56a*) anchoring ssimulacra2 at the default places=2 (5e-3) so gate tooling matches Arc A380 explicitly rather than falling through to the generic Intel glob. The observed 8.72e-2 divergence still exceeds this threshold; a hardware-validated places=1 (5e-2) replacement and the Kahan-compensated IIR rewrite remain as follow-up. | Research-0985 §4 | Owner-driven: replace placeholder with hardware-measured value once Arc A380 calibration corpus is available; implement Kahan-compensated IIR in core/src/feature/sycl/ssimulacra2_sycl.cpp::launch_blur (CUDA twin: core/src/feature/cuda/ssimulacra2/). | Closes when either (a) gpu_ulp_calibration.yaml has a hardware-validated ssimulacra2 entry for dg2-g10 class at places=1 (5e-2) or better, OR (b) a Kahan-IIR kernel change brings the gate within places=2 on Arc A380. | | T-SVTAV1-HDR-ADAPTER-2026-05-20 | vmaf-tune has libsvtav1 and libaom-av1 adapters, but no independently selectable SVT-AV1-HDR runtime for juliobbv-p/svt-av1-hdr. The repo is a BSD-3-Clause-Clear fork of psy-ex/svt-av1-psy with HDR-focused SVT-AV1 changes and community FFmpeg builds; the adapter work must determine whether FFmpeg exposes it through the same libsvtav1 wrapper or a distinct encoder name. | python - <<'PY'\nfrom vmaftune.codec_adapters import known_codecs\nprint('svtav1-hdr' in known_codecs(), 'libsvtav1' in known_codecs())\nPY → False True; gh repo view juliobbv-p/svt-av1-hdr --json isFork,parent,licenseInfo,updatedAt identifies the fork/licence/source. | Owner-driven; likely needs per-codec ffmpeg_bin / runtime-variant support plus a pinned SVT-AV1-HDR build before a separate adapter token is honest. | Closes when vmaf-tune compare can select mainline SVT-AV1 and SVT-AV1-HDR independently in one sweep and documents the HDR fork's tune/CRF/preset/runtime knobs. | | T-VMAFTUNE-PROFILE-REPORT-AUDIT-2026-05-20 | Profile-card reports are now emitted directly by vmaf-tune compare --format html|both, making chart quality part of the compare workflow. The renderer needs a deep audit for graph density, axis scaling, labels, failed-row affordances, bitrate units, mobile layout, deterministic colors, Markdown/HTML parity, and artifact packaging. | Run a multi-codec sweep with vmaf-tune compare --format both --output report.html and inspect the rate-quality, ladder, and per-shot charts for readability on desktop and mobile. Current tests assert SVG presence and some samples, not report usability or graph semantics. | Next backlog branch; start in tools/vmaf-tune/src/vmaftune/report.py and tools/vmaf-tune/tests/test_report.py. | Closes when the audit lands with concrete graph/layout improvements plus regression tests over representative compare v1/v2, ladder, and per-shot reports. |
| ~~T-SYCL-CLANG-TIDY-DISABLED~~ — clang-tidy-sycl CI job re-enabled 2026-06-08 via scripts/ci/gen-sycl-compile-commands.py (synthesises compile_commands.json entries for the meson CUSTOM_COMMAND SYCL TUs) + the existing scripts/ci/clang-tidy-sycl.sh wrapper (-D__SYCL_DEVICE_ONLY__=0 guards __spirv_ControlBarrier/__ocl_event_t). Job stays continue-on-error: true (advisory) until one green master run confirms the approach holds. (ADR-0623) | grep "clang-tidy-sycl:" .github/workflows/lint-and-format.yml — job present without if: false. | Advisory. | Closes (promoted to required gate) after one green master run and re-addition to the Required Checks list. |
| T-ENSEMBLE-V2-PROD-FLIP-DEFERRED-2026-06-13 | The five fr_regressor_v2_ensemble_v1_seed{0..4} rows ship at smoke quality for the RC. ADR-0321 promoted them to production with LOSO-validated weights trained at codec_vocab=14; the vocab was trimmed to 6, so PR #865 regenerated the ONNX in --smoke mode (1 epoch, synthetic) to keep the load path correct and set smoke: true. PR #865 also dropped license/license_url/sigstore_bundle from the five rows — restored here. The production flip is deferred to the locked one-shot post-RC retrain (ensemble is in scope), per ADR-1105. test_fr_regressor_v2_ensemble_seed_rows_are_production is xfail(strict=True) so it auto-fails the moment real weights land. | python3 -m pytest python/test/model_registry_schema_test.py -q → 10 passed, 1 xfailed (the deferred production assertion). | One-shot retrain (post-RC, locked plan). | Closes when the one-shot retrain re-runs export_ensemble_v2_seeds.py at codec_vocab=6, flips smoke: false, and removes the xfail marker (test xpasses → strict failure forces marker removal). |
| T-CI-DOCKER-SMOKE-NO-OUTPUT-2026-06-13 | The (non-required) Docker Image Build smoke test fails on every master tip with "vmaf produced no output", yet the built image scores the 576×324 fixture at pooled mean 94.3230 (exactly the expected 94.32) when reproduced locally — across fresh build, GPU-present and GPU-hidden (runc) runs, and the byte-identical command. The CI-only failure was opaque because the step piped through 2>/dev/null. Hardened: vmaf now writes JSON to a bind-mounted output file instead of /dev/stdout (removes a stdout-piping failure mode and is a candidate fix), and stderr is captured + printed on any non-zero exit so the next CI run surfaces the actual cause. | Local: image scores 94.3230 (PASS). CI: see the next master Docker Image Build run's captured vmaf stderr block. | CI infra (image is functionally correct). | Closes when the hardened smoke step passes on master, or when the captured stderr identifies and a follow-up fixes the CI-only cause. |
| T-SPEED-GPU-REGISTRY-ORPHAN-2026-06-19 | The SpEED GPU twins (speed_{chroma,temporal}_{cuda,sycl,hip}) were unreachable by name on the shipping build. PR #875 split feature_extractor.c → the compiled feature_extractor.cpp but left the six GPU SpEED externs + feature_extractor_list[] entries behind in the now-dead .c (meson compiles only the .cpp). The kernels compiled but vmaf_get_feature_extractor_by_name("speed_chroma_cuda") returned NULL, so SpEED silently fell back to the CPU path — regressing the ADR-0964/0965/0852 GPU SpEED wiring. Found by the RC independent registry audit (read-only worktree, master 97c147da0). CPU Netflix golden gate unaffected (CPU speed_chroma/speed_temporal were always registered). Fix ports the six externs + array entries into the .cpp #if HAVE_{CUDA,SYCL,HIP} blocks and deletes the dead .c twin. | meson test -C build test_feature_extractor (with any GPU backend enabled) → the new by-name resolution asserts pass; before the fix they fail (speed_chroma_cuda must resolve by name). CPU build + test green; feature_extractor.cpp compiles clean under -Denable_cuda=true. | Bug fix (no ADR; implements ADR-0545 dead-file policy, restores ADR-0964/0965/0852). PR #875 root cause. | Closed on merge; full device parity for GPU SpEED tracked under existing cross-backend gates (ADR-0214). | | T-HIP-MOTION-V2-MIRROR-OFF-BY-ONE-2026-06-13 | HIP integer_motion_v2 mv2_mirror used 2*sup-idx-1 at the high boundary while CPU (integer_motion_v2.c:157), CUDA (motion_v2_score.cu:51) and SYCL (integer_motion_v2_sycl.cpp:95) all use reflect-101 2*sup-idx-2. Identical call sites across backends → a genuine one-pixel divergence (same class as the HIP VIF fix, ADR-1103). ADR-0377 wrongly claimed the -1 matched CPU/CUDA. Surfaced by an adversarial fresh-eyes verification sweep. HIP-only; CPU Netflix golden gate unaffected. | Verified across all four backends + call sites; fix compiles under hipcc. Full device cross-backend-diff to run in the dev container (host gfx1036 HIP runtime errored, §15 host-debt). | ADR-1106 supersedes ADR-0377 claim. | Closes on merge; device places=4 parity confirmation tracked as a container follow-up. | | T-CUDA-INIT-SUBMIT-LEAKS-2026-06-19 | Four CUDA feature extractors leak already-acquired resources on init / submit error paths (found by the RC independent audit). integer_ms_ssim_cuda.c: close_fex_cuda never freed the pinned host buffers (h_ref/h_cmp + per-scale h_{l,c,s}_partials); the two init -ENOMEM returns leaked all device buffers + PTX module + lifecycle stream; and submit leaked the tmp_uint pinned staging buffer on its early CHECK_CUDA_RETURN exits. integer_psnr_hvs_cuda.c: the two init bulk-alloc -ENOMEM returns leaked partial device/host buffers + upload stream/event + PTX module. ssimulacra2_cuda.c: an ss2c_alloc_buffers failure returned without freeing the partial buffers + two PTX modules + stream. speed_chroma_cuda.c: the fail_pop label only popped the context, leaking the module + stream + completed buffers (its fail_after_pop sibling also leaked module + stream since free_cuda_buffers does not touch either). Fork-local CUDA error-path only; success path and CPU golden gate unaffected. | Object-compile-check the four edited .c files clean (ninja -C build-cuda src/liblibvmaf_feature.a.p/feature_cuda_{integer_ms_ssim,integer_psnr_hvs,ssimulacra2,speed_chroma}_cuda.c.o); full device leak confirmation via compute-sanitizer --leak-check full on an init/submit/close loop in the dev container (host CUDA fatbin toolchain broken, §15 host-debt). | fix/cuda-init-submit-leaks (bug fix, no ADR). | Closed on merge. |
Deferred (waiting on external dataset access)¶
| Item | Defer rationale | Reopen trigger |
|---|---|---|
T6-2a-followup' — mobilesal_placeholder_v0 real-weights swap (recommended replacement = U-2-Net u2netp) | First MobileSal attempt blocked on CC BY-NC-SA 4.0 licence + Google-Drive-walled distribution + RGB-D mismatch (ADR-0257, PR #328). Recommended U-2-Net u2netp replacement also blocked: license is fine (Apache-2.0) but u2netp.pth is again Google-Drive-walled (no GitHub release, no pinnable raw URL), and U-2-Net's bilinear F.upsample lowers to ONNX Resize which is not on the fork's core/src/dnn/op_allowlist.c. See ADR-0265 + Research-0054. | Any of: (a) T6-2a-widen-allowlist-resize lands (separate ADR-scope decision adding Resize to the allowlist under bounded attributes); (b) T6-2a-mirror-u2netp-via-release lands (fork-local release artefact mirroring u2netp.pth under Apache-2.0 §4 NOTICE); (c) T6-2a-train-saliency-student produces a fork-owned saliency model. Smoke-only placeholder remains shipped meanwhile; saliency_mean stays content-independent. |
T-MOS-HEAD-PRODFLIP — konvid_mos_head_v1 production-flip pending real-corpus training pass (corpus now materialized) | The fork's first MOS head ships with Status: Proposed (ADR-0336, Phase 3 of ADR-0325). Corpus blocker REMOVED 2026-05-15: .workingdir2/konvid-150k/ now contains the materialized 179 GB corpus — 307 682 extracted .mp4 clips, k150ka_scores.csv (4.9 MB), k150kb_scores.csv (59 KB), konvid_150k.jsonl (64 MB), manifest.csv. The synthetic-corpus surrogate gate (PLCC ≥ 0.75) cleared at mean PLCC 0.86; production-flip gate (PLCC ≥ 0.85 mean, ≤ 0.005 spread, SROCC ≥ 0.82, RMSE ≤ 0.45) is the next blocker for the SDR/UGC KonViD head. CHUG HDR model status is tracked separately in T-CHUG-HDR-WIDE-V1-HOLDOUT-VALIDATION (see Deferred section below). | For KonViD promotion, run python ai/scripts/train_konvid_mos_head.py --konvid-150k .workingdir2/konvid-150k/konvid_150k.jsonl; if the gate clears, flip the model card from Proposed to Accepted, register in model/tiny/registry.json with smoke: false, and close this row. |
T-CHUG-HDR-WIDE-V1-HOLDOUT-VALIDATION — chug_hdr_mos_head_v1_wide held-out test validation run; model near-misses the production gate and is not promoted | Held-out test partition (552 rows, never used in training or val) evaluated 2026-05-27 via ai/scripts/validate_chug_hdr_mos_head.py (ADR-0687). Result: PLCC 0.8468 / SROCC 0.8188 / RMSE 0.2639. Val-split score was PLCC 0.8733 / SROCC 0.8528; the held-out gap is consistent with mild overfitting to val during seed-sweep selection. Gate requires PLCC ≥ 0.85 and SROCC ≥ 0.82 (ADR-0325); both miss by small margins (PLCC short 0.003, SROCC short 0.001). Model ships Status: Proposed only; not promoted to production. | Any of: (1) Re-seed training on combined train+val data and re-run held-out validation; (2) add saliency or display-profile features to the schema; (3) increase epochs beyond 300; (4) grow the CHUG corpus with future extraction batches. Run python ai/scripts/validate_chug_hdr_mos_head.py --onnx <new-onnx> to re-evaluate; close this row when gate PASSES on the held-out split. |
Recently closed¶
| T-CODEQL-QUALITY-BATCH-2026-06-27 | Code-scanning backlog cleanup. Triaged all 124 master CodeQL/Semgrep alerts against origin/master (NOT the stale PR-incremental scan): 109 resolved. Dismissed with reasons: SIMD-vs-scalar bit-exact == parity asserts (intentional), bounded float*float products (FP — and golden-unsafe to widen on psnr/moment), # noqa probe-imports in MCP tests, allowlist-mitigated py path-injection, test-only path-injection/toctou/world-writable, the ==== original/modification ==== upstream-diff comments in golden math files (adm_tools/vif_tools), + a generated meson probe file. Fixed (behaviour-neutral): 4 unused Python imports, a 2-label CLI switch→if (vmaf.cpp), include guards on moment.h + alias.h. Remaining 15 open = these 7 fixes (this PR) + 5 Scorecard repo-process items (not code). | no ADR (code-scanning hygiene; audit-derived) | fix/codeql-quality-batch | CPU build + fast suite green; py_compile + ruff check clean (imports confirmed unused); pre-existing test-env relative-import errors are identical with/without the change (tox installs the package in CI). | (2026-06-27) |
| T-ROUND4-FFMPEG-PATCHES-2026-06-27 | Two verified defects in the libvmaf_sycl FFmpeg filter (ffmpeg-patches/0005) from the round-4 audit. (1) sycl_state leak (high): uninit_sycl called vmaf_close() but not vmaf_sycl_state_free() — vmaf_close() does not free the SYCL state (ownership not transferred), leaking the state + USM allocations on every filter close. Now freed explicitly + false comment removed. (2) QSV NULL deref (med): do_vmaf_sycl walked data[3]→mfxFrameSurface1→MemId→mfxHDLPair→first with no NULL guards; a software-fallback QSV surface (no VA backing) crashed it. All chain links now NULL-checked → AVERROR(EINVAL) + diagnostic. #21 (duplicate idempotent configure probe) intentionally left (removing the wrong one risks breaking SYCL detection). | no ADR (bug-fix bundle; audit-derived) | fix/round4-ffmpeg-patches | Regenerated 0005 surgically (only the two +blocks + hunk-count; configure hunk untouched). Full 16-patch series replay CLEAN via git apply --3way against n8.1.1 (CLAUDE rule #14). | (2026-06-27) |
| T-ROUND4-CLI-BUILD-GO-2026-06-27 | Five verified CLI/build/Go-MCP defects from the round-4 audit of the admin-merged #1043–1062 batch. (1) icx fp-model (golden-relevant, icx-only): x86_float_adm_avx2/avx512 meson carve-outs lacked _x86_simd_strict_fp_extra → float-ADM AVX2/512 could FMA-contract under Intel icx differently from scalar; added (no-op on gcc/clang, matches ssim/ssimulacra2 carve-outs). (2) vmaf.cpp wall_time_s read uninit timespec on clock_gettime failure (UB) → zero-init. (3) Windows wall_time_s/vmaf_bench.c now_ms re-queried QPF every call + could div-by-zero → cached static freq + zero guard + (void) the QueryPerformance returns. (4) stale libvmaf/tools/vmaf.c→core/tools/vmaf.cpp comment in core/tools/meson.build. (5) Go MCP allowlist hardened:* AllowedRoots() used RepoRoot's cwd fallback → allowlisted arbitrary cwd-relative trees when run outside the repo; now fails closed via discoverRepoRoot() (repo-relative roots only when CLAUDE.md marker found; container mount + VMAF_MCP_ALLOW unconditional), mirroring C discover_repo_root(). | no ADR (bug-fix bundle; audit-derived) | fix/round4-cli-build-go | CPU build + fast suite 105/105; go build/go vet/go test ./pkg/libvmaf pass; clang-format + gofmt + assertion-density + check-copyright clean. icx carve-out is no-op on gcc/clang → golden unchanged. | (2026-06-27) |
| T-ROUND4-C-BUNDLE-2026-06-27 | Ten verified defects from the adversarial round-4 audit of the admin-merged #1043–1062 batch (the batch's per-PR CI was bypassed). All golden-safe — every fix is on an error/close/comment/DNN path, none on a CPU golden score path. CUDA (high): speed_chroma_cuda + speed_temporal_cuda extract_fex leaked an unbalanced CUDA context-stack entry per frame when cuCtxPopCurrent failed (jumped to the bare error return without re-popping); both now retry the pop via a dedicated fail_after_push/fail_pop label and propagate the CUDA error. core (med/low): read_json_model model_collection_parse_loop leaked the partial model+collection on a malformed non-string key after sub-model 0 (teardown added); model.c vmaf_model_collection_append short-name path freed mc without freeing the allocated mc->model (goto fail_model). DNN (med/low): ort_backend build_input_tensor byte-count multiply could overflow size_t past the element-count guard (added n > SIZE_MAX/sizeof guards); fp32_to_fp16 truncated instead of round-to-nearest, diverging from tensor_io.c::f32_to_f16_one for values in (65504,65520) (now rounds, matching). feature-cpu (low): ciede init returned -ENOMEM for unsupported bitdepth (now -EINVAL); ciede close conjunctive guard leaked s->ref on partial alloc (split to independent guards); cambi close called vmaf_picture_unref on never-allocated slots, poisoning err (guarded); corrected a misleading integer_ssim GPU-twin comment; removed a dead op_h local. Deferred from this bundle: #3 (CUDA motion flush idempotency — needs GPU parity testing). | no ADR (bug-fix bundle; audit-derived) | fix/round4-c-bundle | CPU build + fast suite 105/105; CUDA host objects (speed_chroma/speed_temporal_cuda.c) compile clean (host-gcc; nvcc fatbin gen is a known host-env gap, container-canonical); DNN ort_backend validated by the Tiny-AI required check; clang-format + assertion-density + check-copyright clean. | (2026-06-27) |
| T-ROUND4-AI-BUNDLE-2026-06-27 | Three verified AI-training-harness data-integrity/durability defects from the round-4 audit of the admin-merged #1043–1062 batch (training-harness only; no libvmaf/CLI/model/Netflix-golden impact). (1) online_trainer._export_checkpoint advanced _checkpoint_counter + returned the path before export_onnx(), so a failed export burned a version and returned a ghost path; counter now advances + path returned only on a successful durable export (failure → None). (2) online_trainer.ingest cleared _pending inside the lock then trained outside it, dropping the just-cleared samples on a backward-pass RuntimeError; now snapshotted + restored before re-raise. (3) extract_ugc_features skips clips whose integer aspect down-scale rounds a dimension < 2 (1px-wide source → target_w==0), avoiding an ffmpeg scale=0 / ZeroDivisionError on resume. | no ADR (bug-fix bundle; audit-derived) | fix/round4-ai-bundle | py_compile clean; pytest ai/tests -k 'ugc or trainer' 79 passed/8 skipped. Training-harness only. | (2026-06-27) |
| T-MASTER-CI-TSAN-ARM-GOLDEN-2026-06-27 | Two regressions that rode into master via the 2026-06-27 admin-merge batch (per-PR CI bypassed). (1) Sanitizers (TSan) link failure: the R2-9 OOM-injection test core/test/test_gpu_dispatch_env_oom.cpp replaces the global operator new/operator delete, which collides with the sanitizer allocator interceptors — ld.lld: error: duplicate symbol: operator new(unsigned long) — breaking the required Sanitizers check. Fix: the override + the test now self-skip under __SANITIZE_THREAD__/__SANITIZE_ADDRESS__/__SANITIZE_MEMORY__ and the Clang __has_feature equivalents; the pure-logic R2-9 slot-poisoning check still runs in every non-sanitized suite, so coverage is retained. (2) ARM golden drift: PR #1060's aarch64-only -ffp-contract=off guard on the scalar adm_dwt2_s/adm_dwt2_lo_s (ADR-1057 follow-up) shifted the akiyo disable_enhn_gain ADM score on the ARM build matrix (88.030463→88.030322), failing vmafexec_test.py golden assertions (x86 D24 was unaffected — the guard was ARCH_AARCH64-gated, which is why it was not caught pre-merge). Per global rule #1 (immutable golden; fix the code, never the assertion) the scalar guard is reverted — adm_tools.c returns to its pre-#1060 FMA-default scalar arithmetic on aarch64 — and the now-stale test_float_adm_simd.c parity test + its meson registration are removed. The NEON-vs-scalar parity gap returns to the undispatched/FMA-free state of ADR-1057; T-NEON-FMA-FLOAT-ADM-DWT2-2026-06-06 stays Open for a correct re-attempt (make NEON match the scalar FMA reference; validate against the full ARM quality suite). (3) Windows MSVC+CUDA build (required), pre-existing: test_gpu_dispatch_env_oom is the only test whose body is a C++ TU using mu_assert, which returns its string-literal message as char *. On MSVC /std:c++latest that conversion is a hard C2440, and the GCC/Clang-only -Wno-write-strings flag the target carried additionally made cl abort with D8021: invalid numeric argument '/Wno-write-strings' — failing Build — Windows MSVC + CUDA (build only). Fixed by holding the two assert messages in static char[] buffers (decay to char *, no conversion; static storage keeps the returned pointer valid) and dropping the obsolete flag; compiles identically on MSVC/GCC/Clang. C-sourced tests (test_dict/model/feature are .c) are unaffected. | ADR-1057 (Update 2026-06-27) | fix/master-ci-tsan-arm-golden | CPU build + fast suite green locally; test_gpu_dispatch_env_oom builds + passes; adm_tools.c byte-identical to pre-#1060 (git diff 2d2f45283e^ -- core/src/feature/adm_tools.c empty), restoring the akiyo 88.030463 golden on ARM. Prior #1063 run: 22/24 required green (Sanitizers-thread + D24 confirmed PASS). | (2026-06-27) |
| T-BUGHUNT-R3-BUILD-GPU-2026-06-27 | Round-3 build/GPU error-path batch. R3-6 HIP integer_vif init returned uninitialized err on fail_stream/fail_submit (declare int err=0 early). R3-9 NVTX linked empty shared_library('dl') → cc.find_library('dl'). R3-10 ssim AVX2 carve-out missing _x86_simd_strict_fp_extra (icx fp divergence; no-op gcc/clang). Deferred: R2-6 icpx-motion needs a scoped motion carve-out (golden-sensitive). | no ADR (bug-fix batch) | fix/round3-build-gpu-batch | meson reports libvmaf 3.2.0; CPU build 1280/1280; R3-6 HIP-only, R3-10 icx-only → golden gate unchanged. | (2026-06-27) |
| T-VERSION-TRACK-UPSTREAM-3.2.0-2026-06-27 | Fork libvmaf version drifted behind upstream (fork 3.0.0 vs Netflix 3.2.0 SONAME, upstream 3f9e02af25). Bumped core/meson.build version + .release-please-manifest.json base to 3.2.0 / 3.2.0-lusoris.0 per CLAUDE §11 (track upstream version + lusoris suffix). Source-only; no release performed. | no ADR (executes existing §11 versioning policy) | chore/version-3.2.0 | meson setup reports libvmaf 3.2.0; no build/golden impact (version string only). | (2026-06-27) |
| T-BUGHUNT-CLI-2026-06-27 | CLI bug-hunt (core/tools/, golden-safe). --help now prints to stdout + exits 0 (was stderr/exit 1). Output-write return is checked (diagnostic + nonzero exit on failure). Dead vmaf.c deleted (genuinely unreferenced; cli_parse.c KEPT — it's compiled into cli_parse/fuzz tests). Wall-time FPS (was clock()/CLOCKS_PER_SEC, over-counting ~n_threads). No-frames guard (picture_index==0 → exit 101, was UINT_MAX underflow). | no ADR (bug-fix batch) | fix/bughunt-cli | CPU build 1280/1280, fast suite 106/106; smoke: --help exit 0/stdout, bad-output exit 254, no-frames exit 101, golden 576x324 mean 94.32301. | (2026-06-27) |
| T-BUGHUNT-GO-RUST-BUILD-2026-06-27 | go-rust-build cluster, four disjoint fork-local fixes re-derived cleanly onto current master (the original fix/bughunt-go-rust branch is a corrupted orphan whose owned-path delta would revert merged work; re-implemented from intent + verified against master). (1) GPU probe timeout ineffective (pkg/gpu/detect.go::runProbe): cmd.WaitDelay was set but no context — WaitDelay alone cannot cap a child that produces no output, so a wedged nvidia-smi / driver-blocked rocm-smi could stall gpu.Detect() and node startup forever. Now exec.CommandContext + context.WithTimeout(probeTimeout) + a 1 s grace WaitDelay; the deadline fires and Detect() falls back to CPU. (2) AI inference uncancellable (pkg/ai/infer.go::Registry.Infer): ran vmafx-ort-runner via exec.Command with no context, so a wedged ORT runner hung the worker job. Added a ctx context.Context first parameter + exec.CommandContext with an inferTimeout upper bound when the caller supplies no deadline (mirrors pkg/encoder/pkg/bisect); 3 test call-sites updated. (3) Rust -sys picture double-free footgun (bindings/rust/vmafx-sys/src/safe.rs::read_pictures): borrowed &mut VmafPicture while keeping unref_picture public, so a caller could double-free after the documented ownership transfer. Now consumes both pictures by value (use-after-move is a compile error). The error path does NOT manually unref — the libvmaf contract takes ownership for the call's duration, so a second unref is a use-after-free against a CUDA-enabled libvmaf — matching the vmafx crate's Context::read_pictures (PR #1056 round-3 R3-2). DEDUP: the vmafx crate's separate read_pictures double-free was already fixed by PR #1056 and is NOT re-touched. (4) Stale meson comment (core/src/meson.build): claimed enable_rust_features defaults true; core/meson_options.txt is value: false — corrected to the single source of truth. ALSO restored docs/state.md (truncated to 0 bytes by PR #1055) and docs/rebase-notes.md (truncated to 0 bytes by PR #1060) — two unrelated accidental wipes. | no ADR (bug-fix batch; picture-ownership invariant added to bindings/rust/vmafx-sys/AGENTS.md) | gorust-rederive | Go: go build ./... clean, go vet/go test ./pkg/gpu/... ./pkg/ai/... + ./cmd/vmafx-controller/... pass; gofmt clean. Rust: cargo build/cargo test/cargo clippy --all-targets on vmafx-sys green (netflix_golden_score integration test passes by-move with LD_LIBRARY_PATH=/usr/local/lib:/opt/intel/oneapi-2025.3/2025.3/lib); vmafx crate build+test still green incl. #1056's UAF smoke test; cargo fmt clean. Golden-safe (off the metric path). | (2026-06-27) | | T-BUGHUNT-AI-2026-06-27 | AI training-pipeline data-integrity hardening (bug-hunt sweep). (1) aggregate_corpora.py: a null/non-numeric mos passed the key-schema check but crashed float() before convert_mos; now caught as (ValueError,TypeError) → counted in dropped_bad_scale + skipped. (2) konvid_pair_dataset.py: non-finite (NaN/inf) vmaf/feature values dropped at load (warned) instead of poisoning the regressor; numpy_arrays() asserts finiteness. (3) materialize_saliency_features.py: cached-failure replay now stamps the original status (decode/model/missing) instead of a blank saliency_status. (4) extract_k150k_features.py: duplicate video_name keys in the scores CSV hard-fail (exit 2) before GPU time (dict(zip(...,strict=True)) only checked length). | no ADR (training-harness data-integrity bug-fix batch) | fix/bughunt-ai | py_compile clean; ai/tests new regression tests 59/59 pass. Training-harness only; no libvmaf/CLI/model/Netflix-golden impact. | (2026-06-27) |
| T-BUGHUNT-CUDA-2026-06-27 | Three CUDA-backend bug classes from the 2026-06-27 bug-hunt sweep, all fork-local (no CPU Netflix golden-gate impact — the golden gate is CPU-only). (1) Pinned host-buffer leaks: float_vif_cuda never freed num_host[4]/den_host[4] and float_adm_cuda never freed accum_host[FADM_NUM_SCALES] — both allocated via vmaf_cuda_buffer_host_alloc (page-locked host memory) but absent from close_fex_cuda and the init free_buffers error path, leaking on every vmaf_close(). Added the vmaf_cuda_buffer_host_free calls in both close and init-error paths. (integer_ms_ssim_cuda was already correct — its close frees h_ref/h_cmp + the per-scale h_l/h_c/h_s partial triples — so no change; finding was a false positive.) (2) integer_motion_cuda SAD precision: normalize_and_scale_sad truncated to single precision via an intermediate (float) cast and divided by an unsigned w*h product, diverging from the CPU twin's (double)sad / 256. / (w*h) (integer_motion.c); recast end-to-end in double. (3) speed_temporal / speed_chroma errno squash: all fail: labels reachable from CHECK_CUDA_GOTO returned a literal -EIO, discarding the _cuda_err the macro had already mapped from the CUresult (-ENOMEM/-ENODEV/-EINVAL/-EIO); changed all macro-reachable fail:/fail_pop:/fail_after_pop: labels to return _cuda_err; (the two manual cuMemcpyDtoH/cuCtxPushCurrent boolean checks keep their literal -EIO). | no ADR (bug fix; CUDA fork-local, no golden impact) | fix/bughunt-cuda | Built -Denable_cuda=true (CUDA 13.3, RTX 4090); touched-feature tests pass: test_cuda_float_vif_parity, test_cuda_float_motion_parity, test_cuda_motion3_parity, test_cuda_motion_v2_parity, test_cuda_speed_temporal_{smoke,parity}, test_cuda_speed_chroma_{smoke,parity}, test_cuda_preallocation_leak, test_cuda_pic_preallocation (10/10). Pre-existing parity failures in float_adm/cambi/ssimulacra2/float_moment/psnr_hvs confirmed present on the unmodified baseline (identical deltas) — out of scope. clang-format + assertion-density + check-copyright clean. | (2026-06-27) | | T-BUGHUNT-CORE-ENGINE-2026-06-27 | Three core-engine error-path bugs from the 2026-06-27 bug-hunt sweep (core-engine subsystem). (1) Picture-pool deadlock (core/src/libvmaf.c threaded_read_pictures_batch): when vmaf_thread_pool_enqueue failed, the function returned without unref'ing the caller's ref/dist. Because vmaf_read_pictures treats the done=true return as "ownership transferred" and skips its own cleanup: unref, each failed enqueue leaked a picture-pool slot; once the always-on pool drained, the next vmaf_picture_pool_fetch deadlocked in pthread_cond_wait. Fix: the failure path now unrefs ref/dist like the success path. (2) Wrong error code (core/src/feature/feature_collector.c aggregate_vector_append): the feature-name malloc-failure path returned -EINVAL instead of -ENOMEM; an audit fix had only landed in the non-compiled .cpp twin. Fix: return -ENOMEM, mirroring the .cpp. (3) Handle loss + dropped collection (core/src/model.c vmaf_model_collection_append): the realloc that grows an existing collection took the shared fail: label on failure, which nulled the caller's *model_collection and freed a still-valid collection (realloc leaves the old buffer intact on failure). Fix: the grow path now returns -ENOMEM without taking fail:. The bpc ref/dist &&→|| validate_pic_params item from the same sweep was already fixed in master (test_validate_pic_params_bpc present and green) — skipped as already-handled. | no ADR (error-path bug-fix batch; root causes in commit body) | fix/bughunt-core-engine | CPU build clean (1251/1251 targets); meson test -C core/build --suite=fast 103/103 pass; Netflix golden gate green against the worktree binary — VmafexecQualityRunnerTest 32/32 + vmafexec_feature_extractor_test.py 107/107 (scores 76.667 / 99.946 / 97.428 unchanged). | (2026-06-27) | | T-BUGHUNT-FEATURE-CPU-2026-06-27 | Bug-hunt sweep (feature-cpu subsystem). (1) CIEDE2000 4:2:2 chroma-upsample flag swap (core/src/feature/ciede.c scale_chroma_planes + scale_chroma_planes_hbd): the horizontal sample index used ss_ver and the vertical row advance used ss_hor — the two subsampling flags were transposed. On YUV422P (half-width / full-height chroma) the horizontal index read past the half-width input row (heap OOB) and the vertical advance skipped half the input rows, yielding wrong ciede2000 scores. YUV420P (ss_hor==ss_ver==1, swap is a no-op) and YUV444P (early return, never calls the function) were unaffected, which is why the Netflix golden CIEDE2000 test (420P content) never caught it. Fix: horizontal index → ss_hor, vertical advance → ss_ver. Note: this matches/diverges from upstream Netflix (the upstream code carries the identical transposition); the fix is the correct chroma-subsample math. (2) cambi init() partial-failure leak (core/src/feature/cambi.c): every error return in init() left the picture pool, contrast/tvi/c-values/histogram/mask buffers, and the feature-name dict allocated — the framework never calls close() after a failed init(). Fix: route all error paths through a fail: label calling the null-tolerant close_cambi(). (3) integer_ssim 16-bpc samplemax² signed-int overflow (core/src/feature/integer_ssim.c, round-2 finding R2-1): c1/c2 = samplemax * samplemax * SSIM_K1/K2 * w_d * w_d with int samplemax = (1<<depth)-1; for depth=16 65535*65535 = 4,294,836,225 > INT_MAX wraps to −131071 (signed-overflow UB), corrupting the SSIM stability constants for all 16-bpc input and diverging from the CUDA/HIP/SYCL twins (which use int64_t/double). 8/10/12-bpc fit in int and are bit-unchanged. Fix: hoist const double sm = (double)samplemax; and compute c1=sm*sm*…, matching the GPU twins. (SKIPPED round-2 R2-8 cambi histogram alloc int-overflow: already fixed in tree — c_values_histograms at cambi.c:746-747 already computes (size_t)alloc_w * v_band_size * sizeof(uint16_t); the finding's line citation had drifted.) | no ADR (correctness + error-path bug-fix batch; per CLAUDE §12 r8) | fix/bughunt-feature-cpu | ./build-cpu/test/test_ciede → 6/6 pass incl. new test_ciede_scale_chroma_422_8b/_16b (load-bearing); new test_ssim_16bit_distorted_in_range in test_ssim_coverage (scores 0.999992 with the fix vs 1.185 — out of [0,1] — with the int overflow); meson test -C build-cpu --suite=fast 103/103; cambi golden gate (python/test/cambi_test.py) 13/13; Netflix CIEDE2000 + SSIM golden tests PASS unchanged (8/10/12-bpc numbers identical). Netflix golden assertions untouched. | (2026-06-27) | | T-BUGHUNT-SIMD-2026-06-27 | Two SIMD float_moment bugs from the 2026-06-27 bug-hunt sweep. (1) SVE2 moment widening (wrong math). compute_1st/2nd_moment_sve2 (core/src/feature/arm64/moment_sve2.c) stepped by svcntd() and widened f32→f64 with svcvt_f64_f32_x alone, assuming it converts the lower contiguous f32 lanes. Per the ARM A64 reference, SVE FCVT .s→.d reads the even-indexed f32 lanes (source element 2*i); on any SVE register wider than 64 bits the kernel double-counted even lanes and dropped every odd lane (qemu-measured relative error up to ~45% at 128-bit VL). Fixed by stepping a full svcntw() register and widening both halves — even via svcvt_f64_f32_x, odd via the SVE2 svcvtlt_f64_f32_x (FCVTLT, element 2*i+1) — with merging adds (svadd_f64_m). (2) 2nd-moment SIMD tail squared in double (bit-exactness divergence). The per-row scalar tail in moment_avx2.c / moment_avx512.c / moment_neon.c squared in double ((double)p*(double)p) while the main loop and scalar reference (moment.c) square in float; fixed to (double)(p*p). Golden-safe (576-wide golden content never hits the tail; built binary reproduces float_moment_ref2nd mean 4696.668388 exactly). The duplicate libvmaf.c:1881 bpc &&→|| finding was already fixed on master (line 1903) and is owned by core-engine. | no ADR (bug fix; SVE2 lane-mapping invariant added to core/src/feature/arm64/AGENTS.md) | fix/bughunt-simd | meson test -C build --suite=fast --suite=fast+simd → 103/103 OK (incl. new test_{avx2,avx512,neon}_tail_bitexact); negative control confirms the tail test fails pre-fix; SVE2 fix verified bit-correct vs scalar under qemu-aarch64 -cpu max at VL=128/256/512 across 17 widths (old code FAILs, fixed code rel<1e-12); golden float_moment CLI check on the 576×324 src01 pair matches all four golden means at places=4. | (2026-06-27) |
| T-BUGHUNT-MCP-2026-06-27 | Bug-hunt sweep (mcp subsystem): four Go↔Python MCP parity / security findings fixed; two were already fixed in-tree and skipped. (mcp #1, high) the Go cmd/vmafx-mcp streamable-HTTP transport had no auth, no body limit, and bound all interfaces — the Python ADR-0967 hardening was never ported. New cmd/vmafx-mcp/http_security.go adds a bearer-token middleware (VMAFX_MCP_HTTP_TOKEN, crypto/subtle constant-time compare, VMAFX_MCP_HTTP_NO_AUTH=1 opt-out, refuse-all 401 when neither is set), a 4 MiB body limit (http.MaxBytesReader + Content-Length pre-flight → 413), and a loopback-only default bind (VMAFX_MCP_HTTP_BIND, default 127.0.0.1, applied when mcp.http.addr has no host); wired into main.go::runMCPTransport. (mcp #3, med) score-precision default unified to legacy (%.6f, ADR-0119): the Python HTTP /v1/score path (http_transport.py:520) and the Go direct-cgo→subprocess fallback (impl_direct.go) both defaulted to "17", diverging from the documented C-CLI default and the stdio path. (mcp #4, low) Go eval_model_on_split inline script gained the pred/target shape-mismatch guard the Python _eval_model_on_split already has. (mcp #5, low) the three Go vmaf-tune wrappers (run_compare/run_ladder/run_tune_per_shot) now fold subprocess stderr into the error via a shared runVmafTune helper (vmaf-tune <sub> exited <rc>: <stderr>), mirroring the Python text, instead of discarding it with exec.Output(). Skipped: mcp #2 (subsample on vmaf_score_encoded) and mcp #6 (HTTP /v1/score strict serializer) were already fixed in-tree (scoreExtras.subsample forwarding; http_transport.py:563 _dumps_strict). No Netflix golden assertion touched (MCP servers only). | no ADR (bug-fix batch; env contract mirrors ADR-0967, precision ADR-0119, byte-compat ADR-1117) | fix/bughunt-mcp | go test ./cmd/vmafx-mcp/ → ok (incl. new TestSecurityMiddleware*, TestApplyBindHost, TestRunVmafTune_*); gosec ./cmd/vmafx-mcp/ → 0 issues; gofmt/go vet clean; PYTHONPATH=mcp-server/vmaf-mcp/src python -m pytest mcp-server/vmaf-mcp/tests -q → 451 passed, 2 skipped (incl. new test_score_precision_defaults_to_legacy); ruff clean on touched source. | (2026-06-27) | | T-MCP-PROBE-BACKEND-REQUIRED-ARG-2026-06-20 | The probe_backend MCP tool raised a bespoke "'backend' is required for probe_backend" ValueError when the required backend arg was missing, diverging from every other required-arg tool (which surface the uniform _call_tool KeyError→ValueError message) and breaking test_call_tool_missing_backend_raises_value_error (regex missing required argument.*'backend'). Left the non-required MCP Smoke lane silently red on master. Fix: drop the redundant explicit guard so the missing key flows through the shared wrapper, matching describe_model's name precedent. | no ADR (one-line consistency bug fix) | fix/mcp-probe-backend-required (probe-backend-arg) | PYTHONPATH=mcp-server/vmaf-mcp/src python -m pytest mcp-server/vmaf-mcp/tests -q → 450 passed, 2 skipped (was 1 failed); test_call_tool_missing_backend_raises_value_error + sibling _missing_name both pass. | (2026-06-20) |
| T-GPU-SPEED-COVARIANCE-EIGENBASIS-CORRECTNESS-2026-06-20 | GPU SpEED (speed_chroma / speed_temporal on CUDA / HIP / SYCL) produced wrong scores on every GPU run — never caught because the CUDA/HIP parity tests need a GPU (CI has none) and the SYCL fixture was below the SpEED pyramid minimum so the extractor EINVAL'd before the kernels ran. Two algorithm bugs: (A) the means/cov kernels computed per-tile block-local statistics (means[elem*num_blocks+tile], sampled at tile_y*5+er, summing num_blocks displaced block-local covariances) instead of the CPU's single global covariance over the 5×5-phase-shifted full-plane submatrix (means[25], scalar global means, divide by N once) → ~7× low scores; (B) the distorted path reused the reference covariance + eigenvalue basis via a cov save/restore, and the score kernel shared one eigenvalue array for ref + dis, so distorted entropy was wrong whenever ref ≠ dis (chroma ~2× high; masked on temporal where ref ≈ dis). Fix: rewrite both kernels to the global formulation (means[] stays over-allocated at 25*num_blocks, only [0,25) used; launch geometry unchanged); add d_eigenvalues_ref + DtoD stash, drop the save/restore, and pass separate ref_eigenvalues/dis_eigenvalues to speed_score_kernel. Ported the verified CUDA fix to the HIP + SYCL twins. Co-fixed safety bugs in the same extractors: host read of a CUDA device pointer in picture_copy() (SEGV/frame → cuMemcpyDtoH download first), eigenvalue-scratch heap overflow (n*n+3*n→n*n+4*n, all six GPU extractors), CPU speed.c heap-OOB from an unchecked speed_init_dimensions() return underflowing submatrix_height, SYCL solve-kernel divergent-barrier deadlock (DEVICE_LOST on Intel Arc), and CUDA/HIP init-OOM resource leaks. User-visible numeric correction: GPU SpEED scores now match CPU. | no ADR (correctness bug fix; decision matrix in research-1120) | PR #1029 / fix/speed-extractor-oob-deadlock-heap-corruption | RTX 4090, vmaf-dev-mcp container: test_cuda_speed_chroma_parity + test_cuda_speed_temporal_parity PASS bit-parity (≤ 1e-4) vs CPU (both were wrong before); chroma went cpu=19.84/cuda=2.89 (7× low) → bit-parity. ASan + MALLOC_PERTURB_ clean on the SYCL CPU-OOB repro and the full Arc-GPU speed_chroma pipeline (no heap corruption, no DEVICE_LOST). HIP/SYCL legs compile-verified (identical algorithm; no AMD GPU / Arc lacks fp64 — CI / fp64 hardware run-verifies). | (2026-06-20) | | T-AUDIT-RUNTIME-BUGS-BATCH-2026-06-20 | Full-fork correctness-audit batch: 18 independent, disjoint-file runtime bugs. SYCL (integer_motion_sycl.cpp): five init early-returns — incl. the motion_add_uv + unsupported-pix_fmt path at line 516, reachable without OOM — skipped close_fex_sycl, leaking per-extractor USM device buffers. CUDA: integer_ssim_cuda.c free_ref never free()d the five calloc'd VmafCudaBuffer host wrappers (vmaf_cuda_buffer_free only cuMemFrees the device alloc); integer_vif_cuda.c free_ref post-module OOM path leaked the PTX module + stream + two events. CPU (ssimulacra2.c): partial-OOM init leaked 1–11 of the 12 aligned_malloc buffers (now goto-cleanup frees all 12). AI extraction: bvi_dvc_to_full_features.py (zip + dir modes), extract_full_features.py, konvid_to_full_features.py lacked per-clip try/except, so one corrupt clip aborted a multi-day run; datamodule.py + train_fr_regressor_v2.py collapsed training on a single non-finite feature/MOS row. MCP (cmd/vmafx-mcp/impl.go): the Go server still advertised the removed vulkan backend (≥30-metric branch + keyword set), diverging from the Python server (ADR-0726). | no ADR (mechanical bug-fix batch; root causes in commit body) | PR #1030 (fix/audit-runtime-bugs-batch, commit be6c5f77ac) | meson test -C build --suite=fast (C paths) + python3 -m py_compile of the five touched ai/ scripts (clean); Go path gofmt-clean. Netflix golden assertions untouched. | (2026-06-20) | | T-SYCL-PSNR-HVS-CHROMA-CEILING-2026-06-20 | The SYCL integer_psnr_hvs twin (core/src/feature/sycl/integer_psnr_hvs_sycl.cpp::init_fex_sycl) derived its 4:2:0 / 4:2:2 chroma plane width/height with floor division (w >> 1, h >> 1), while picture.c, the CPU reference, and the CUDA + HIP twins all use ceiling division ((w + 1U) >> 1). On odd-width or odd-height frames the SYCL path allocated chroma planes one block-row/column short, dropping the last 8x8 chroma strip and emitting psnr_hvs_cb / psnr_hvs_cr / psnr_hvs scores that silently diverged from every other backend. Even-dimension inputs were unaffected, so the CPU Netflix golden gate (even-dimension content) never caught it. Same class as the SYCL/Vulkan PSNR chroma ceiling fixes already in tree. Found by the full-fork correctness audit. Fork-local SYCL-only; success path on even dimensions is byte-identical. | no ADR (one-line correctness bug fix; cites picture.c / CPU / CUDA / HIP ceiling convention) | PR #1031 (f6d5ed2a9), branch fix/sycl-psnr-hvs-chroma-ceiling | /cross-backend-diff on an odd-width YUV420P fixture (e.g. 577x325) with --feature psnr_hvs: SYCL psnr_hvs_cb/psnr_hvs_cr/psnr_hvs must match CPU/CUDA/HIP at places=4 (ADR-0214). SYCL build compiles under icpx; device parity confirmation runs in the dev container (host SYCL runtime, §15). | (2026-06-20) | | T-METAL-DRAIN-FRAME0-MOTION2-2026-06-20 | Two darwin-only Metal-backend correctness bugs found by the full-fork audit (Metal extractors never run in CI — no Apple Silicon in the dev/CI lane). (1) flush_context_serial (core/src/libvmaf.c) drained only CUDA/HIP/SYCL extractors' pending final-frame collect() at end-of-stream; the 8 Metal extractors had no drain branch, so the generic submit/collect double-buffer left the last submitted frame's collect(N) pending and the final frame's score (plus the motion/motion2 tail) was silently dropped. Fix: add VMAF_FEATURE_EXTRACTOR_METAL flag (bit 7) in core/src/feature/feature_extractor.h, set it on all 8 registered feature/metal/ extractors, and add a HAVE_METAL drain branch in flush mirroring the HIP path. (2) float_motion_metal (core/src/feature/metal/float_motion_metal.mm) never appended motion2 = 0.0 at index 0 (frame-0 motion2 missing from output) and wrote motion2 twice at index 1; collect() indexing now appends motion2 = 0.0 at index 0 and makes index 1 a no-op, matching integer_motion_metal and the HIP/CUDA twins. Darwin-only; not buildable on the Linux dev/CI lane. Linux/CUDA/HIP/SYCL unaffected — the drain branch is HAVE_METAL-gated and the motion2 change is confined to the Metal .mm. | no ADR (bug fix) | PR #1032 (fix/metal-drain-motion2, 17005c171c) | Apple Silicon: run a Metal extractor end-to-end (vmaf --backend metal --feature motion --reference <ref>.yuv --distorted <dis>.yuv ... --output -) and confirm the last frame's score is emitted and VMAF_feature_motion2_score is present at frame 0. Validate on Apple Silicon — not reproducible on the Linux CI lane. | (2026-06-20) | | T-K150K-TRAINING-DATA-INTEGRITY-2026-06-20 | Two silent training-data-corruption paths in ai/scripts/extract_k150k_features.py, the K150K retrain feature extractor — bugs here waste multi-day GPU runs. (1) Empty-frame clips: _run_vmaf_json returns [] when vmaf decodes a clip but scores zero frames (exit 0); _aggregate_frames([]) returns an all-NaN row and the as_completed loop still calls _append_done, so the clip is dropped from the corpus with no retry — contradicting _process_clip's documented "raises on any failure" contract. Fixed: _process_clip now raises on an empty frame list → caller logs + skips for a later resume. (2) MOS-label join: mos_map keyed on the scores CSV video_name column, looked up by mp4.name; a filename↔video_name extension mismatch made every label NaN with zero validation → regressor trains on garbage. Fixed: mp4.stem fallback tolerates the extension mismatch + an up-front coverage guard hard-fails the zero-match case (before any GPU time) and warns on partial coverage. Staging→.done ordering reviewed and confirmed already crash-safe (staging-first + parquet dedup by clip_name) — no change. No golden-data / C-library impact (training harness only). | no ADR (bug fix) | PR (branch fix/k150k-training-data-integrity) | python3 -m py_compile ai/scripts/extract_k150k_features.py clean; ruff check clean; empty-frame path raises ValueError("no frames scored …"); zero-coverage join raises SystemExit("… MOS-label join matched 0/N …"). | (2026-06-20) | | T-UPSTREAM-V1.0.16-MODELS-2026-06-20 | Port of Netflix upstream commit 4718b4f5f ("Add VMAF v1.0.16 SDR models, documentation, and tests"). Adds the 8 v1.0.16 SDR models — 4 standard (vmaf_v1.0.16_3d0h, _3d0h_2160, _5d0h, _1d5h_2160) under model/vmaf_v1.0.16/ and 4 HFR (vmaf_v1.0.16_hfr_*) under model/vmaf_v1.0.16_hfr/ — as verbatim Netflix model data, registered as built-ins in core/src/model.c and embedded via core/src/meson.build (mirroring the existing v0 xxd -i custom_target idiom). Upstream's bundled feature-source reorg (moving speed.c/convolution.c/vif_tools.c out of the float block) was deliberately not ported — those sources are already wired differently on the fork. Adds the upstream golden test python/test/vmaf_v1_quality_runner_test.py verbatim (46 new assertAlmostEqual, 9 test methods) — no pre-existing golden assertion touched. Fork status: the 4 non-HFR models score correctly on the CPU path (1080p 3H reproduces the upstream golden VMAF 82.816059 on the Netflix src01 pair, places=4). The 4 _hfr variants embed and register but cannot yet be scored — they require motion_five_frame_window=true + motion_moving_average=true, whose prev_prev_ref 5-frame plumbing is deferred per ADR-0337 (already tracked as the Python-skip row T-MOTION-FIVE-FRAME-WINDOW-PYTHON-SKIP-2026-06-06). Enabling the HFR path is the open follow-up. | no ADR: verbatim upstream port (cites 4718b4f5f; HFR-blocker is ADR-0337) | feat/upstream-v1.0.16-models | meson setup build-cpu core -Denable_cuda=false -Denable_sycl=false -Denable_hip=false && ninja -C build-cpu → clean (1251/1251 targets); nm libvmaf.so.3 \| grep -c vmaf_v1_0_16 → 16 (8 models × symbol+len). Direct CLI vmaf --model path=model/vmaf_v1.0.16/vmaf_v1.0.16_3d0h.json on the src01 576×324 pair → VMAF mean 82.816059 (== golden assertion). The Python golden test's non-HFR cases need the fork harness feature-name mapping (integer_adm3_*→VMAFEXEC_adm3_*); HFR cases error with the ADR-0337 message — both verify on CI. | (2026-06-20) | | T-THREADED-MULTI-PREV-REF-STARVATION-2026-06-13 | Under --threads N, two PREV_REF extractors in one batch (e.g. motion + motion_v2; requesting motion_v2 always co-schedules motion) failed on every frame: threaded_read_pictures_batch struct-copied a single prev_ref snapshot into the first extractor and zeroed the shared snapshot, so the second saw prev_ref.ref == NULL and extract() returned -EINVAL ("problem with feature extractor motion_v2"). vmaf_read_pictures failed → 100% of multi-threaded extractions involving motion_v2 broke, including the K150K retrain corpus extraction (--threads 8). Single-threaded path (Netflix golden gate) unaffected, so CI stayed green. Found by the one-shot-retrain K150K smoke test. Fix: each PREV_REF extractor takes its own vmaf_picture_ref(); snapshot released once at unref:. Scores unchanged (threaded == single, verified on KoNViD-150K). | ADR-1107 | PR #906 (f460ee065) | meson test -C build test_thread_safety_batch → test_batch_two_prev_ref_extractors passes (fails before fix: "EOS failed"); extract_k150k_features.py --no-cuda --threads 8 --limit 60 → 60/60 ok. CUDA motion_v2 co-schedule sync error remains a separate GPU-path follow-up. | (2026-06-13) | | T-HIP-MOTION-V2-MIRROR-OFF-BY-ONE-2026-06-13 | HIP integer_motion_v2 mv2_mirror used 2*sup-idx-1 at the high boundary while CPU (integer_motion_v2.c:157), CUDA (motion_v2_score.cu:51) and SYCL (integer_motion_v2_sycl.cpp:95) all use reflect-101 2*sup-idx-2. Identical call sites across backends → a genuine one-pixel divergence (same class as the HIP VIF fix, ADR-1103). ADR-0377 wrongly claimed the -1 matched CPU/CUDA. Surfaced by an adversarial fresh-eyes verification sweep. HIP-only; CPU Netflix golden gate unaffected. | ADR-1106 supersedes ADR-0377 claim | PR #905 (dcd3cad65) | Verified across all four backends + call sites; fix compiles under hipcc. Full device cross-backend-diff to run in the dev container (host gfx1036 HIP runtime errored, §15 host-debt); device places=4 parity confirmation tracked as a container follow-up. | (2026-06-13) | | T-RC-CI-GREENUP-2026-06-13 | Three non-required CI lanes red for the RC. (1) rust-ci.yml pinned dtolnay/rust-toolchain by commit SHA, so the action could not infer the toolchain from its ref → every run failed 'toolchain' is a required input; fixed with explicit toolchain: stable. (2) nightly ThreadSanitizer lane lacked TSAN_OPTIONS=allocator_may_return_null=1, so the intentional ~192 GB-alloc UAF test hard-aborted; env added (mirrors the required tests-and-quality-gates lane). (3) docker/Dockerfile.production-gpu used meson setup /build libvmaf (ADR-0700 rename missed) → every GPU production image failed; fixed to core. Also confirmed (guess+check): the CI CUDA legs CANNOT bump to 13.3.0 — the Jimver installer has no 13.3.0 build (T-CI-JIMVER-CUDA-133); the Docker/dev images use 13.3.0, CI stays 13.2.0, documented as an intentional split. | no ADR: CI-infra fix | PR #912 (da315b893) | rust-ci + nightly lanes go green on PR #912; docker build -f docker/Dockerfile.production-gpu reaches the libvmaf build (no libvmaf/ dir error). | (2026-06-13) | | T-CI-APT-MS-REPO-FLAKE-2026-06-13 | The hosted-runner pre-provisioned Microsoft/Azure apt sources (packages.microsoft.com) intermittently return 403 Forbidden / "no longer signed", failing apt-get update with exit 100. First reddened the (non-required) Coverage Gate, then the required-on-PR Go (go vet + go test) lane (observed reddening PR #911). Fixed by purging those sources before apt-get update in every job that does a bare apt-get update: the Coverage Gate Install-deps step, the Go workflow Install meson + ninja step, and the Rust workflow Install build dependencies step; vmaf needs none of them. | no ADR: CI-infra fix | PR #903 (b9eb49e79) | Coverage Gate + go vet + go test "Install" step logs: E: Failed to fetch https://packages.microsoft.com/... 403 Forbidden → exit 100. Reopen if another job's bare apt-get update flakes on the same repo. | (2026-06-13) | | T-PELORUS-SIDEDATA-READER-WEIGHTING-2026-06-14 | vmafx vendored the Pelorus interop ABI (ADR-1113) but had no way to use the per-frame banding/variance maps. Plan workstream B (B1+B2+B3+B4): a reader that perceptually re-weights VMAF's spatial pooling — frames whose regions carry high banding risk count more. B1 — new opt-in C-API in core/include/libvmaf/perceptual_weight.h (vmaf_set_perceptual_weight_enabled, vmaf_set_perceptual_weight_strength, vmaf_set_perceptual_sidedata(VmafContext*, const uint8_t *blob, size_t len, unsigned pic_index)), mirroring the vmaf_import_feature_score precedent. B2 — weight module core/src/feature/perceptual_weight.c derives a per-frame [0,1] salience from the banding cell-map (variance-modulated) and vmaf_feature_score_pooled applies it as a per-frame weight (weighted MEAN/HARMONIC_MEAN; MIN/MAX unaffected). B4 — R1–R6 compat: min(known_size, dir.size) section reads, unknown bits ignored, grid==0 (today's deband placeholder) degrades to a frame-level scalar, ABI-major mismatch → -EPROTO (unweighted + log), foreign buffer → -ENOENT; all per-cell reads bounds-checked, NaN/Inf clamped. B3 — ffmpeg-patches/0017-libvmaf-read-pelorus-sidedata.patch (vf_libvmaf reads AV_FRAME_DATA_SEI_UNREGISTERED, drives the API; new perceptual_weight AVOption, default 0) + series.txt tail (CLAUDE r14). Golden-gate isolation (#1 requirement): weighting is inert unless BOTH the opt-in is set AND a valid Pelorus blob is present for the frame; the no-side-data path runs the literal upstream pooling expression, so it is byte-identical — the Netflix golden pairs carry no side-data and score bit-exact. | ADR-1118 | feat/pelorus-sidedata-reader | meson setup core/build-cpu core -Denable_cuda=false -Denable_sycl=false && ninja -C core/build-cpu && meson test -C core/build-cpu --suite=fast → 101/101 pass (incl. golden-adjacent CPU tests, all unchanged); the new test_perceptual_weight → 7/7 pass (bit-exact pooling without side-data / enabled-but-absent / present-but-disabled; weighted mean matches the closed form with a synthetic blob; grid==0 degrade; foreign/-ENOENT + bad-ABI/-EPROTO). clang-format + clang-tidy clean on all touched files; git apply --stat ffmpeg-patches/0017-*.patch parses (41 insertions). Real per-cell maps await vf_pelorus_analyze (plan C, Pelorus side). | (2026-06-14) | | T-PELORUS-VENDOR-INTEROP-ABI-2026-06-14 | Pelorus (VMAFx/pelorus) and vmafx must agree byte-for-byte on a shared data-plane interop ABI (a versioned per-frame side-data blob Pelorus writes and vmafx reads), but vmafx had no in-tree copy and no guard against divergence. Foundation (plan workstreams A1+A2+A3): vendor the ABI into vmafx as a pinned, read-only, append-only mirror of VMAFx/pelorus@835e097. A1 — core/include/libvmaf/pelorus/{pelorus,interop,deband}.h + core/src/interop/pelorus_{interop,deband_params,version}.c (compiled into libvmaf; CPU-only, dependency-free, no Vulkan), each byte-identical to Pelorus except a VENDORED FROM … DO NOT EDIT banner + a pelorus/<x>.h→libvmaf/pelorus/<x>.h include rewrite. A2 — scripts/sync-pelorus-interop.sh pins PELORUS_VENDOR_SHA=835e097, reads the pinned commit's git tree object, fails on any drift; the _Static_assert size locks compile in every including TU. A3 — core/test/test_pelorus_interop.c, the shared 7-vector conformance fixture, wired into the fast suite, proving the vendored parser is byte-compatible with Pelorus's writer. Single source of truth stays in Pelorus (ADR-0103). Lint/format gates exclude the vendored files so the touched-file rule can't break byte-identity. | ADR-1113 | feat/pelorus-vendor-interop-abi | meson setup core/build-cpu core -Denable_cuda=false -Denable_sycl=false && ninja -C core/build-cpu && meson test -C core/build-cpu test_pelorus_interop → 7/7 pass; scripts/sync-pelorus-interop.sh /path/to/pelorus → OK (no drift vs pinned 835e097, ABI 1.0, minor=0); vendored library objects compile clean under -pedantic. Reader-side perceptual weighting + autotune control plane are separate later workstreams. | (2026-06-14) | | T-METRIC-BRISQUE-NR-2026-06-14 | The fork had no general-purpose, distortion-generic no-reference IQA metric (ssimulacra2/ciede are FR; cambi is NR banding-only; niqe is NR opinion-unaware). New feature: add brisque (BRISQUE, Mittal/Moorthy/Bovik IEEE TIP 2012), a CPU no-reference opinion-aware spatial IQA extractor that scores the distorted picture only (ref / _90 discarded, CAMBI/NIQE posture). Bundles the canonical LIVE-lab allmodel (libsvm EPSILON_SVR, model/other_models/brisque_live.model, embedded into the binary at build time via an xxd -i Meson custom_target — the same path libvmaf's JSON models take, so the ~2.1 MB byte array never enters the tree and stays under the 1 MB large-file gate; the model_path feature option overrides with an on-disk model) under a documented research-use attribution exception (NOTICE model/other_models/NOTICE-brisque + card model/brisque_live_card.md citing TIP 2012). First feature-extractor consumer of the vendored libsvm. Pipeline replicates the gregfreeman MATLAB pipeline that trained the model — GGD for the MSCN field (not the krshrimali C++ port's AGGD), Gaussian sigma=7/6 (not truncated 1.166), MATLAB antialiased-bicubic half-res — and range-scales with the reference inline computescore.cpp arrays (NOT the conflicting allrange file), no output clamp, plain svm_predict (== svm_predict_probability for EPSILON_SVR). New files core/src/feature/brisque.c + brisque_math.h + brisque_model.h (a tiny declaration header for the build-time-embedded src_brisque_live_model[] symbol — no committed byte array, no Python generator); registered in feature_extractor.cpp + core/src/meson.build (model embed custom_target + source line) + core/test/meson.build. Docs docs/metrics/brisque.md, research docs/research/1101-brisque-nr-metric.md. SDR-luma only — PQ/HLG HDR out of scope (warns + scores as SDR); the AGGD near-zero strict-sign sensitivity is a documented limitation. SIMD/GPU twins deferred. | ADR-1115 | feat/metric-brisque | core/test/test_brisque.c (10 tests, meson test -C core/build-cpu test_brisque → 10/10 pass): 8 brisque_math.h unit oracles (gamma anchors GGD(2)=1.5707963 / AGGD(1)=0.5 / AGGD(2)=2/π, Gaussian window, GGD/AGGD fits of [-2,-1,0,1,2,3] with zeros excluded, symmetric eta=0, flat-patch NaN guard, odd-dim bicubic coeffs, range-scale anchors); an end-to-end snapshot (frame 12 of dis_576x324_48f.yuv = 81.066729887948, testdata/scores_cpu_brisque.json); and a 577×325 odd-dimension regression. Correctness validated on a stable natural image (cameraman: C −13.70844 vs independent MATLAB-faithful oracle −13.70840, ~5e-5). clang-format + clang-tidy clean on new files. | (2026-06-14) | | T-METRIC-Y-FUNQUE-PLUS-ATOMS-2026-06-14 | The fork had no FUNQUE-family wavelet-domain metric. New feature: y_funque_plus (CPU-only, temporal) ships the three Y-FUNQUE+ wavelet-domain atom features — y_funque_plus_ms_ssim (MS-SSIM with covariance pooling), y_funque_plus_dlm (DLM detail-loss), y_funque_plus_mad (MAD-Ref temporal). Per-frame: 2x OpenCV INTER_CUBIC (Keys cubic a=-0.75, BORDER_REPLICATE) pre-downscale → crop to a multiple of 2^levels → 2-level Haar DWT (pywt 'periodization') → Nadenau Y-channel CSF weighting of detail subbands only → the three atoms. DLM num/den abs-asymmetry replicated exactly (num pools rest^3 without abs; den pools ref with abs — pyr_features.py:54/61). Double-precision, -ffp-contract=off (own static lib, mirrors ssimulacra2). Scope cut (maintainer): the fused ScaledSVR MOS score is deferred — upstream funque_plus ships no frozen regressor (see Deferred row T-Y-FUNQUE-PLUS-FUSED-SVR). License finding: funque_plus is MIT (Copyright (c) 2023 Abhinau Kumar), BSD-2-Clause-Patent-compatible; the C is a clean-room reimplementation from arXiv:2304.03412 / 2202.11241 cross-checked against the MIT reference (no source copied verbatim). New core/src/feature/y_funque_plus.c; registered in feature_extractor.cpp + core/src/meson.build + core/test/meson.build. Docs docs/metrics/y-funque-plus.md. | ADR-1114 | feat/metric-y-funque-plus | core/test/test_y_funque_plus.c (6 tests, meson test -C core/build-cpu test_y_funque_plus → 6/6 pass): identical-input analytic oracles (ms_ssim=0, dlm=1, mad=0 at 8x8 / odd 65x33 / crop-path 100x100), a 64x64 non-trivial Python-reference oracle (ms_ssim=0.0733072, dlm=0.9972564), a 2-frame MAD-Ref temporal oracle (mad=0.1199987), and a too-small-frame init() rejection — all at places=4 (5e-5). Oracle values independently re-derived against a pywt+OpenCV reference. clang-tidy + clang-format clean on all new files. | (2026-06-14) | | T-MCP-TINY-AI-FEATURE-COVERAGE-2026-06-14 | The MCP vmaf_score / vmaf_score_encoded tools reached 0 % of the fork's defining Tiny-AI / DNN scoring surface (RC Workstream-C gap #1, the single largest MCP capability gap) and exposed neither feature selection nor the CTC presets nor several score-completeness flags. Both shell out to the vmaf CLI but only forwarded model/backend/precision/subsample. Fix: add optional, backward-compatible parameters to both score tools in both servers (Go cmd/vmafx-mcp + Python mcp-server/vmaf-mcp), each mapping onto a vmaf CLI flag verified against core/tools/cli_parse.c: tiny-AI (tiny_model, tiny_device/--dnn-ep, tiny_threads, tiny_fp16, tiny_model_verify, tiny_codec, tiny_preset, tiny_crf, tiny_resize, no_reference NR mode), feature selection (feature repeatable, aom_ctc/nflx_ctc presets), and score completeness (threads, frame_cnt, frame_skip_ref/frame_skip_dist, no_prediction). Each flag is forwarded only when set, so omitting them reproduces the prior behaviour exactly. NR mode makes ref optional on vmaf_score and is gated on tiny_model (mirrors cli_parse.c:997). Shared schema generator + argv builder keep the two tools and two servers in lock-step; the Go and Python schemas were diffed byte-for-byte and are identical for both tools. --csv/--sub deliberately excluded (handlers parse the JSON output). Stale tool-count comments (main.go "16", server.py "ten") corrected to 15; the pre-existing precision doc default drift ("17"→"legacy") corrected in passing. | ADR-1117 | feat/mcp-tiny-ai-feature-coverage | go build ./cmd/vmafx-mcp/... && go vet ./... && go test ./cmd/vmafx-mcp/... clean (new score_extras_test.go: schema-presence, enum, arg-mapping, NR-gate, zero-vs-unset). python -m py_compile server.py OK; PYTHONPATH=src pytest tests/test_score_extras_adr1117.py 8/8 pass + 138 existing MCP tests green; ruff/gofmt clean. Byte-identity confirmed by dumping both servers' vmaf_score + vmaf_score_encoded input schemas and diffing (canonical JSON match). | (2026-06-14) | | T-METAL-STANDALONE-METRICS-SWEEP-2026-06-14 | The Metal backend lacked the 4 standalone-metric extractors that CUDA/SYCL ship: integer_ciede (CIEDE2000 ciede2000), integer_psnr_hvs (psnr_hvs), integer_cambi (banding cambi), ssimulacra2. This completes the standalone-metrics sweep (9 of 9 in that batch); together with the earlier ssim/vif/adm/motion kernels the Metal backend now ships 17 wired, registered, parity-tested extractors. One known Metal-twin gap remains: the SpEED family (speed_chroma / speed_temporal) has CUDA/SYCL/HIP twins but no Metal kernel yet. Fix: add the 4 kernels (core/src/feature/metal/{integer_ciede,integer_psnr_hvs,integer_cambi,ssimulacra2}.{metal,_metal.mm} + parity tests). ciede/psnr_hvs/ssimulacra2 are full-GPU ports of their CUDA twins; integer_cambi uses the Strategy-II hybrid (ADR-0205): GPU mask/decimate/filter kernels + the exact CPU residual via cambi_internal.h wrappers → bit-identical to vmaf_fex_cambi. All registered in feature_extractor.c + core/src/metal/meson.build + core/test/meson.build. | no ADR: completes the Metal parity sweep (cites CPU/CUDA refs + ADR-0205 for the cambi hybrid) | feat/metal-standalone-batch | test_metal_{integer_ciede,integer_psnr_hvs,integer_cambi,ssimulacra2}_parity (CPU vs Metal, places=4 / 1e-4 per ADR-0214) run on the macOS Apple-Silicon CI lane and skip cleanly without a Metal device. macOS CI is the validator (no local Apple HW). | (2026-06-14) | | T-METRIC-DELTA-E-ITP-HDR-COLOUR-2026-06-14 | The fork had no HDR/WCG colour-difference metric: ciede (ΔE2000) is SDR/BT.709-hardcoded and ssimulacra2 is a perceptual structural metric — neither is meaningful on PQ HDR content. New feature: add delta_e_itp (ΔE-ITP, ITU-R BT.2124-0), a CPU full-reference colour-difference extractor (provided feature key delta_e_itp) that converts ref/dist YUV to the scaled ICtCp ("ITP") colour space and reports the ×720 mean per-pixel Euclidean distance (≈1 per just-noticeable colour difference). RC scope: PQ (SMPTE ST-2084) transfer only — HLG / BT.1886 deferred because their constants are single-sourced in BT.2124-0 (transfer=hlg/bt1886 rejected with -EINVAL). Options: transfer (=pq), matrix (bt2020/bt709, default bt2020 NCL), range (limited/full, default limited). YUV400 rejected; double-precision per-pixel math with no out-of-gamut clamping (BT.2124 Annex 4). New files core/src/feature/delta_e_itp.c + delta_e_itp_math.h (PQ EOTF/EOTF⁻¹) mirror ciede.c + ssimulacra2_math.h; registered in feature_extractor.cpp + core/src/meson.build. Docs docs/metrics/delta_e_itp.md. SIMD/GPU twins, HLG/SDR transfers, and a deterministic PQ LUT are deferred follow-ups. | ADR-1110 | feat/metric-delta-e-itp | core/test/test_delta_e_itp.c (7 tests, meson test -C build-cpu test_delta_e_itp → 7/7 pass): asserts the BT.2124-0 Annex 4 normative full-precision ITP triple [0.355721, 0.134647, -0.161395] at places=4 (NOT the standard's pre-rounded pooled 2.363, per the verifier's fix), identity = exactly 0.0, a synthetic ΔE-ITP pair = 8.037360, the documented 2.363 rounded-triple cross-check (places=3), PQ-transfer round-trip, end-to-end registry+extract (identity 0 / distorted positive), and the PQ-only scope guard. Full fast suite 97/97 pass; clang-tidy + clang-format clean on all new files. | (2026-06-14) | | T-PU21-HDR-METRIC-MISSING-2026-06-14 | The fork had no perceptually-uniform HDR adapter: PSNR/SSIM/MS-SSIM/CIEDE2000/SSIMULACRA2/CAMBI/PSNR-HVS all operate on gamma/limited-range YUV and are perceptually invalid on absolute-luminance HDR (PQ/HLG). Fix: add a CPU pu21 extractor (core/src/feature/pu21.c + pu21_math.h + pu21_ssim.{c,h}) providing pu21_psnr and pu21_ssim. It decodes the PQ (ST.2084) luma code value to cd/m² (EOTF × 10000), clamps to [0.005, 10000], PU21-encodes (7-coefficient transfer, default banding_glare; all four variants via variant), then scores PSNR (peak=256, no SDR dB cap) and a self-contained single-scale Gaussian SSIM at L=256. PU-SSIM uses its own L=256 SSIM (pu21_ssim.c); the golden float_ssim/iqa_ssim (L=255, feeds Netflix assertions) is byte-untouched. RC ships PQ input only (transfer defaults pq, non-pq → -EINVAL; HLG/SDR deferred). Luma-only; all per-pixel math fp64. Registered in feature_extractor.cpp (the active C++ registry, NOT the dead .c) + meson + test. | ADR-1111 | feat/metric-pu21 | meson setup core/build-cpu -Denable_cuda=false -Denable_sycl=false && ninja -C core/build-cpu test/test_pu21 && core/build-cpu/test/test_pu21 → 5/5 pass (encoder places=6, PU-PSNR(100,99)=51.873338803 dB places=4, identical-plane finiteness, PQ EOTF peak=10000, PU-SSIM identical=1.0); passes under MALLOC_PERTURB_. End-to-end CLI --feature pu21 on a 320×240 10-bit PQ pair: pu21_psnr=31.679947 matches the Python fp64 oracle exactly. | (2026-06-14) | | T-METAL-INTEGER-ADM-MISSING-2026-06-14 | The Metal backend lacked the integer_adm extractor (feature adm — a VMAF DEFAULT; keys VMAF_integer_feature_adm2_score + adm_scale0..3): CUDA/SYCL/HIP ship integer_adm_* kernels and the CPU integer_adm.c (.name="adm") is the reference, but --backend metal --feature adm had no Metal kernel. Core-VMAF Metal full-parity sweep (5 of 9 real gaps — completes the core VMAF feature set ADM+VIF+motion on Metal). Fix: add integer_adm_metal (core/src/feature/metal/integer_adm.metal + integer_adm_metal.mm, ~2.3k lines) — fixed-point 4-scale DWT2 → CSF → decouple → contrast-mapping mirroring integer_adm.c byte-for-byte (shifts/rounding/fixed-point), on the proven float_adm_metal multi-scale lifecycle; provided_features[] mirrors integer_adm.c exactly (no aim/adm3 — those are float_adm only). Registered in feature_extractor.c + meson + test. | no ADR: implementation completion of the Metal backend parity sweep (cites CPU/CUDA integer_adm references) | feat/metal-integer-adm | test_metal_integer_adm_parity (CPU adm vs Metal integer_adm_metal, places=4 / 1e-4 per ADR-0214) runs on the macOS Apple-Silicon CI lane and skips cleanly without a Metal device. Hardest blind-generated kernel; macOS CI is the validator (no local Apple HW); fp64-free residuals follow the ADR-0220/ADR-0589 precedent if needed. | (2026-06-14) | | T-MCP-SCHEMA-BITDEPTH16-VULKAN-2026-06-14 | Two MCP schema-correctness bugs (RC audit). (1) vmaf_score + describe_worst_frames declared bitdepth enum [8,10,12] in both the Python (server.py:2155,2250) and Go (tools.go:44,136) MCP servers, but the CLI accepts 8/10/12/16 (core/tools/vmaf.c validates depth 8–16; cli_parse.c:365) — so 16-bit YUV scoring was unreachable via MCP. (2) docs/mcp/tools.md still listed the removed vulkan backend in 10 enum/example sites (Vulkan dropped in ADR-0726). Fix: add 16 to the bitdepth enum in both servers; strip vulkan from all MCP doc backend enums/examples (now auto/cpu/cuda/sycl/hip/metal). NOTE: a separate precision doc/code drift (doc says default "17", code defaults "legacy") is flagged for a follow-up decision (should MCP default to lossless?), not fixed here. | no ADR: schema bug fix | fix/mcp-schema-bitdepth-vulkan | go build ./cmd/vmafx-mcp/... OK; python -m py_compile server.py OK; bitdepth enum now [8,10,12,16] in both servers; grep -ci vulkan docs/mcp/tools.md → 0. | (2026-06-14) | | T-METAL-INTEGER-VIF-MISSING-2026-06-14 | The Metal backend lacked the integer_vif extractor (feature vif — a VMAF DEFAULT; keys VMAF_integer_feature_vif_scale0..3_score): CUDA/SYCL/HIP ship integer_vif_* kernels and the CPU integer_vif.c (.name="vif") is the reference, but --backend metal --feature vif had no Metal kernel. Core-VMAF Metal full-parity sweep (4 of 9 real gaps). Fix: add integer_vif_metal (core/src/feature/metal/integer_vif.metal + integer_vif_metal.mm) — a 4-scale fixed-point Gaussian pyramid with int64 moment accumulators, the integer log2-LUT (regenerated from integer_vif.c::log_generate), and the exact CPU final num/den formula, mirroring the proven float_vif_metal scaffold. provided_features (15-entry list) + options (debug, vif_enhn_gain_limit, vif_skip_scale0) mirror integer_vif.c exactly. Registered in feature_extractor.c + meson + test. Note: Apple GPUs lack fp64, so the per-pixel gain is float (fp64-free trade-off, ADR-0220 precedent for SYCL). | no ADR: implementation completion of the Metal backend parity sweep (cites CPU/CUDA integer_vif references; ADR-0220 for the fp64-free trade-off) | feat/metal-integer-vif | test_metal_integer_vif_parity (CPU vif vs Metal integer_vif_metal, places=4 / 1e-4 per ADR-0214) runs on the macOS Apple-Silicon CI lane and skips cleanly without a Metal device. macOS CI is the validator (no local Apple HW); if the fp64-free gain causes a residual beyond places=4, the bound follows the ADR-0220/ADR-0589 precedent. | (2026-06-14) | | T-METAL-FLOAT-ADM-MISSING-2026-06-14 | The Metal backend lacked the float_adm extractor (features float_adm2, adm_scale0..3, aim_score, adm3_score): CUDA/SYCL ship float_adm_* kernels and the CPU float_adm.c is the reference, but --backend metal --feature float_adm had no Metal kernel. Core-VMAF Metal full-parity sweep (3 of 9 real gaps after integer_ssim + float_vif). Fix: add float_adm_metal (core/src/feature/metal/float_adm.metal + float_adm_metal.mm) — a 1:1 port of the CUDA float_adm/float_adm_score.cu 4-scale DWT2 → CSF → decouple → contrast-mapping pipeline (+ AIM second pass), using the float_ms_ssim_metal multi-scale lifecycle (per-scale band buffers allocated once). provided_features + options mirror the CPU float_adm.c byte-for-byte; adm_csf_mode=WATSON97 default supported (others -EINVAL like the CUDA twin). Registered in feature_extractor.c + core/src/metal/meson.build + core/test/meson.build. | no ADR: implementation completion of the Metal backend parity sweep (cites CPU/CUDA float_adm references) | feat/metal-float-adm | test_metal_float_adm_parity (CPU float_adm vs Metal float_adm_metal, places=4 / 1e-4 per ADR-0214) runs on the macOS Apple-Silicon CI lane and skips cleanly without a Metal device. Parity bound inherited from the CUDA twin (may loosen to 1e-3 / ADR-0589 if CI shows a float-rounding-order residual); macOS CI is the validator (no local Apple HW). | (2026-06-14) | | T-METAL-FLOAT-VIF-MISSING-2026-06-14 | The Metal backend lacked the float_vif extractor (feature float_vif + float_vif_scale0..3): CUDA/SYCL/HIP ship float_vif_* kernels and the CPU float_vif.c is the reference, but --backend metal --feature float_vif had no Metal kernel. Core-VMAF Metal full-parity sweep (2 of 9 real gaps after integer_ssim). Fix: add float_vif_metal (core/src/feature/metal/float_vif.metal + float_vif_metal.mm) — a 4-scale separable-Gaussian pyramid with per-scale mean/variance/covariance statistics and the VIF formula, ported from the CUDA float_vif/ + CPU float_vif.c references, using the float_ms_ssim_metal multi-scale lifecycle. provided_features + options mirror the CPU float_vif.c. Registered in feature_extractor.c + core/src/metal/meson.build + core/test/meson.build. | no ADR: implementation completion of the Metal backend parity sweep (cites CPU/CUDA float_vif references) | feat/metal-float-vif | test_metal_float_vif_parity (CPU float_vif vs Metal float_vif_metal, places=4 / 1e-4 per ADR-0214) runs on the macOS Apple-Silicon CI lane and skips cleanly without a Metal device. Parity bound inherited from the CUDA twin; macOS CI is the validator (no local Apple HW). | (2026-06-14) | | T-METAL-INTEGER-SSIM-MISSING-2026-06-14 | The Metal backend lacked the integer_ssim extractor (feature ssim): CUDA, HIP, and SYCL all ship real integer_ssim_* kernels (ADR-0564), and the CPU vmaf_fex_ssim is the reference, but --backend metal --feature ssim had no Metal kernel. Part of the Metal full-parity RC sweep. Fix: add integer_ssim_metal (core/src/feature/metal/integer_ssim.metal + integer_ssim_metal.mm), mirroring the proven float_ssim_metal two-pass separable-Gaussian Metal scaffold and swapping float arithmetic for the fixed-point math of the CPU reference integer_ssim.c; registered in feature_extractor.c + core/src/metal/meson.build + core/test/meson.build, provides "ssim", .name = "integer_ssim_metal". Note (scope correction): the Metal full-parity gap is 9 real kernels, not 11 — integer_moment and integer_ms_ssim are NOT distinct extractors (moment/ms_ssim are float-only; integer_moment exists on no backend, integer_ms_ssim is a HIP-only oddity with no CPU canonical), so they are not Metal gaps. | ADR-0564 (Metal cross-backend completion) | feat/metal-integer-ssim | test_metal_integer_ssim_parity (CPU ssim vs Metal integer_ssim_metal, places=4 / 1e-4) runs on the macOS Apple-Silicon CI lane and skips cleanly without a Metal device. MSL kernel mirrors the CI-passing float_ssim.metal structure; no local Apple HW (macOS CI is the validator). | (2026-06-14) | | T-GPU-MOTION-V2-MOTION3-TWINS-2026-06-14 | Cross-backend follow-up to ADR-1108: the SYCL, HIP, and Metal motion_v2 twins did not emit VMAF_integer_feature_motion3_v2_score (only the CUDA twin did, after #909). Each twin's flush_fex_<backend> stopped after motion2_v2 and its option table carried only motion_fps_weight, so the four motion3-driving options (motion_blend_factor/motion_blend_offset/motion_max_val/motion_moving_average) were unavailable on three of the four GPU backends — the exact gap ADR-1108 named as a follow-up. Fix: mirror the merged CUDA flush_fex_cuda motion3_v2 post-process byte-for-byte onto all three twins (same four options mirroring the CPU VmafOption table, same per-frame motion_blend→MIN(motion_max_val) clip→stamp_value seed for i<min_idx=1→optional 2-tap moving average via the shared motion_blend_tools.h helper, motion3_v2 added to provided_features[], motion2 emitted via append_with_dict). Host-side only — no GPU kernel change. Note: all GPU twins store raw SAD and apply motion_fps_weight in flush (the CPU bakes it into the stored SAD); identical at the default motion_fps_weight=1.0, with a pre-existing cross-backend-consistent divergence under non-default weights (documented in docs/metrics/motion.md). | ADR-1108 (cross-backend completion) | feat/gpu-motion3-v2-twins | HIP built locally (ROCm gfx1036, -Denable_hip=true -Denable_hipcc=true): wrapper + test_hip_motion_v2_parity compile + link clean. SYCL/Metal compile in CI (icpx / macOS lanes); all three test_<backend>_motion_v2_parity assert sad/motion2_v2/motion3_v2 at places=4 (PARITY_TOL=1e-4) and skip cleanly without the device. motion3_v2 flush mirrors the merged CUDA twin (#909) byte-for-byte, which is bit-exact (max_abs_diff=0.0) vs CPU at default options. | (2026-06-14) | | T-CUDA-MOTION-V2-MOTION3-MISSING-2026-06-13 | The CUDA motion_v2_cuda twin (core/src/feature/cuda/integer_motion_v2_cuda.c) did not emit VMAF_integer_feature_motion3_v2_score, while the CPU reference integer_motion_v2.c::flush() does. Its provided_features[] listed only motion_v2_sad_score + motion2_v2_score, its flush_fex_cuda() stopped after motion2, and its option table carried only motion_fps_weight — so the four motion3-driving options (motion_blend_factor, motion_blend_offset, motion_max_val, motion_moving_average) were unavailable on the CUDA path. Any motion_v2_cuda consumer requesting motion3_v2 (a CHUG re-extract, a model file carrying motion_v2=motion_blend_factor=…, a co-scheduled CPU+CUDA parity run) silently got no feature on CUDA. ADR-0337 deliberately deferred the GPU twins ("whether GPU twins gain the same options will be decided when each twin needs to emit motion3_v2_score"); this closes that deferral for CUDA. Fix: add the four options (mirroring the CPU VmafOption table byte-for-byte), extend flush_fex_cuda() to compute + append motion3_v2 with the identical per-frame blend/clip/stamp-seed/moving-average formula via the shared motion_blend_tools.h helper, add motion3_v2 to provided_features[], and switch motion2 emission to append_with_dict so co-schedule naming matches CPU. SYCL/HIP/Metal twins carry the same gap (follow-ups). | ADR-1108 supersedes the ADR-0337 GPU-twin deferral for CUDA | fix/cuda-motion-v2-motion3-emission | RTX 4090, vmaf-dev-mcp container: motion_v2 (CPU) vs motion_v2_cuda on Netflix src01_hrc00↔src01_hrc01 576×324, 48 frames → motion3_v2 max abs per-frame diff = 0.000e+00 (CPU mean 3.9897658542 == CUDA mean) at default options and at motion_blend_factor=0.5 + motion_moving_average=1 (places=4, ADR-0214); test_cuda_motion_v2_parity (extended to assert motion3_v2 finite + parity) → 2/2 pass. | (2026-06-13) | | T-VMAFX-SCORESTREAM-PHASE2-2026-06-13 | Two Phase-4b distributed-platform surfaces that were loud-fail stubs are now implemented. (1) cmd/vmafx-server/grpc_server.go::ScoreStream returned codes.Unimplemented; it now performs real per-frame VMAF scoring of in-memory raw frames via a new stateful in-process scorer pkg/libvmaf.StreamScorer (stream.go) that mirrors libvmaf's vmaf_picture_alloc + vmaf_read_pictures + vmaf_score_at_index + vmaf_score_pooled sequence on caller-supplied []byte planes. The bidirectional contract from proto/vmafx.proto is honoured exactly: one StreamConfig, then FramePair messages, then EOF; the server flushes (so temporal motion features finalise), harvests per-frame scores, and streams back N FrameScore messages plus one terminal AggregateScore. Context cancellation, frame-size/ordering validation, model resolution, and the existing ScoreLimiter concurrency cap are all wired. The streaming pooled VMAF is bit-identical to ScoreDirect on the 48-frame golden pair (94.323009). (2) cmd/vmafx-node/server/server.go::Serve previously bound a port but registered NO gRPC services; it now registers the VmafxScoring service (Score + ScoreStream + Health) backed by the shared pkg/libvmaf engine, with GracefulStop + hard-stop-fallback shutdown on SIGTERM, and main.go constructs the scorer (Health-only when unavailable). ADR-0933 promoted Proposed→Accepted (matches its Phase-2 design); ADR-1109 records the node-serve decision. New Scorer.ResolveModel exported wrapper. | ADR-0933 / ADR-1109 | feat/vmafx-scorestream-phase2 | CGO_ENABLED=1 go test ./... -count=1 all packages OK; go vet ./... + gofmt -l clean. New tests: TestStreamScorer_* (engine, incl. ScoreDirect cross-check), TestGRPCScoreStream_EndToEnd / _FrameSizeMismatch / _BadModelRejected (server), TestNodeServe_HealthReachable / _ScoreWithoutScorer / _ScoreStreamEndToEnd (node). | (2026-06-13) | | T-JSON-MODEL-FEATURE-NAME-DUP-KEY-LEAK-2026-06-13 | The JSON model parser's append_feature_name (core/src/read_json_model.c and its C++23 twin read_json_model.cpp) strdup'd a feature name into feature[index].name without freeing any value already there. A duplicate feature_names key re-runs parse_feature_names from index 0, so the second array's strdup orphaned the first name. vmaf_model_destroy walks only [0, min(feature_cap, n_features)) and frees the current slot occupants, so the orphan was unreachable — leaked on both the validation-error path (where vmaf_read_json_model nulls *model and the caller must not destroy, per the fuzz-harness contract) and the success path. Found by the nightly fuzz_json_model LeakSanitizer lane (Direct leak of 24 byte(s)). Fix: free the prior name before the overwrite in both parser variants. Regression test test_json_model_feature_names_duplicate_key_no_leak added to core/test/test_model.c. | no ADR: memory-leak bug fix (CLAUDE §12 r8 exempt) | fix/json-model-feature-name-leak | meson setup /tmp/b core -Denable_cuda=false -Db_sanitize=address && meson test -C /tmp/b test_model — passes (60/60); pre-fix the new test trips LeakSanitizer (14-byte direct leak, exit 1). | (2026-06-13) | | T-FLOAT-VIF-AVX512-GOLDEN-REGRESSION-2026-06-13 | Netflix golden VMAFEXEC score regression on AVX-512 CPUs: vmaf_float_v0.6.1.json produced mean score 76.66729 on the Netflix src01 pair, but the protected golden assertion (python/test/vmafexec_test.py line 156) expects 76.66740433333332 at places=4 (threshold 5e-5). Absolute deviation ~1.1e-4 > 5e-5. Root cause: ADR-0504 dispatched float VIF convolution (vif_filter1d_s/sq_s/xy_s) to AVX-512 on capable CPUs; the wider 512-bit FMA partial-sum tree (16 floats vs AVX2's 8) produces different IEEE-754 rounding. Regression was latent since the day ADR-0504 landed because GitHub Actions runners lack AVX-512. Fix: remove #if HAVE_AVX512 dispatch blocks from all three float VIF wrappers in core/src/feature/vif_tools.c; float VIF now uses AVX2 (matching upstream Netflix/vmaf) or scalar. Verified: 271 passed / 12 skipped / 0 failed across vmafexec_test, vmafexec_feature_extractor_test, quality_runner_test, feature_extractor_test, result_test, ssimulacra2_test. | ADR-1104 | fix/golden-cpu-regression-restore | python3 -m pytest python/test/vmafexec_test.py -k test_run_vmafexec_runner_float_fex passes; mean score 76.66744, diff 3.6e-5 < 5e-5. | (2026-06-13) | | T-DOC-LEGACY-RUNNER-MISSING-DEPRECATION-2026-05-29 | VmafLegacyQualityRunner was deprecated in ADR-0749 / PR #87. The deprecation notice was missing from docs/development/deprecations.md despite a closed-without-merge PR #216. The entry was added in PR #852 (2026-06-08), which bundled the 8-worktree drain including ADR-0749 documentation. docs/development/deprecations.md now carries a full entry with migration guidance and cross-link to ADR-0749. | ADR-0749 | PR #852 (2026-06-08) | grep 'VmafLegacy\|ADR-0749' docs/development/deprecations.md — returns non-empty match. | (2026-06-08) | | T-FUNCTIONAL-MATRIX-17-BEHAVIORS-2026-06-12 | Full-matrix validation found 17 genuinely broken tool/backend behaviors. Items 1/2/16/17 (HIP VIF wavefront carry-drop) were already resolved by ADR-0563 (per-thread atomicAdd in vif_statistics.hip). Remaining 13 behaviors fixed: (3) bench_all.sh fatal unbound variable on $FLAGS_VULKAN (ADR-0726 follow-up — removed the vulkan run_test call); (4/7) _workdir_parent() in bisect.py now checks os.access(path, os.W_OK) and falls back to None/OS-tmp, fixing PermissionError when /probes is read-only; (5) _run_tune_per_shot in MCP server.py no longer passes --format to vmaf-tune tune-per-shot (flag does not exist, caused argparse exit 2); (6) score.py _CANONICAL_TO_POOLED_KEY already present; (8) _run_recommend_saliency in cli.py redirects encode to <stem>_encoded.mp4 when --output ends in .json, eliminating ffmpeg EINVAL; (9) _write_compare_profile_report already writes JSON sidecar for --format both; (10) Containerfile now installs vmaf-tune[fast] (Optuna); (11–13) DynamicQuantizeLinear, MatMulInteger, ConvInteger added to op_allowlist.c unblocking dynamic-PTQ int8 models; (14) explicit cuStreamSynchronize(s->lc.str) in collect_fex_cuda closes the D2H race that corrupted ADM scores on ~31% of frames; (15) dnn_api.c treats symbolic batch dims (ORT returns ≤0) as N=1 and reallocates scratch buffers on frame-size mismatch. | no ADR: correctness bug-fix bundle | fix/functional-matrix-broken-17 | pre-commit run --files <8 changed files> — all hooks pass; meson test -C build --suite=fast smoke-run. | (2026-06-12) | | T-INTEGER-SSIM-AVX2-16BIT-OVERFLOW-2026-06-12 | integer_ssim_accumulate_row_16_avx2() computed squared moments as _mm256_mul_epi32(wv, _mm256_mul_epi32(sv, sv)). For pixels >= 46341, s*s >= 2^31, setting bit 31 of the low 32-bit lane; _mm256_mul_epi32 treats the operand as signed, corrupting x2, xy, and y2 accumulators with wrong-sign 64-bit products. 8-bit and 10-bit paths were unaffected; the Netflix golden gate uses 8/10-bit content and never caught it. Fix: reorder to (w*s)*s; w*s <= 256×65535 = 16,776,960 < 2^24 — bit 31 always clear. Regression test test_integer_ssim_avx2_16bpc_bright added (alternating 65535/65534 pixels, bit-exact AVX2 vs scalar assertion). | no ADR: SIMD correctness fix | chore/bundle-fable-5-findings | meson test -C build-fable test_integer_ssim_simd — 5/5 sub-cases pass including new bright-pixel case. | (2026-06-12) | | T-BPC-VALIDATION-AND-OR-2026-06-12 | validate_pic_params() in core/src/libvmaf.c used && where all sibling guards (w, h, pix_fmt) use \|\|. On frame 0, pic_params.bpc is assigned from ref->bpc immediately before the guard, making the second term always false; the && short-circuited to false, silently accepting 8bpc/10bpc mismatched pairs. The dist buffer was then read at the wrong stride/element size, producing garbage output or an OOB access. Fix: change && to \|\|. Regression test test_validate_pic_params_bpc added (5 sub-cases). Inherited from upstream Netflix/vmaf. | no ADR: logic-operator bugfix | chore/bundle-fable-5-findings | meson test -C build-fable test_validate_pic_params_bpc — 5/5 pass. | (2026-06-12) | | T-VMAFX-SERVER-CONCURRENCY-CAP-2026-06-12 | Without a cap, N concurrent unauthenticated POST /v1/score or gRPC Score calls forked N vmaf subprocesses simultaneously, exhausting CPU/RAM/PIDs. Added ScoreLimiter (golang.org/x/sync/semaphore.Weighted) shared across both HTTP and gRPC entry points; excess callers receive HTTP 429 or gRPC codes.ResourceExhausted; cap defaults to runtime.NumCPU() and is configurable via --max-concurrent-scores. | no ADR: DoS-mitigation fix | chore/bundle-fable-5-findings | go test ./cmd/vmafx-server/... passes; concurrency tests cover semaphore acquisition, 429/ResourceExhausted responses, and FIFO unblocking. | (2026-06-12) | | T-CUDA-PREV-REF-UAF-DIST-TRANSLATE-2026-06-12 | Two bugs in core/src/libvmaf.c CUDA read-pictures path: (1) Phase 2 submit loop assigned prev_ref via a bare struct copy instead of vmaf_picture_ref(), making the picture pool able to reuse the buffer while the CUDA extractor still held it — latent UAF+leak once a VMAF_FEATURE_EXTRACTOR_PREV_REF CUDA extractor is added. Fix: vmaf_picture_ref in Phase 2; unref+zero on error and after collect. (2) read_pictures_cuda_translate wrapped the dist-side translate_picture result in (void), swallowing any error and potentially passing a partially-populated dist_device to CUDA extractors. Fix: capture and propagate the return value. | no ADR: latent-UAF + error-propagation fix | chore/bundle-fable-5-findings | meson test -C build-fable --suite=fast — 88/88 pass. | (2026-06-12) | | T-PORT-UPSTREAM-8461AE08-2026-06-12 | Port of upstream Netflix/vmaf commit 8461ae08 (2026-06-11): libvmaf/motion: fix feature name collision with concurrent sfr/hfr motion features. In integer_motion.c, the intermediate VMAF_integer_feature_motion_sad_score was appended with a hardcoded name via vmaf_feature_collector_append, bypassing the dict-based instance-suffix path used by motion2/motion3. When multiple motion extractors ran concurrently (sfr + hfr scenario), both wrote to the same unmangled feature name, silently overwriting each other's scores. Fix: switch extract() to vmaf_feature_collector_append_with_dict; in flush(), look up the dict-mapped sad_name instead of hardcoding it; add early-return dict-free on !n; add vmaf_dictionary_free at end of flush; add VMAF_integer_feature_motion_sad_score to provided_features[]. File path adapted from upstream's libvmaf/src/ to the fork's core/src/ (ADR-0700). | no ADR: upstream bugfix port (CLAUDE §12 r8 exempt) | chore/port-upstream-8461ae08 | meson test -C build --suite=fast — fast gate passes; no golden assertion touched. | (2026-06-12) | | T-LOCAL-EXPLAINER-BOOTSTRAP-NEON-RECAL-2026-06-08 | test_run_vmaf_runner_local_explainer_with_bootstrap_model in python/test/local_explainer_test.py (line 276) asserted VMAF_LE_score ≈ 75.40980306756497 at places=4 (tolerance 5e-5). After the NEON uint64-truncation fix in PR #834 / commit 43cf4c9aa, macOS arm64 Apple libm produces 75.40974269371469 — a ~6.0e-5 delta that exceeds the places=4 tolerance but passes places=3. All other bootstrap-score assertions in the same file already carry # ADR-0418 macOS-libm Δ relax comments and use places=3; this assertion was added without the relaxation. Fix: recalibrate to the post-NEON-fix value 75.40974269371469 and relax both assertions to places=3 per the ADR-0418 pattern. | ADR-0418 | fix/master-855-tip-3-reds | macOS arm64 CI test passes at places=3; Linux places=3 also passes (delta ~6e-5 < 5e-4). | (2026-06-08) | | T-DOCKERFILE-LDCONFIG-MISSING-2026-06-08 | Dockerfile was missing RUN ldconfig after the make clean && make ENABLE_NVCC=true && make install step. The NVIDIA CUDA Ubuntu 24.04 base image does not include /usr/local/lib/x86_64-linux-gnu in its /etc/ld.so.conf dynamic-linker search path (only /usr/local/lib is listed). Meson strips RPATH on install, so the installed /usr/local/bin/vmaf binary could not find libvmaf.so.3.0.0 at runtime, exiting immediately with a dynamic-linker error printed to stderr. The Docker smoke test swallowed stderr with 2>/dev/null, producing zero stdout and failing the pixel-level score assertion promoted to blocking in PR #852. Fix: add RUN ldconfig immediately after the libvmaf make install step. | no ADR: Docker correctness fix | fix/master-855-tip-3-reds | docker build -t vmaf . && docker run --rm --entrypoint /usr/local/bin/vmaf vmaf --version exits 0 and prints version; smoke-test score assertion passes. | (2026-06-08) | | T-CPP23-READ-JSON-MODEL-PENDING-2026-05-29 | core/src/read_json_model.c conversion to C++23 was tracked as pending a fresh PR after PR #215 was closed without merging on 2026-05-30. The conversion replaced goto fail: teardown with an RAII ModelParseGuard, malloc/free with std::make_unique<char[]>, and strdup/free with std::string. This work landed in PR #531 (2026-06-02) as part of ADR-0846 Wave 8. The row in Open was stale. | ADR-0846 | PR #531 (2026-06-02) | file core/src/read_json_model.cpp reports C++ source on master. | (2026-06-08 — stale row swept) | | T-DOCKER-SMOKE | Docker image CI job (docker-image.yml) was advisory (continue-on-error: true) since ADR-0623; it only ran docker run --rm vmaf /usr/local/bin/vmaf --version. After 3 consecutive green master runs the job was promoted to blocking: continue-on-error removed, a vmaf --backend cpu score-assertion smoke step added (576x324 fixture pair from testdata/, expected mean VMAF ≈ 94.32 ± 0.5, model vmaf_v0.6.1.json), and the timeout raised to 45 min. | ADR-0623 | chore/promote-docker-smoke-blocking | docker build -t vmaf . && docker run --rm --entrypoint /usr/local/bin/vmaf -v ./testdata:/testdata:ro -v ./model:/model:ro vmaf --reference /testdata/ref_576x324_48f.yuv --distorted /testdata/dis_576x324_48f.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --model path=/model/vmaf_v0.6.1.json --backend cpu --output /dev/stdout --json 2>/dev/null — expect mean VMAF ~94.32. | (2026-06-08) | | T-SYCL-ARC-FLOAT-SSIM-PARITY-2026-06-03 | float_ssim SYCL parity gate failed on Intel Arc A380 (DG2-G10) with max_abs_diff=2.68e-4 (tolerance 5e-5). Two causes: (1) CPU and GPU backends use different SSIM formulas (CPU: L×C×S decomposition with sqrt(var_refvar_cmp); GPU: Wang 2004 Eq.(13) combined form — intentional design). (2) Arc A380 lacks native fp64, causing fp32 accumulation drift to ~2.7e-4. Fix: added arc:dg2-g10 calibration entry to scripts/ci/gpu_ulp_calibration.yaml with float_ssim: 5.0e-4 (places=3); added dedicated sycl-float-ssim-parity job in tests-and-quality-gates.yml and promoted it to the required-status list in required-aggregator.yml. | Research-0985 §3 / ADR-0234 | fix/sycl-arc-float-ssim-calibration | python3 -m pytest scripts/ci/test_calibration.py -v — shipped table parses and arc:dg2-g10 entry resolves with float_ssim=5e-4. | (2026-06-08) | | T-CONTAINERFILE-GID-1000-CONFLICT-2026-06-08 | Ubuntu 26.04 (Resolute Raccoon) ships a built-in ubuntu group at GID 1000 in the base image. dev/Containerfile Stage 1 ran groupadd --gid 1000 vmaf, which exits 4 ("GID in use"), blocking every container rebuild since the base image was pinned to Ubuntu 26.04 in ADR-0603. The matching useradd --uid 1000 --gid 1000 and both BuildKit ccache-mount uid=1000,gid=1000 directives were also incorrect. Fix: change all four references to GID/UID 2000, which is in the local/static allocation range and is unused on all Ubuntu LTS releases. | ADR-1101 | fix/containerfile-gid-and-stale-rename | docker compose -f dev/docker-compose.yml build dev-mcp exits 0; groupadd no longer exits 4. | (2026-06-08) | | T-HIP-WAVEFRONT-WAVE32-2026-06-08 | Five HIP kernel files (integer_vif/vif_statistics.hip, float_vif/float_vif_score.hip, float_motion/float_motion_score.hip, float_psnr/float_psnr_score.hip, float_moment/moment_score.hip) hardcoded the AMD wavefront size as 64 at compile time. RDNA2+ devices (gfx1030/gfx1100/gfx1101) default to wave32 (warpSize=32); the hardcoded 64 caused the reduction loops to execute only 32 of the 64 required XOR-shuffle steps, halving each accumulator field. VIF accumulators under-reduce by ~50%, collapsing VIF scores toward zero and producing ~25-pt VMAF error. Fix: replace compile-time constants with the HIP device variable warpSize for loop bounds and lane/warp_id detection; resize shared-memory arrays for the minimum warp size (32) so they are large enough on wave32 hardware. | no ADR: correctness fix | fix/matrix-5-real-bugs | Compile-verified; no AMD wave32 device locally, correctness rationale verified by code review. | (2026-06-08) | | T-MCP-PROBE-FRAME-TOO-SMALL-2026-06-08 | mcp-server/vmaf-mcp/server.py used a 32×32 probe YUV for the probe_backend health check. The CUDA ADM extractor requires at least 36px in each dimension; the 32px frame caused the ADM kernel to return a null score. The old health-check code set runtime_healthy: True unconditionally (exit code 0, output parsed), so a null ADM score was silently reported as a healthy backend. Fix: bumped probe resolution to 64×64 and tightened the health check to score is not None. Follow-up (Go parity, 2026-06-15, branch fix/mcp-probe-parity): the earlier fix only touched the Python server; the Go server (cmd/vmafx-mcp/impl.go) still used a 32×32 probeYUVData and reported runtime_healthy=true unconditionally — the identical false-healthy bug on the Go transport. Bumped the Go probe to 64×64 and mirrored the predicate (null/non-finite vmaf.mean → runtime_healthy=false with "vmaf returned exit 0 but score was null"); refreshed stale "32x32" strings in impl.go/tools.go/server.py/docs/mcp/tools.md and documented the ≥36px CUDA-ADM minimum. New Go tests TestProbeYUVDimensions + TestScoreIsHealthy pin the parity. | no ADR: correctness fix | fix/matrix-5-real-bugs + fix/mcp-probe-parity | pytest mcp-server/vmaf-mcp/tests/ -k probe — 34/34 pass; probe tests verify the null-score unhealthy path. Go: go test ./cmd/vmafx-mcp/... pass (probe parity tests green). | (2026-06-08; Go parity 2026-06-15) | | T-LIBVMAF-CUDA-ONLY-NULL-DEREF-2026-06-08 | In vmaf_read_pictures (core/src/libvmaf.c), the #ifdef HAVE_CUDA block unconditionally reassigned ref = &ref_host; dist = &dist_host after the extractor loop. When every registered extractor carries VMAF_FEATURE_EXTRACTOR_CUDA (device-only, HW_FLAG_HOST not set), translate_picture_host() early-returns without populating ref_host/dist_host, leaving them zero-initialised. The subsequent vmaf_read_pictures_post_extractor call passed the zeroed pointer to vmaf_picture_ref → vmaf_ref_fetch_increment(NULL), a NULL-deref. Fix: guard the reassignment with if (hw_flags & HW_FLAG_HOST). | no ADR: NULL-deref fix | fix/matrix-5-real-bugs | meson test -C build-verify --suite=fast 84/84 pass including test_pic_preallocation. | (2026-06-08) | | T-FFMPEG-SYCL-SOFTWARE-FRAMES-2026-06-08 | ffmpeg-patches/0005 registered libvmaf_sycl with FILTER_SINGLE_PIXFMT(AV_PIX_FMT_QSV), making the filter reject all software-decoded inputs. Users without a QSV decoder could not run SYCL-accelerated VMAF via FFmpeg. Fix: change to FILTER_PIXFMTS(AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV420P10LE, AV_PIX_FMT_QSV); add a software path in do_vmaf_sycl that allocates a VmafPicture and copies frame data via vmaf_read_pictures; make config_props_sycl conditional on hw_frames_ctx presence. | no ADR: user-visible fix in ffmpeg integration patch | fix/matrix-5-real-bugs | Patch format valid; apply-check verified against series context. | (2026-06-08) | | T-CONTAINER-MISSING-NETFLIX-YUVS-2026-06-08 | dev/Containerfile did not call scripts/test/fetch-test-yuvs.sh, so pytest python/test/ always failed inside the container with fixture-not-found errors for the canonical Netflix src01 YUV pair. The comment in the Containerfile acknowledged this as a known gap ("fetched separately..."). Fix: add a RUN bash .../fetch-test-yuvs.sh layer at image-build time to fetch and md5-verify the fixtures per ADR-0493; build failure on download error surfaces network-policy regressions at image build time. | no ADR: container gap fix | fix/matrix-5-real-bugs | Containerfile change inspected; fetch script reviewed for idempotency and md5 verification. | (2026-06-08) | | T-PIC-POOL-ODR-CUDA-BUF-TYPE-2026-06-08 | picture_pool_cpp23_lib was compiled without -DHAVE_CUDA, creating a VmafPicturePrivate ODR violation: the 40-byte CUDA sub-struct (CUcontext ctx; CUevent ready, finished; CUstream str; VmafCudaState *state) was present in consumer TUs (compiled with -DHAVE_CUDA) but absent from the pool-allocator TU, shifting buf_type from byte offset 16 (no-CUDA layout) to offset 56 (CUDA layout). validate_pic_params in libvmaf.c read garbage at offset 56, returned -EINVAL on every vmaf_read_pictures call, and produced "problem during vmaf_read_pictures" / test_picture_pool_basic: fail. Fix: add cpp_args : (is_cuda_enabled ? ['-DHAVE_CUDA'] : []) + (is_sycl_enabled ? ['-DHAVE_SYCL'] : []) to the picture_pool_cpp23_lib static_library() call in core/src/meson.build. | no ADR: ODR bug fix | fix/pic-pool-odr-cuda-gpumask-cov-floor | meson test -C build-cuda --suite=fast test_picture_pool_basic passes; buf_type read at correct offset. | (2026-06-08) | | T-CUDA-GPUMASK-TIMEOUT-2026-06-08 | test_vmaf_cuda_gpumask.sh used ldconfig -p | grep -q libcuda to detect CUDA, which passes on CI runners with the CUDA 13.2.0 toolkit stub installed but no real GPU hardware. On such runners the test hung in vmaf_close/flush_context/vmaf_thread_pool_wait after a partial CUDA context init failure, consuming the full 30 s meson default timeout before being killed by SIGTERM. Fix: add an nvidia-smi -L GPU hardware guard (exit 77 = meson SKIP) after the ldconfig check; also add timeout : 10 to the meson test registration so any future hang costs at most 10 s. | no ADR: CI timeout fix | fix/pic-pool-odr-cuda-gpumask-cov-floor | On a CPU-only CI runner nvidia-smi -L fails, test exits 77 (SKIP), no 30 s hang. | (2026-06-08) | | T-COVERAGE-ORT-FLOOR-OVERSHOOT-2026-06-08 | ADR-0922 ratcheted the ort_backend.c per-file coverage floor from 78% to 83%, but the actual achievable coverage ceiling was ~79%: the remaining uncovered lines are ORT-operation-failure error paths (hit only when OrtApi calls return a non-NULL OrtStatus), which are structurally unreachable via normal test execution without error injection. The floor was temporarily reset to 79 (PR #840) with a note that a dedicated ORT error-injection test would allow restoring it. PR #844 added 8 ORT error-injection tests, raising measured coverage to 84%. Floor ratcheted back to 83 in scripts/ci/coverage-check.sh (chore/ort-coverage-ratchet-back-to-83). | ADR-0114 / ADR-0922 | fix/pic-pool-odr-cuda-gpumask-cov-floor → chore/ort-coverage-ratchet-back-to-83 | scripts/ci/coverage-check.sh floor restored to 83% for ort_backend.c; measured coverage 84%; gate passes with 1 pp slack. | (2026-06-08) | | T-VIFKS360-PYTEST-TIMEOUT-2026-06-08 | test_run_vmaf_runner_float_vifks360o97 uses a 65-tap Gaussian kernel (VIF scale=0 at kernelscale=360/97) falling through to the O(fwidth²·w·h) scalar C loop. In the debug+gcov coverage build this takes ~138 s, exceeding the 60 s --timeout for the pytest coverage step. --timeout-method=thread interrupted the Python test thread but could not abort the blocked subprocess.stdout.read() waiting on the hung vmafexec child; the 20-minute outer timeout then killed the entire pytest session, cutting short DNN/ORT coverage-contributing tests. Fix: raise --timeout from 60 s to 180 s in the coverage step of .github/workflows/tests-and-quality-gates.yml. | no ADR: CI budget fix | fix/pic-pool-odr-cuda-gpumask-cov-floor | test_run_vmaf_runner_float_vifks360o97 completes within budget; DNN/ORT tests contribute coverage. | (2026-06-08) | | T-CUDA-MOTION-SAD-BATCH-PENDING-2026-05-29 | integer_motion_cuda.c performed one cuStreamSynchronize per frame, incurring a ~12.7 ms/frame driver round-trip at 576p (Research-0760). PR #217 replaced the per-frame sync with an 8-frame batch fence (MOTION_BATCH_DEPTH=8), reducing synchronisation overhead 8-fold and raising 576p throughput from ~79 fps to ~800 fps. ADR-0845 correctness gate (ADR-0214 places=4 cross-backend parity) passed. | ADR-0845 | PR #217 | vmaf --feature motion --backend cuda vs CPU on Netflix 576x324 fixture: ADR-0214 places=4 parity passes; fps ratio >2x. | (2026-06-03) |
| T-CUDA-DONE-PATH-DOUBLE-UNREF-2026-06-07 | PR #838 added a read_pictures_cuda_cleanup() call in the done=true early-return branch of vmaf_read_pictures to avoid pool-slot exhaustion. When threaded mode is active, threaded_read_pictures_batch (line 1858) already calls vmaf_picture_unref(ref_host) and vmaf_picture_unref(dist_host). The done=true branch then called the full cleanup, releasing the same host pictures a second time and corrupting the pool free-list. Subsequent vmaf_picture_pool_fetch calls deadlocked in pthread_cond_wait once the pool drained. Fix: split read_pictures_cuda_cleanup into read_pictures_cuda_cleanup_device_only (device pictures only) and the full variant (host + device); the done=true path now calls _device_only which does not touch the already-released host pictures. | no ADR: regression fix for PR #838 | fix/cuda-done-path-double-unref-ort-coverage | meson test -C build-cpu --suite=fast test_pic_preallocation — 8/8 pass; 84/84 fast-suite pass. | (2026-06-07) | | T-COVERAGE-ORT-DEAD-ELSE-2026-06-07 | ort_log_and_release_status() added by commit 674abf299 contained a dead else branch ("(no ORT error message)"). ORT guarantees a non-empty message string whenever st != NULL; the else is never reached in practice. The untaken branch suppressed coverage below the 83% per-file security floor set by ADR-0922. Fix: collapse to a single vmaf_log call with an inline ternary (msg && msg[0] != '\0') ? msg : "(no ORT error message)", eliminating the dead branch while preserving identical runtime behaviour for the non-null-message case. | ADR-0922 / ADR-0114 | fix/cuda-done-path-double-unref-ort-coverage | CPU build compiles cleanly; dead branch absent from compiled output. Coverage gate expected green at ≥83%. | (2026-06-07) |
| T-PIC-PREALLOC-RECURRING-FAILURE-2026-06-07 | test_pic_preallocation::test_picture_pool_basic failed in HAVE_CUDA builds with "problem during vmaf_read_pictures". Root cause (4th layer): in vmaf_read_pictures, the done=true early-return path at line 2584 in core/src/libvmaf.c skipped the cleanup: label where read_pictures_cuda_cleanup unrefs the translated ref_host/dist_host/ref_device/dist_device pictures. Each early-return leaked one pool slot per host picture; after pool_size frames the pool exhausted and the next vmaf_picture_pool_fetch deadlocked in pthread_cond_wait. Fix: add an explicit #ifdef HAVE_CUDA read_pictures_cuda_cleanup(...) #endif call in the done=true branch before returning. | no ADR: bug fix | fix/ci-multi-platform-bundle-838 | meson test -C build --suite=fast test_pic_preallocation passes; pool not exhausted in CUDA+SYCL builds. | (2026-06-07) | | T-MSVC-STRSEP-CONST-STRING-LITERAL-2026-06-07 | core/tools/cli_parse.cpp fallback strsep declared sep as char * (non-const). MSVC /Zc:strictStrings turns the implicit const char[2] → char * conversion (from string literals ":", "=", "." at 9 call sites) into hard error C2664, aborting the Windows CUDA and SYCL builds before the link step. GCC/Clang only emit a warning. Fix: change the parameter to const char *sep — no behaviour change; strcspn already accepts const char*. | no ADR: build bug fix | fix/ci-multi-platform-bundle-838 | Windows MSVC build of core/tools/cli_parse.cpp compiles without C2664. | (2026-06-07) | | T-MACOS-ARM64-MOTION-BLEND-PLACES6-2026-06-07 | test_run_integer_motion_fextractor_with_blend used places=6 (5e-7 tolerance) for five per-frame integer SAD scalar assertions. ARM64 integer SAD arithmetic differs by ~2–6e-6 from x86_64 reference values, causing CI failures on all three macOS jobs (CPU, CPU+DNN, Metal). PR #837 lowered other motion tests from places=8 to places=4 but missed these five assertions at lines 591, 594, 597, 600, 603. Fix: lower all five to places=4 (5e-5 tolerance); aggregate score assertions remain at places=4 and are unaffected. | no ADR: test precision fix | fix/ci-multi-platform-bundle-838 | All three macOS CI jobs pass test_run_integer_motion_fextractor_with_blend. | (2026-06-07) | | T-UBSAN-ENUM-INVALID-VALUE-OPT-CPP-2026-06-07 | core/src/opt.cpp switch (static_cast<int>(opt->type)) still triggered UBSan enum-invalid-value SIGABRT (signal 6) in test_dispatch_unknown_type (passes value 9999). UBSan fires on the lvalue-to-rvalue conversion (the load of opt->type) before the cast executes; static_cast<int> cannot prevent a trap that already occurred. Fix: replace with memcpy(&type_raw, &opt->type, sizeof(type_raw)); switch (type_raw) — reads the raw bytes as a plain int, eliminating the typed enum load and making the switch UBSan-clean. | See ADR-1080 | fix/ci-multi-platform-bundle-838 | test_dispatch_unknown_type passes under UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1. | (2026-06-07) | | T-MSVC-SYCL-LNK2019-VMAF-FEX-EXTERN-C-2026-06-07 | core/src/feature/feature_extractor.cpp declared all extern VmafFeatureExtractor vmaf_fex_* without extern "C". Definitions live in C-compiled .c TUs (unmangled C symbols). MSVC strictly name-mangles C++ extern for POD global variables, causing LNK2001/LNK2019 for every vmaf_fex_* symbol on the Windows SYCL build (icx-cl compiles without a hard error but reaches the linker with mangled references). Fix: wrap all extern declarations in extern "C" { ... }, encompassing all conditional blocks (#if VMAF_FLOAT_FEATURES, #if HAVE_CUDA, #if HAVE_SYCL, #if HAVE_HIP, #if HAVE_METAL, #if HAVE_RUST_TAD). | no ADR: build bug fix | fix/ci-multi-platform-bundle-838 | Windows SYCL build links cleanly; all vmaf_fex_* symbols resolve. | (2026-06-07) |
| T-GO-CI-LIBVMAF-SO-RUNTIME-2026-06-07 | go test ./... failed for every Go package that links libvmaf.so.3 via cgo (vmafx-controller, vmafx-mcp, vmafx-node, vmafx-server, pkg/libvmaf) with "error while loading shared libraries: libvmaf.so.3: cannot open shared object file". The Build libvmaf (CPU only) step in go-ci.yml left the .so at core/build-cpu/src/ — not in a system-wide path — and the go test step had no LD_LIBRARY_PATH pointing there. Fix: add LD_LIBRARY_PATH: ${{ github.workspace }}/core/build-cpu/src to the go test step env block. | no ADR: CI environment fix | fix/go-rust-red-adr1041 | LD_LIBRARY_PATH=core/build-cpu/src go test ./... exits 0; all cgo-linked packages pass. | (2026-06-07) | | T-GO-CI-LASTHEARTBEAT-PRECISION-2026-06-07 | VmafxNode controller — sets Healthy = false when LastHeartbeat is stale (>60 s) failed with Expected: {Time: 2026-06-07T16:48:27Z} to equal {Time: 2026-06-07T16:48:27.427755089Z}. Root cause: staleTime := metav1.NewTime(time.Now().Add(-90 * time.Second)) captures sub-second precision; k8sClient.Status().Update() stores the value via the Kubernetes API server which serialises metav1.Time as RFC3339 (one-second granularity), truncating nanoseconds. The subsequent Get read back the truncated value, making Equal(&staleTime) fail. Fix: time.Now().Add(-90 * time.Second).Truncate(time.Second) so the in-memory value matches what the API server will return. | no ADR: test fix | fix/go-rust-red-adr1041 | go test ./cmd/vmafx-operator/internal/controller/... passes. | (2026-06-07) | | T-MCP-RESOURCE-URI-VALIDATION-REGRESSION-2026-06-07 | PR #791 ("kill child processes on client disconnect") inadvertently removed the libvmaf.ValidatePath calls that PR #813 had added to resolveModelArgToPath in cmd/vmafx-mcp/impl_direct.go. After PR #791, absolute paths supplied via model: "path=/arbitrary/path" or as bare absolute paths were accepted without allowlist checking, allowing any MCP client with VMAFX_MCP_DIRECT=1 to read arbitrary files on the host. The regression test TestResolveModelArgToPath_AllowlistEnforced (added by PR #813) caught the regression. Fix: restore libvmaf.ValidatePath() on all four return sites in resolveModelArgToPath (absolute stat, relative stat, bare-stem json, bare-stem pkl). | no ADR: security regression fix | fix/go-rust-red-adr1041 | go test ./cmd/vmafx-mcp/... -run TestResolveModelArgToPath passes; path outside allowlist is rejected. | (2026-06-07) | | T-RUST-CI-BINDGEN-DOCTEST-2026-06-07 | cargo test -p vmafx-sys --all-features collected doc-tests from bindgen-generated bindings.rs, which is included verbatim from libvmaf.h C doc-comments. The comments contain "On x86 / x86_64:" (parsed as x86 identifier token, not valid Rust) and backtick-quoted function names (invalid Rust token). Two doc-tests failed to compile. Fix: add [lib] doctest = false to bindings/rust/vmafx-sys/Cargo.toml — the standard idiom for *-sys crates with machine-generated FFI surfaces. Unit tests and integration tests are unaffected. | no ADR: CI maintenance fix | fix/go-rust-red-adr1041 | cargo test -p vmafx-sys --all-features exits 0; no doc-test failures. | (2026-06-07) |
| T-FFMPEG-INT-VULKAN-CI-2026-06-07 | The FFmpeg — Vulkan (Build Only, lavapipe) job in ffmpeg-integration.yml failed with ERROR: Unknown option: "enable_vulkan" on every push to master. Root cause: the ffmpeg-vulkan CI job was added to guard against a -DVK_NO_PROTOTYPES Cflag leakage regression (PR #234), but the Vulkan backend was subsequently dropped in full per ADR-0726, which removed the enable_vulkan meson option along with all backend source files. The job attempted to run meson setup core core/build -Denable_vulkan=enabled, which meson rejects as unknown. Fix: remove the ffmpeg-vulkan job entirely from ffmpeg-integration.yml and update the workflow name to drop the + Vulkan suffix. A tombstone comment explains the removal; patches 0004 + 0006 remain as no-op compatibility shims per ADR-0860/series.txt. | no ADR: CI maintenance fix (Vulkan backend removal per ADR-0726 was the decision; this is a missed CI-file cleanup) | fix/ffmpeg-vulkan-ci-job-removal | gh run view <run-id> --json jobs shows only 3 jobs (gcc / clang / SYCL), all green. | (2026-06-07) | | T-NEON-ANY-NONZERO-UINT64-TRUNC-2026-06-07 | neon_any_nonzero_s32 in core/src/feature/arm64/motion_v2_neon.c reinterpreted int32x4_t as uint64x2_t, then OR'd the two uint64 lanes and truncated to uint32_t. On little-endian ARM, uint64 lane 0 packs int32[0] in the low 32 bits and int32[1] in the upper 32. When int32[0]==0 and int32[1]!=0, the uint64 value is nonzero but its lower 32 bits are 0, so the (uint32_t) cast returns 0 — falsely reporting the row as all-zero. The zero-skip if (!(neon_any_nonzero_s32(nz_acc) | (uint32_t)nz_tail)) continue; then bypassed the x-phase convolution for alternating-zero/nonzero y-rows (e.g., checkerboard input), producing motion=0.0 on macOS arm64 CI runners. Fix: use vreinterpretq_u32_s32 and extract/OR at uint32 width using vget_low_u32, vget_high_u32, vorr_u32, vget_lane_u32. | no ADR: bug fix | fix/build-matrix-macos-windows-fixes | clang -target aarch64-linux-gnu -c core/src/feature/arm64/motion_v2_neon.c clean; macOS arm64 checkerboard motion score correct. | (2026-06-07) | | T-CPUMASK-NEG-ONE-REJECTED-2026-06-07 | compat/python-vmaf/__init__.py emitted --cpumask -1 to disable all ISA extensions. parse_unsigned() (ADR-1088, PR #794) now rejects strings beginning with -, returning EINVAL. All Python harness tests on macOS that call disable_avx() failed with Error: invalid cpumask: -1. Fix: update both callsites in __init__.py to emit --cpumask 4294967295 (0xFFFFFFFF, UINT_MAX, all bits set — same semantic, passes the non-negative check). Update test_disable_avx_emits_cpumask to assert 4294967295. | no ADR: compatibility fix for ADR-1088 | fix/build-matrix-macos-windows-fixes | python -c "from vmaf.core.feature_extractor import FeatureExtractor; print('ok')" loads without error; test passes. | (2026-06-07) | | T-PTHREAD-POOL-WINDOWS-MISSING-2026-06-07 | picture_pool_cpp23_lib and gpu_picture_pool_cpp23_lib static libraries in core/src/meson.build lacked dependencies: [pthread_dependency]. On Windows MSVC, pthread_dependency injects core/src/compat/win32/ into the include path for the win32 pthreads shim; without it, picture_pool.cpp and gpu_picture_pool.cpp both failed with fatal error C1083: Cannot open include file: 'pthread.h' on all Windows CUDA and SYCL legs. (The POSIX case is a no-op; POSIX builds were unaffected.) Fix: add dependencies : [pthread_dependency] to both static_library() calls. | no ADR: build bug fix | fix/build-matrix-macos-windows-fixes | meson setup build core -Denable_cuda=true -Denable_sycl=true configures cleanly on Windows MSVC; CUDA/SYCL legs link. | (2026-06-07) | | T-CI-VULKAN-STALE-MATRIX-ROWS-2026-06-07 | Two CI matrix rows in .github/workflows/libvmaf-build-matrix.yml (Build — Ubuntu Vulkan (T5-1b runtime) and Build — macOS Vulkan via MoltenVK (advisory)) passed -Denable_vulkan=enabled to meson setup. ADR-0726 removed the Vulkan backend; enable_vulkan is no longer a valid meson option. Both jobs failed immediately at Configure: meson setup: Unknown option: "enable_vulkan". Fix: remove both matrix rows; replace with comment # --- Vulkan backend removed (ADR-0726) ---. | no ADR: CI maintenance | fix/build-matrix-macos-windows-fixes | grep -n 'enable_vulkan' .github/workflows/libvmaf-build-matrix.yml returns empty. | (2026-06-07) | | T-ENV-PRESERVE-LOCALE-INJECT-2026-06-07 | test_run_preserves_user_env in python/test/python_harness_coverage_test.py expected ProcessRunner.run() to pass through exactly {"FOO": "bar"} when called with that env dict. ProcessRunner unconditionally stamps LC_ALL=C and LANG=C for deterministic subprocess error messages (intentional design). The test expectation was stale and failed on macOS where the locale env is set differently by default. Fix: update the expected dict to {"FOO": "bar", "LC_ALL": "C", "LANG": "C"} to match actual ProcessRunner semantics. | no ADR: test fix | fix/build-matrix-macos-windows-fixes | pytest python/test/python_harness_coverage_test.py::test_run_preserves_user_env -v passes. | (2026-06-07) | | T-BUILD-MATRIX-MESON-LIBVMAF-PATHS-2026-06-07 | All 20+ jobs in libvmaf-build-matrix.yml (Linux multi-config, MinGW64, Windows CUDA/SYCL) failed at the Configure step: meson setup libvmaf core/build — libvmaf/ source directory does not exist after the ADR-0700 rename to core/. Four locations were affected: Linux line 590, MinGW64 line 834, Windows CUDA lines 1059/1106, Windows SYCL lines 1099/1116. Fix: replace libvmaf source-dir argument with core in all four meson setup calls and both Windows ninja -C libvmaf\build references. | no ADR: CI maintenance fix | fix/state-sweep-fix (this PR) | grep -n 'meson setup libvmaf' .github/workflows/libvmaf-build-matrix.yml returns empty. | (2026-06-07) | | T-ASAN-ALLOCATOR-NULL-RETURN-2026-06-07 | test_gpu_picture_pool_uaf SIGABRTed under ASan in the "Run unit tests under sanitizer" step of tests-and-quality-gates.yml. Root cause: ASan's malloc interceptor aborts the process by default when it cannot fulfil an allocation (the test intentionally passes SIZE_MAX/2 to vmaf_gpu_picture_pool_init to exercise the NULL-return failure path). Fix: add ASAN_OPTIONS: allocator_may_return_null=1 to the sanitizer step environment so ASan returns NULL instead of aborting, matching the POSIX malloc contract. | no ADR: CI environment fix | fix/state-sweep-fix (this PR) | test_gpu_picture_pool_uaf passes under ASAN_OPTIONS=allocator_may_return_null=1. | (2026-06-07) | | T-MOTION-V2-COVERAGE-LSAN-LEAK-2026-06-07 | test_integer_motion_v2_coverage multi-frame tests (test_motion_v2_three_frame_flow, test_motion_v2_moving_average_branch, test_motion_v2_10bit_extract) leaked VmafPicturePrivate, VmafRef, and pixel buffers under LSan. Root cause: the tests set ctx->fex->prev_ref = refs[i-1] as a raw struct copy (no vmaf_picture_ref call), bypassing the ref-count protocol. The PREV_REF wrapper in feature_extractor.cpp then called vmaf_picture_unref on this raw copy and vmaf_picture_ref on the current frame — correct when called from libvmaf.c which uses vmaf_picture_ref to hand off frames, but incorrect here because the test's raw copy was not a counted reference. After the loop, the final frame's ref count was 2 (1 from alloc + 1 from wrapper); context_destroy decremented one, the test loop decremented one — count stayed at 0 but was decremented twice, causing double-free UB on one frame and leaking another. Fix: remove all manual prev_ref assignments and memset calls; the PREV_REF wrapper manages prev_ref automatically as in production; context_destroy handles final release. Also adds vmaf_picture_pool_flush() to picture.c/picture.h to drain the global pixel-buffer pool at test teardown. | no ADR: test bug fix | fix/state-sweep-fix (this PR) | ASAN_OPTIONS=allocator_may_return_null=1:detect_leaks=1 ./test/test_integer_motion_v2_coverage — 6/6 pass, no LSan output. | (2026-06-07) | | T-NIGHTLY-BISECT-TRACKER-WRONG-ISSUE-2026-06-07 | nightly-bisect.yml set BISECT_TRACKER_ISSUE: "40", pointing at PR #40 (a closed pull request, not a standalone issue). The post-bisect-comment.py script posts the sticky result comment via gh api repos/VMAFx/vmafx/issues/{N}/comments. GitHub's GITHUB_TOKEN with issues: write can create comments on standalone issues but not on closed pull requests without pull-requests: write. The workflow has been failing every night since 2026-05-29. Fix: created standalone tracking issue #827 ("tracking: nightly bisect-model-quality results"), updated BISECT_TRACKER_ISSUE to "827", updated workflow comments, and improved _gh_with_stdin to surface gh stderr on failure for future diagnostics. | no ADR: CI infrastructure bug fix | fix/nightly-bisect-tracker-issue | python scripts/ci/post-bisect-comment.py --issue 827 --report /tmp/bisect-dl/result.json --repo VMAFx/vmafx exits 0 and posts the sticky comment to issue #827. | (2026-06-07) | | T-DNN-ONNX-DOMAIN-BYPASS-2026-06-07 | core/src/dnn/onnx_scan.c checked only NodeProto.op_type (field 4) against the allowlist, never NodeProto.domain (field 7). ONNX Runtime dispatches via (domain, op_type) tuple; a crafted model could pass an allowlisted op_type (e.g. "Conv") alongside a non-standard domain (e.g. "com.evil") to execute arbitrary custom-registered ops before ORT's own session sandbox applies. Fix: read_domain() helper added to onnx_scan.c; any domain other than "" or "ai.onnx" returns -EPERM at every node level including control-flow subgraphs. Five new unit tests added. | ADR-1089 | fix/dnn-onnx-domain-bypass | test_onnx_scan 26/26 pass. | (2026-06-07) | | T-VMAF-MCP-ALLOW-WINDOWS-PATH-SEPARATOR-2026-06-06 | pkg/libvmaf/paths.go::AllowedRoots split the VMAF_MCP_ALLOW env-var on a hardcoded ":" instead of filepath.SplitList. On Windows the OS path-list separator is ";", so entries with drive letters such as C:\foo were silently mis-split at the drive colon, leaving C as a root and \foo as a second — neither of which is the intended path. The project CI matrix includes a windows-2025 runner, confirming Windows is a supported target. Fix: replace strings.Split(extra, ":") with filepath.SplitList(extra). Unix behaviour unchanged. | ADR-1084 | fix/cross-platform-path-list-separator | go test ./pkg/libvmaf/... passes; VMAF_MCP_ALLOW=C:\foo;D:\bar is correctly split on Windows. | (2026-06-06) | | T-WIN32-PTHREAD-ONCE-REDEFINITION-2026-06-06 | core/src/compat/win32/pthread.h defined pthread_once_t, PTHREAD_ONCE_INIT, and pthread_once() twice in the same translation unit: once correctly near the top of the file (lines 68–87, using a context-struct trampoline), and again redundantly near the bottom (lines 204–226, using a raw (void(*)(void)) cast). MSVC and clang-cl emitted error: redefinition of pthread_once (and equivalent C2371 on MSVC), breaking Build — Windows (MSVC + CUDA), Build — Windows MSVC + oneAPI SYCL (build only), and Build — Windows MSVC + CUDA (build only). Fix: delete the duplicate block (typedef, macro, callback, and inline function). The first definition is retained unchanged; it is the cleaner implementation and already consistent with ADR-0871 / ADR-0181. | no ADR: build bug fix | fix(compat): remove duplicate pthread_once definition in win32 shim | grep -c pthread_once core/src/compat/win32/pthread.h returns 1 definition. | (2026-06-06) | | T-UBSAN-ENUM-INVALID-VALUE-LOG-OPT-2026-06-06 | UBSan's enum-invalid-value check fired in vmaf_log (log.cpp:127) when test_log passed VMAF_LOG_LEVEL_NONE-1 (= -1 / 4294967295u). The prior fix (cast-to-int in the function body) did not eliminate the UBSan violation because UBSan fires on the enum parameter LOAD in the function prologue, before any user code executes. Real fix: annotate vmaf_log with __attribute__((no_sanitize("enum"))) guarded by a __clang__ / GCC≥8 version check so only the enum sub-check is suppressed on this function; all other UBSan diagnostics remain active. | ADR-1080 | fix/ci-tests-quality-gate-failures (this PR) | test_log passes under UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1:print_stacktrace=1. | (2026-06-07) | | T-CI-VULKAN-OPTION-REMOVED-2026-06-07 | vulkan-vif-cross-backend and vulkan-parity-matrix-gate jobs in tests-and-quality-gates.yml both failed at the meson setup step with ERROR: Unknown options: "enable_vulkan". The Vulkan backend was removed in ADR-0726 (PR #47); meson_options.txt dropped the enable_vulkan option at that time but the two workflow jobs were never disabled and continued to pass -Denable_vulkan=enabled to every subsequent meson invocation. Fix: set if: false on both jobs (with an inline comment referencing ADR-0726), preventing them from running until Vulkan support is formally reinstated under a new ADR. | ADR-0726 | fix/ci-tests-quality-gate-failures (this PR) | Jobs do not appear in the workflow run; no meson error. | (2026-06-07) | | T-TSAN-OOM-ABORT-POOL-UAF-2026-06-07 | test_gpu_picture_pool_uaf SIGABRTed under TSan in tests-and-quality-gates.yml. The test passes a huge allocation count (pic_cnt = 0x7FFFFFFFu) to vmaf_gpu_picture_pool_init to verify the NULL-return failure path. TSan's allocator (like ASan's, but unlike the default glibc allocator) aborts the process on oversized-allocation failure instead of returning NULL. The ASan lane already had ASAN_OPTIONS: allocator_may_return_null=1; the TSan lane was missing the equivalent. Fix: add TSAN_OPTIONS: allocator_may_return_null=1 to the sanitizer step env block in tests-and-quality-gates.yml. | no ADR: CI environment fix | fix/ci-tests-quality-gate-failures (this PR) | test_gpu_picture_pool_uaf passes under TSan with TSAN_OPTIONS=allocator_may_return_null=1. | (2026-06-07) | | T-COVERAGE-GATE-ORT-BACKEND-FLOOR-BREACH-2026-06-07 | core/src/dnn/ort_backend.c measured 79.0% at CI run 27093693903 (2026-06-07), below the 83% per-file floor set by ADR-0922 ratchet (+5pp over ADR-0114's original 78%). EP-device branch paths (try_append_cuda, try_append_openvino, try_append_rocm, try_append_coreml) and the ADR-0113 two-stage CreateSession fallback were unreachable through existing tests. Fix: 12 EP-device fallback tests added to test_ort_internals.c; each requests a non-CPU device which on CPU-only runners either fails EP registration (-ENOSYS) or registers an EP but fails CreateSession (no hardware), triggering the fallback and returning 0. Also covers the threads>0 config path. | ADR-0114 / ADR-0922 | fix/ci-tests-quality-gate-failures (this PR) | meson test -C build-fix-check test_ort_internals PASS; Coverage Gate expected green at ≥83%. | (2026-06-07) |
| T-MCP-SCORE-POOLED-EAGAIN-2026-06-06 | vmaf_score_pooled returned -EAGAIN (−11) on any multi-frame sequence called after flush, causing the MCP compute_vmaf handler to emit a JSON-RPC error. Root cause: the -EAGAIN guard in vmaf_score_at_index (added by ADR-0154 / Netflix#755 to protect retroactive-write input features) was also applied to the model output score slot. After frame 0 prediction creates the "vmaf" feature vector (capacity > 1, slot 0 written), frames 1+ returned -EAGAIN from get_score (unwritten slot); the guard suppressed vmaf_predict_score_at_index, propagating -EAGAIN through vmaf_score_pooled. Fix: if (err && err != -EAGAIN) → if (err) in vmaf_score_at_index. Input features are fully available after flush, so no -EAGAIN propagates from the input side at scoring time. Companion fix: test_compute_vmaf_10bit fixture bumped 64→192. | ADR-1073 | fix(core): vmaf_score_at_index EAGAIN guard misapplication | fast suite 84/84, test_mcp_smoke 18/18. | (2026-06-06) | | T-PREV-REF-BATCH-REFCOUNT-LEAK-2026-06-06 | threaded_extract_batch_func did a bare struct copy of f->prev_ref into fex->prev_ref (no vmaf_picture_ref), then after extract() called memset without vmaf_picture_unref. The PREV_REF SWAP in feature_extractor.cpp bumps the current frame's refcount into fex->prev_ref; the bare memset discarded that counted reference without triggering the picture-pool release callback, exhausting the pool after ~pool_size frames. vmaf_picture_pool_fetch deadlocked in pthread_cond_wait. threaded_extract_func (dead code) had the same pattern. Three companion test failures: (1) test_hip_ms_ssim_parity and test_cuda_float_ms_ssim_parity used 256x144 fixtures; float_ms_ssim requires min(w,h)>=176; (2) test_hip_motion_parity queried VMAF_integer_feature_motion_score without debug=true. | ADR-1072 | fix(core): PREV_REF refcount leak in batch dispatch + test fixture/debug-flag fixes | test_picture_pool_basic passes; fast-suite 83/83. | (2026-06-06) |
| T-PIC-PREALLOC-ASAN-LEAK-2026-06-06 | test_pic_preallocation failed ASan detect_leaks=1 in CI even after PR #765 fixed the deadlock. Root cause: flush_context_threaded calls fex->flush(fex, ...) directly on the shared (never-initialized) extractor instance rather than a per-thread deep copy. integer_motion::flush lazily creates s->feature_name_dict when it was not set by init; however vmaf_feature_extractor_context_close returned early because is_initialized == false, so close_fex was never invoked and the dict leaked. Fix: set fex_ctx->is_initialized = true before the flush loop so that feature_extractor_vector_destroy at teardown actually calls fex->close. Verified: 8/8 subtests pass under ASAN_OPTIONS=halt_on_error=1:detect_leaks=1 UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1. | no ADR: bug fix (mechanically follows from ADR-1072 analysis) | fix(core): mark shared fex_ctx initialized before batch flush to prevent dict leak | 8/8 test_pic_preallocation subtests pass; fast-suite 83/83. | (2026-06-06) |
| T-GPU-POOL-UAF-OOM-ASAN-UBSAN-GAP-2026-06-06 | test_gpu_picture_pool_uaf, test_integer_motion_v2_coverage, and test_pic_preallocation intentionally allocate up to ~192 GiB to exercise OOM cleanup paths. PR #735 had added test_gpu_picture_pool_uaf to the TSan exclusion list in sanitizers.yml, but the same three tests were missing from the ASan+UBSan exclusion regex in that same file, and from all three sanitizer arms (address, undefined, thread) of the case-based deselect block in tests-and-quality-gates.yml (the ADR-0347 mechanism). The ASan/UBSan and TSan allocators abort with SIGABRT instead of returning NULL on huge-alloc paths, causing spurious CI failures with no diagnostic value. PR #767 added all three tests to the ASan+UBSan+TSan exclusion regex in sanitizers.yml; PR #770 mirrored the same exclusions into tests-and-quality-gates.yml. The OOM paths remain covered by the unsanitized meson suite on every run. | no ADR: CI exclusion fix | fix(ci): extend ASan/UBSan + TSan exclusion list for intentional huge-alloc tests (PR #767) + apply to tests-and-quality-gates.yml (PR #770) | Sanitizer CI jobs no longer SIGABRT on the three OOM tests; meson test -C build --suite=fast remains unaffected. | (2026-06-06) |
| T-HIP-MOTION-DEBUG-BOOL-SYCL-GRAPH-DANGLING-2026-06-06 | Two test-and-runtime correctness bugs fixed together in PR #768. (1) HIP motion test (test_motion_cpu_hip_parity): vmaf_use_feature(motion) returned -EINVAL because the test passed "1" for a VMAF_OPT_TYPE_BOOL option; set_option_bool() in opt.c accepts only "true" / "false". Fixed by changing "1" to "true" in test_hip_motion_parity.c. (2) SYCL graph dangling-priv SIGSEGV (test_sycl_motion_add_uv_parity): the test shared one VmafSyclState across two sequential VmafContext instances. When Pass 1 closed, close_fex_sycl freed MotionStateSycl priv but left its entry in sycl_state->graph_extractors[]. Pass 2's record_combined_graphs() called config_fn(ge.priv, slot) for all entries, including slot 0 whose priv was dangling — SIGSEGV. Fixed by adding vmaf_sycl_graph_unregister(state, priv) (compacts the array by priv pointer, invalidates recorded graphs) and calling it from close_fex_sycl. | no ADR: test file + SYCL internal lifecycle bug fix | fix(test,sycl): HIP motion debug bool "1"→"true" + SYCL graph dangling-priv SIGSEGV (PR #768) | meson test -C build test_hip_motion_parity test_sycl_motion_add_uv_parity — both skip cleanly on hosts without HIP / SYCL device (which is a pass). | (2026-06-06) |
| T-MOTION-FIVE-FRAME-WINDOW-PYTHON-SKIP-2026-06-06 | Nine VmafIntegerFeatureExtractor test methods in python/test/feature_extractor_test.py that set motion_five_frame_window=True were failing CI with hard exit-234 errors because the C integer-motion path returns -ENOTSUP per ADR-0337 (the prev_prev_ref picture-pool plumbing is deferred). PR #771 adds @unittest.skip("ADR-0337: motion_five_frame_window not yet plumbed into C; see ENOTSUP") to all 9 affected methods. No assertAlmostEqual golden assertions are modified. The skip decorators preserve the test bodies for future re-enable once the C-side plumbing lands. | no ADR: test maintenance (skip-marking) | test(python): skip motion_five_frame_window tests pending ADR-0337 C plumbing (PR #771) | python -m pytest python/test/feature_extractor_test.py -k "motion_five_frame_window" -v — all 9 show SKIPPED; no errors. | (2026-06-06) |
| T-NEON-FMA-FLOAT-ADM-DWT2-REVERT-2026-06-06 | PR #685 wired AdmSimdDispatch so float-ADM NEON kernels were called at runtime; test_float_adm_dwt2_bitexact failed on ARM CI with 1-ULP FMA gap. A #pragma clang fp contract(off) carve-out (PR #690) was insufficient across all ARM toolchain configurations. PR #695 reverted PR #685 in full — the SIMD kernels remain compiled but are no longer dispatched. T-NEON-FMA-FLOAT-ADM-DWT2-2026-06-06 left Open for the follow-up rewire. | ADR-1057 | revert(perf): roll back float-ADM SIMD dispatch wiring (PR #695) | ARM CI test_float_adm_dwt2_bitexact passes after revert. | (2026-06-06) |
| T-METAL-FEATURE-COLLECTOR-EXTERN-C-2026-06-06 | feature_collector.h lacked extern "C" guards. Objective-C++ Metal .mm files that included the header in C++ mode emitted undefined-reference linker errors on macOS Clang+Metal builds. Fix (PR #694): add #ifdef __cplusplus guards around the entire public API in feature_collector.h. | no ADR: build bug fix | fix/metal-feature-collector-extern-c (PR #694) | macOS Clang+Metal CI leg links cleanly. | (2026-06-06) |
| T-SYCL-MOTION-ADD-UV-SIGSEGV-2026-06-07 | test_sycl_motion_add_uv_parity SIGSEGV after PRs #768 and #796. Two root causes: (1) SYCL test executables linking libvmaf.a lacked -fsycl at link time — clang-offload-wrapper is skipped, ProgramManager never registers device kernels, first q.submit() null-dereferences in getDeviceKernelInfo. (2) vmaf_feature_score_at_index queries used raw VMAF_*_score names, but with motion_add_uv=true the feature-name system stores under aliased names (integer_motion2_mau, float_motion2_mau). Fix: embed -fsycl in sycl_dependency.link_args; update queries; remove should_fail. | ADR-1099 | fix/sycl-fsycl-link-propagation | meson test -C build --suite=fast test_sycl_motion_add_uv_parity on SYCL-capable host passes. | (2026-06-07) |
| T-SYCL-DICT-INCLUDE-MISSING-2026-06-06 | test_sycl_motion_add_uv_parity.cpp used VmafDictionary without including dict.h. Inclusion order masked this on builds where another header pulled it in transitively; became visible after header-guard restructuring in PRs #696. Fix: add #include "dict.h" to the test TU. | no ADR: build fix | fix/sycl-test-dict-include (PR #696) | SYCL test suite links cleanly. | (2026-06-06) |
| T-INTEGER-SSIM-I686-INCLUDE-2026-06-06 | test_integer_ssim_simd.c gated the integer_ssim.h include behind #if HAVE_AVX2, causing integer_ssim_moments_t to be an unknown type on i686 (no-asm) builds. Fix (PR #700): include integer_ssim.h unconditionally. | no ADR: build fix | fix/integer-ssim-simd-i686-include (PR #700) | i686 no-asm build produces no "unknown type" errors. | (2026-06-06) |
| T-WAVE8-OBJ-TARGET-DEPS-2026-06-06 | 35 meson test targets depended on wave8_cpp23_objects — a target that was renamed to wave8_opt_only_objects in PR #531. The stale dependency name caused all 35 tests to fail at link time on fresh build directories. PRs #699 and #701 corrected the target name in all affected meson.build entries. | no ADR: build fix | fix/wave8-target-dep-rename (PRs #699 and #701) | meson test -C build --suite=fast resolves all 35 link targets. | (2026-06-06) |
| T-DNN-INT8-TEST-ADR1032-ALIGN-2026-06-06 | test_session_open_int8_missing expected vmaf_dnn_session_open to return an error when the int8 sidecar was absent. ADR-1032 changed the behaviour to fall through to fp32 (return 0). The test was asserting old error-return semantics. Fix (PR #705): update the test to assert rc == 0 and that the session operates in fp32 mode. | no ADR: test-correctness fix | fix/dnn-int8-test-adr1032 (PR #705) | meson test -C build test_session_open_int8_missing passes. | (2026-06-06) |
| T-MCP-SMOKE-11-FAILURES-2026-06-06 | 11 MCP Smoke CI tests failed after Vulkan removal (ADR-0726) and async callsite changes in earlier PRs left stale import paths, non-async await expressions missing, and removed tool names in the smoke manifest. Fix (PR #706): repair async callsites, update tool-name list, remove Vulkan backend references from smoke suite. | no ADR: test fix | fix/mcp-smoke-failures (PR #706) | MCP Smoke CI job passes all 11 repaired tests. | (2026-06-06) |
| T-GPU-POOL-CPP-NULL-CLEAR-2026-06-06 | vmaf_gpu_picture_pool_init in gpu_picture_pool.cpp omitted *pool = nullptr at the free_p failure label. When the struct-level malloc succeeded but the pic-array malloc or pthread_mutex_init failed, the caller's handle was left pointing to freed memory. The companion C stub already carried the null-clear; the C++ version did not. Caught by test_gpu_picture_pool_uaf (exit status 1 on Ubuntu gcc static + Ubuntu clang CI). Fix: add *pool = nullptr; before the fail: label. | no ADR: one-line bug fix | fix/gpu-picture-pool-cpp-null-on-failure | test_gpu_picture_pool_uaf passes. | (2026-06-06) |
| T-FFMPEG-PATCHES-SCORE-FMT-GAP-2026-06-06 | vmaf_write_output_with_format() (ADR-0119, commit 695d29626) exposed --precision flag semantics on the C API, but all four FFmpeg filters (libvmaf, libvmaf_sycl, libvmaf_vulkan, libvmaf_metal) still called the old vmaf_write_output(), hard-coding %.6f precision regardless of any user-supplied format string. PR #723 adds new FFmpeg patch 0016-libvmaf-wire-score-fmt-on-all-vmaf-filters.patch: adds score_fmt AVOption (string, default NULL = %.6f) to all four filters and replaces vmaf_write_output() with vmaf_write_output_with_format(). PR #740 regenerated patch 0016 with correct hunk counts after a merge conflict. | ADR-1064 | feat/ffmpeg-patches-score-fmt-gap (PR #723 + hotfix PR #740) | ffmpeg -h filter=libvmaf 2>&1 \| grep score_fmt shows the option; log output with score_fmt=%.17g contains 17 significant figures. | (2026-06-06) |
| T-VENDORED-CJSON-PDJSON-SECURITY-2026-06-06 | Five security and correctness bugs in vendored pdjson and cJSON fixed: (1) PDJSON_STACK_MAX was never defined — the depth guard inside #ifdef PDJSON_STACK_MAX was compiled out, allowing unlimited heap growth on deeply-nested JSON inputs; (2) pdjson push() size calculation lacked overflow guard before the multiplication (CERT-C INT30-C); (3) pdjson pushchar() string-buffer doubling lacked a guard for string_size >= SIZE_MAX/2, wrapping the product to zero and causing realloc to shrink the buffer (CERT-C INT30-C); (4) ADR-0683 banned-function remediation (sprintf → snprintf, strcpy → memcpy) implemented at 12 call sites in cJSON.c — prior PRs #890 and #891 were closed without merging, leaving the violations in tree; (5) cJSON_GetArraySize size_t-to-int cast wrapped on arrays with more than INT_MAX elements — clamped to INT_MAX (CERT-C INT31-C). | ADR-1061 | fix/vendored-pdjson-cjson-depth-overflow (PR #725) | meson test -C build --suite=fast passes; grep 'PDJSON_STACK_MAX' core/src/pdjson.c shows a definition; grep 'sprintf\|strcpy' core/src/mcp/3rdparty/cJSON/cJSON.c returns no matches. | (2026-06-06) |
| T-GO-STATICCHECK-R10-TIMER-BODY-2026-06-06 | Three Go staticcheck-class bugs fixed: (1) pkg/storage/waitForHTTP and waitForPath called time.After inside for/select poll loops, allocating a new *time.Timer per iteration that leaked until it fired when ctx.Done() won the race — replaced with time.NewTicker + defer Stop() (SA1015); (2) cmd/vmafx-controller/handleScore decoded the request body without a size cap — an adversary could push an arbitrarily large payload; added http.MaxBytesReader(1 MiB) + HTTP 413 response, matching the guard already in vmafx-server; (3) both vmafx-controller and vmafx-server HTTP servers had ReadHeaderTimeout (Slowloris guard) but no ReadTimeout, leaving the body-read phase wall-clock unbound — added ReadTimeout: 30s. | ADR-1065 | fix/go-r10-staticcheck-timer-body (PR #729) | go vet ./... and staticcheck ./... clean; curl -X POST http://localhost:8080/v1/score -d @large_payload returns HTTP 413. | (2026-06-06) |
| T-JSON-MODEL-SLOPES-FEATURE-CAP-OOB-2026-05-30 | core/src/read_json_model.c::parse_slopes called ensure_feature_capacity(model, i + 1u) for each slope value but never updated model->n_features to match the resulting feature_cap. vmaf_model_destroy walked max(feature_cap, n_features) slots, dereferencing model->feature[i].name for uninitialised tail entries — an ASan heap-buffer-overflow read when a fuzzer-mangled JSON has a slopes array longer than feature_names. Surfaced by the fuzz_json_model harness (ADR-0882, PR #716) at ~1.28 M iterations / 10 s. Fix (PR #743): sync_n_features max-merge helper called after every per-feature walker; validate_feature_arrays rejects mismatched models at parse time; vmaf_model_destroy bounds walk at min(feature_cap, n_features). | ADR-0887 | fix/vmaf-model-destroy-heap-oob (PR #743) | fuzz_json_model runs 60 s on the seed corpus without re-discovering the crash; nightly fuzz CI green. | (2026-06-06) |
| T-MSVC-CPP-STD-C23-2026-06-06 | Build — Windows MSVC + CUDA CI leg failed at meson configure with ERROR: None of values ['c++23'] are supported by the CPP compiler after ADR-1003 set cpp_std=c++23 in default_options. Meson's MSVC backend accepted-values list omits c++23; valid tokens are c++11/14/17/20/vc++latest. Fix (ADR-1056): remove cpp_std from default_options; inject via add_project_arguments guarded by get_option('cpp_std')=='none' — MSVC receives /std:c++latest, all other compilers receive -std=c++23. SYCL leg unaffected (its -Dcpp_std=c++14 override bypasses the guard). | ADR-1056 | fix/msvc-cpp-std-vc-latest-1056 (PR #692) | meson setup build core -Denable_cuda=false -Denable_sycl=false configures clean on Linux GCC; MSVC leg verified via CI. | (2026-06-06) |
| T-CI-JIMVER-CUDA-133-NOT-AVAILABLE-2026-06-06 | Build — Linux (GCC, all backends) and Build — Ubuntu SYCL + CUDA failed with Error: Version not available: 13.3.0. The Jimver/cuda-toolkit action v0.2.35 does not include 13.3.0 in its installer index; the version was bumped to 13.3.0 in PR #664 which works for apt-based installs (Containerfile, Dockerfile) but not for the GH-action path. Fix: revert CI CUDA pin to 13.2.0 in build.yml and libvmaf-build-matrix.yml (PR #691). Container installs stay at 13.3 (NVIDIA apt repo has it). | no ADR: CI pin fix with no user-visible delta | fix/ci-pin-cuda-132-jimver (PR #691) | CI passes; grep "cuda:" .github/workflows/*.yml shows 13.2.0 on all Jimver steps. | (2026-06-06) |
| T-MINGW64-CONSTINIT-MUTEX-2026-06-04 | Build — Windows MinGW64 (CPU) CI failing with constinit variable '{anonymous}::g_lock' does not have a constant initializer; call to non-constexpr function 'std::mutex::mutex()'. std::mutex has a non-constexpr constructor in GCC/MinGW libstdc++; MSVC's STL provides one, so the issue was masked on MSVC builds. constinit was applied in gpu_dispatch_env.cpp per ADR-0858. Fix: remove constinit from g_lock; static storage duration already guarantees zero-initialization before any dynamic init. std::array<EnvRow, kTableCap> retains constinit (aggregate default-init is constant). Also corrects SPDX BSD-3-Clause-Plus-Patent → BSD-2-Clause-Patent in the file header. | no ADR: build bug fix per CLAUDE §12 r8 | fix/macos-docker-platform-unblock | MinGW64 CI green; g++ -std=c++23 -c core/src/gpu_dispatch_env.cpp clean without error. | (2026-06-04) | | T-STRING-VIEW-MISSING-INCLUDE-2026-06-04 | core/tools/vmaf.cpp used std::string_view (lines 856 and 935) without #include <string_view>. Linux GCC/Clang pulled it in transitively through <string> machinery; Apple Clang did not. Caused build failures on Build — macOS (Clang, CPU + Metal), Build — macOS clang (CPU), Build — macOS clang (CPU) + DNN, and FFmpeg — macOS clang (Build Only). Introduced by the C++23 Wave bundle (PR #531, commit 9f844f06). Fix: add #include <string_view> to the include block. No functional change. | no ADR: build bug fix per CLAUDE §12 r8 | fix/macos-docker-platform-unblock | macOS Clang build clean; clang++ -std=c++23 core/tools/vmaf.cpp no "undefined type" errors. | (2026-06-04) | | T-CUDA-CLOSE-VMAFCUDAFUNCTIONS-2026-06-04 | PR #516 (GPU resource-leak bundle) added cuModuleUnload teardown calls to 13 CUDA feature-extractor close() callbacks using the fabricated type VmafCudaFunctions. The actual type defined in core/src/cuda/common.h is CudaFunctions. VmafCudaFunctions is not defined anywhere; on CUDA-enabled builds (-Denable_cuda=true, Docker, Build — Linux GCC all backends, Build — Windows MSVC+CUDA) the compiler emitted "unknown type name" hard errors. Fix: replace const VmafCudaFunctions *cu_f with const CudaFunctions *cu_f in all 13 affected .c files. No functional change — same pointer type, correct member access. | no ADR: build bug fix per CLAUDE §12 r8 | fix/macos-docker-platform-unblock | Docker ninja -C build -Denable_cuda=true clean; no "unknown type name VmafCudaFunctions" errors. | (2026-06-04) | | T-CUDA-ADM-CM-MODULE-UNDECLARED-2026-06-06 | PR #516 (GPU resource-leak bundle, commit aa177510d) also left four cuModuleGetFunction call-sites in integer_adm_cuda.c::init_fex_cuda() referencing bare adm_cm_module instead of s->adm_cm_module. The struct member is declared at line 96 (CUmodule adm_cm_module) and loaded correctly at line 1335 (cuModuleLoadData(&s->adm_cm_module, ...)); only the four cuModuleGetFunction calls at lines 1393, 1396, 1401, 1405 dropped the s-> prefix. GCC emitted error: use of undeclared identifier 'adm_cm_module' on CUDA-enabled builds. PR #693 fixed the distinct VmafCudaFunctions typo but missed these sites. Fix: prefix all four occurrences with s->. No functional change. | no ADR: build bug fix per CLAUDE §12 r8 | fix/adm-cm-module-undeclared-identifier | ninja -C build-cuda clean on CUDA-enabled build; no "undeclared identifier" errors. | (2026-06-06) |
| T-MACOS-SIGSEGV-UNRESOLVED-2026-05-19 | macOS CI RED (Build — macOS clang (CPU), Build — macOS clang (CPU) + DNN, Build — macOS Metal (runtime)) was a compile error, not a runtime SIGSEGV. integer_ssim_moments_t was defined only inside #if ARCH_X86 in core/src/feature/x86/integer_ssim_avx2.h; the type was used unconditionally in integer_ssim.c for function-pointer typedefs and scalar wrappers. macOS arm64 runners (macos-latest → macos-15-arm64, image 20260527.0100.1) produced 8 "unknown type name" errors at build time. Fix: promote typedef to new shared core/src/feature/integer_ssim.h included unconditionally; update x86 header to pull from the shared header. Historical SIGSEGV crashes (ADR-0602, ADR-0606) were separate output.c bugs already resolved. Investigation doc: macos-sigsegv-investigation.md. | ADR-1040 | fix/integer-ssim-moments-type-non-x86 (PR #654, commit 695d29626) | ninja -C build on macOS arm64 clean; 8 compile errors absent. | (2026-06-04) | | T-CPU-SCORING-NAN-UB-GUARDS-2026-06-04 | Round-6 audit surfaced 11 CPU-path scoring edge-case defects: APSNR log10(0) on silent video, MS-SSIM pow(neg, frac) + size overflow, float SSIM/MS-SSIM convert_to_db NaN, iqa/ssim_tools.c assert-abort on zero variance, ADM score_aim uninit + harmonic-mean NaN + skip_scale0 sentinel, MOTION wrong bilinear stride + OOM crash, and CAMBI v_band_size uint16_t overflow. All 11 fixed in one cluster PR (fix/r6-cpu-scoring-nan-ub-guards). | ADR-1033 | fix/r6-cpu-scoring-nan-ub-guards | meson test -C build --suite=fast passes; UBSan build exercises each NaN/overflow path without fault. | (2026-06-04) | | T-ARM-MOTION-V2-MISSING-2026-06-04 | vmaf_get_feature_extractor_by_name("motion_v2") returned NULL on CPU-only builds (including ARM64) after PR #532 / commit 6bb5464511 removed integer_motion_v2.c from the meson CPU source list and dropped the extern declaration + list entry from feature_extractor.c. All tests calling the lookup received NULL and failed. Secondary failure: test_motion_three_frame_extract_emits_scores called vmaf_feature_collector_get_score for motion2_score before flush(), but the post-port contract defers motion2 emission to flush time. Fix: re-add integer_motion_v2.c to meson CPU sources, restore extern + list entry, and move the get_score call to after flush. | ADR-1052 | fix/arm-motion-v2-re-register-and-test-order | vmaf_get_feature_extractor_by_name("motion_v2") != NULL; meson test -C build --suite=fast test_integer_motion_v2_coverage passes. | (2026-06-04) | | T-SIMD-DIVERGENCE-CLUSTER-2026-06-04 | test_ms_ssim_decimate, test_psnr_hvs_simd, and test_ssimulacra2_simd failed on CI because FMA-unification commits (15001cd6c3, 83698bd5b, 31dce40e1) existed only on a local branch. PR #681 cherry-picked all three onto master: (1) ffp-contract carve-outs + ssimulacra2 X colour-matrix reorder; (2) -fp-model=precise for icx; (3) FMA round-2 — extend -fp-model=precise to libvmaf_feature_static_lib and libvmaf_ssimulacra2_static_lib. Also fixed a pre-existing #include <assert.h> omission in libvmaf.c (from PR #268 / ADR-0795). | ADR-0891 | fix/simd-fma-unification-cluster (PR #681, commit 9bd6bf80) | meson test -C core/build-cpu --suite=fast test_ms_ssim_decimate test_psnr_hvs_simd test_ssimulacra2_simd → all pass. | (2026-06-04) |
| T-GPU-COVERAGE-STABLE-WEEKS — coverage-gpu job in .github/workflows/tests-and-quality-gates.yml was advisory (continue-on-error: true) pending a two-week stability window on the self-hosted gpu-full runner. Tracking started 2026-05-19 (ADR-0623); the promotion target date of 2026-06-02 elapsed with no advisory-fail runs. continue-on-error: true removed and job renamed from (Advisory) to required. | ci/promote-gpu-coverage-gate-required | Two-week stability window (2026-05-19 → 2026-06-02) with no advisory failures confirmed. coverage-gpu is now a hard CI gate. | (2026-06-04) | | T-R9-HELM-SCHEMA-DRIFT-2026-06-04 | Four Helm chart correctness gaps: (1) storage key in values.schema.json but absent from values.yaml; (2) networkPolicy, auth, otelCollector defined in values.yaml but absent from schema — typos silently accepted; (3) gpu.count minimum 0 caused silent no-op deployments with vendor device plugins; (4) gpu.enabled not in gpu.required. All four fixed. | ADR-1047 | fix/r9-helm-vmaftune-grpc-bugs | helm lint deploy/helm/vmafx passes with storage key present and gpu.count:0 rejected. | (2026-06-04) | | T-R9-VMAFTUNE-DURATION-SENTINEL-2026-06-04 | _stamp_tracked_default_sentinels iterated only ("framerate", "duration", "target_vmafs", "target_vmaf"). The ladder sub-command registers --duration with dest="duration_s", so _duration_s_was_default was never set — ladder source-duration always appeared as user-overridden. Fix: add "duration_s" to the tuple. | ADR-1048 | fix/r9-helm-vmaftune-grpc-bugs | python -c "from vmaftune.cli import _stamp_tracked_default_sentinels; import argparse; a=argparse.Namespace(); _stamp_tracked_default_sentinels(a); assert a._duration_s_was_default" passes. | (2026-06-04) |
| T-R9-FEEDBACK-FIXED-RETRY-2026-06-04 | online_feedback.go drainLoop used a fixed feedbackRetryInterval = 10s constant. During extended sidecar outages (rolling restarts, OOMKills) the drainer retried every 10s indefinitely, generating steady log noise. Fix: exponential backoff starting at 2s, doubling up to 2m cap, reset on successful connection. | ADR-1049 | fix/r9-helm-vmaftune-grpc-bugs | go build ./cmd/vmafx-node/... — no compile errors. | (2026-06-04) | | T-CI-WF-CONCURRENCY-TIMEOUT-2026-06-04 | CI workflows lacked concurrency guards (nightly, nightly-bisect, supply-chain, release-please could run duplicate jobs on rapid pushes); rust-ci, go-ci, and scorecard had no timeout (defaulted to 6 hours, burning runner minutes on hung jobs); four mutable Docker/artifact action tags in e2e-k8s.yml were unversioned (supply-chain risk). Fix: add concurrency blocks with cancel-in-progress semantics; add timeout-minutes to critical jobs; SHA-pin action tags. | ADR-1035 | fix/r7-ci-wf-concurrency-timeout | All CI workflows validate in act --list. | (2026-06-04) |
| T-DOCS-BROKEN-ADR-LINKS-2026-06-04 | Two broken intra-doc links to nonexistent docs/adr/0720-sunset-float-ansnr.md in docs/development/build-flags.md and docs/metrics/features.md corrected to ADR-0865. 11 orphaned mkdocs.yml nav entries added (9 metric pages: ansnr, motion, ms-ssim, psnr-hvs, speed_qa, ssim, tad, vif, vmaf-neg; 2 MCP pages: backends, http-transport). | no ADR: doc fix | fix/r7-docs-broken-links-mkdocs-nav | mkdocs build --strict completes with no nav warnings. | (2026-06-04) |
| T-MCP-PRECISION-DEFAULT-DRIFT-2026-06-04 | MCP Go surface (cmd/vmafx-mcp/impl.go, tools.go) defaulted to precision "17" (%.17g) and "6" (%.6g) in the probe path. MCP Python surface (mcp-server/vmaf-mcp/src/vmaf_mcp/server.py) defaulted to "17" (%.17g). The C CLI default is "legacy" (%.6f per ADR-0119). MCP output was numerically different from C CLI output at 6+ significant figures. Fix: change all defaults and hardcoded precision values to "legacy" in both surfaces. | ADR-1038 | fix/r7-mcp-precision-subsample-drift | Running vmafx-mcp vmaf_score without explicit precision now produces %.6f output matching the C CLI. | (2026-06-04) |
| T-VENDORED-SVM-REALLOC-OOM-2026-06-04 | Three CERT MEM04-C realloc OOM defects in core/src/svm.cpp: Cache::get_data (line ~241), svm_group_classes (line ~1982), and svm_check_parameter (line ~2785) all overwrote the source pointer with the realloc return value — on OOM, realloc returns NULL and the original allocation is silently lost. Fix: replace with save-tmp/check-NULL/abort idiom consistent with existing Malloc behaviour in the file. | ADR-1039 | fix/r7-vendored-svm-realloc-oom | meson test -C build --suite=fast passes; valgrind shows no double-free on OOM injection. | (2026-06-04) |
| T-SPDX-SVM-COPYRIGHT-2026-06-04 | SPDX identifier BSD-3-Clause-Plus-Patent (not a valid SPDX identifier) corrected to BSD-2-Clause-Patent in 8 package manifests (workspace Cargo.toml, bindings/rust/vmafx/Cargo.toml, and 6 Python pyproject.toml files). Missing upstream libsvm BSD-3-Clause copyright (Copyright (c) 2000-2019 Chih-Chung Chang and Chih-Jen Lin) added to core/src/svm.cpp; it was present in svm.h but absent from the implementation file, violating BSD-3-Clause clause 1. | ADR-1036 | fix/r7-licensing-spdx-svm-copyright | python3 -c "import json; json.load(open('release-please-config.json'))" passes; grep -r BSD-3-Clause-Plus-Patent Cargo.toml */pyproject.toml returns nothing. | (2026-06-04) |
| T-SYCL-SPEED-INCOMPLETE-TYPE-ACCESS-2026-06-04 | speed_chroma_sycl.cpp and speed_temporal_sycl.cpp accessed s->sycl_state->queue via raw pointer cast, but VmafSyclState is an incomplete type at the call site (struct body only in sycl/common.cpp). GCC SYCL build failed at 4 sites in each file: "member access into incomplete type 'VmafSyclState'". Fix: replace all 8 direct member dereferences with vmaf_sycl_get_queue_ptr(s->sycl_state), the public accessor already declared in sycl/common.h. No kernel logic changed. | no ADR: bug fix per CLAUDE §12 r8 | fix/sycl-speed-incomplete-type-access | meson setup build-sycl -Denable_sycl=enabled && ninja -C build-sycl — both TUs compile cleanly. | (2026-06-04) |
| T-INTEGER-SSIM-MOMENTS-TYPE-NON-X86-2026-06-04 | integer_ssim_moments_t was defined only in core/src/feature/x86/integer_ssim_avx2.h, included under #if ARCH_X86. integer_ssim.c used the type unconditionally for function pointer typedefs and scalar wrappers. On macOS arm64 / Windows arm64 builds, eight "unknown type name" errors caused a build failure. Fix: promote the typedef to a new shared core/src/feature/integer_ssim.h header included unconditionally in integer_ssim.c; update the x86 header to pull from the shared header. | ADR-1040 | fix/integer-ssim-moments-type-non-x86 | ninja -C build on Linux x86-64 completes cleanly; gcc -std=c11 integer_ssim.c -DARCH_X86=0 also clean. | (2026-06-04) |
| T-CLI-NARROWING-CASTS-VMAF-CPP-2026-06-04 | VmafPictureConfiguration initializer in core/tools/vmaf.cpp lines 1360-1362 had three implicit int to unsigned narrowing conversions in designated initializers: .w = info.pic_w, .h = info.pic_h, and .bpc = common_bitdepth. Clang -std=c++11 strict mode treats these as hard errors (-Wc++11-narrowing), failing macOS builds; GCC accepted them silently. Fix: wrap all three with static_cast<unsigned>(...). No functional change. | no ADR: bug fix per CLAUDE §12 r8 | fix/cli-narrowing-casts-vmaf-cpp | clang++ -std=c++11 -Werror -Wc++11-narrowing core/tools/vmaf.cpp — no narrowing errors. | (2026-06-04) |
| T-SIMD-PSNR-16BIT-SCALAR-TAIL-OVERFLOW-2026-06-04 | Scalar-tail loops in psnr_sse_line_16_avx2 (core/src/feature/x86/psnr_avx2.c:105), psnr_sse_line_16_avx512 (core/src/feature/x86/psnr_avx512.c:109), and psnr_sse_line_16_neon (core/src/feature/arm64/psnr_neon.c:96) computed squared error as (int32_t)e * (int32_t)e where e can reach ±65535 for 16-bit input. The product 65535² = 4 294 836 225 exceeds INT32_MAX (2 147 483 647), invoking signed-integer overflow — undefined behaviour under C99/C11 that UBSan flags and that can produce wrong SSE values on optimising compilers. Fix: use const uint32_t e = (uint32_t)abs((int32_t)ref[j] - (int32_t)dis[j]) and accumulate as (uint64_t)e * e, mirroring the sse_line_16_c reference in integer_psnr.c. <stdlib.h> added for abs() in all three files. No ADR per CLAUDE §12 r8. | no ADR | fix/simd-psnr-16bit-scalar-tail-overflow | meson test -C build --suite=fast — all fast tests pass; UBSan build of the scalar tail with input (ref=65535, dis=0) produces 4294836225 (correct) not UB. | (2026-06-04) |
| T-R6-CUDA-VIF-FILTER1D-WIDTH-GUARD-2026-06-04 | filter1d.cu 16-bit vertical kernel had a typo in the rd-filter upper-bound guard: fwidth_rd - fwidth_rd (always 0) instead of fwidth - fwidth_rd. This widened the rd-filter tap window to cover all fwidth taps and indexed vif_filt.filter[scale+1] 1–4 entries past its allocation, causing OOB reads and wrong VIF scores at scales 0–2 on the CUDA backend. Fix: change the guard to fi < (fwidth - (fwidth - fwidth_rd) / 2), matching the correct 8-bit form at line 183. | no ADR: bug fix per CLAUDE §12 r8 | fix/cuda-vif-filter1d-adm-cm-opprec | meson test -C build-cuda --suite=fast + scripts/dev/cross_backend_diff.py CPU vs CUDA for VIF convergence within existing tolerance. | (2026-06-04) |
| T-R6-CUDA-ADM-CM-OPERATOR-PRECEDENCE-2026-06-04 | adm_cm.cu lines 373 and 712 computed x_sq as (int64_t)accum * accum + add_shift_sq >> shift_sq, parsed by C++ as + (add_shift_sq >> shift_sq) = + 0 because >> binds tighter than +. The normalisation shift was silently dropped, leaving x_sq carrying an un-normalised squared value (~10^6 vs correct ~0–1) and causing int32 overflow in the cubic accumulator — producing wrong ADM scale-0 and AIM scores on the CUDA backend. Fix: wrap as ((int64_t)accum * accum + add_shift_sq) >> shift_sq, matching CPU reference macro I4_ADM_CM_ACCUM_ROUND at integer_adm.c:743 and the correct fused kernel at line 259. | no ADR: bug fix per CLAUDE §12 r8 | fix/cuda-vif-filter1d-adm-cm-opprec | meson test -C build-cuda --suite=fast + scripts/dev/cross_backend_diff.py CPU vs CUDA for ADM convergence within existing tolerance. | (2026-06-04) |
| T-R6-SYCL-VIF-RD-STRIDE-OOB-2026-06-04 | HIGH: launch_vif_hori_impl (scalar/SIMD-32) and launch_vif_fused_impl (SIMD-16) in integer_vif_sycl.cpp used truncating e_w / 2 as the row stride for the rd_ref/rd_dis downsampled buffers. For odd frame widths, the last even column thread (gx = e_w-1) mapped to rd_x = (e_w-1)/2 which is out of bounds for the truncated stride, corrupting the next device-memory row. The rd_size allocation had the same truncation error. Fix: rd_stride = (e_w + 1U) / 2U (ceiling division) in both kernel variants; allocation changed to ((w+1U)/2U) * ((h+1U)/2U). | ADR-1034 | fix/sycl-vif-rd-stride-motion-uv-sync | meson test -C build-sycl --suite=fast — needs SYCL toolchain; CI gates otherwise. | (2026-06-04) |
| T-R6-SYCL-MOTION-UV-QUEUE-SYNC-2026-06-04 | HIGH: submit_fex_sycl in integer_motion_sycl.cpp submitted UV H2D copies via vmaf_sycl_memcpy_h2d_async() to state->queue (primary queue), but vmaf_sycl_graph_submit() only barriers combined_queue on last_upload_event from the DMA copy queue. UV data was not guaranteed visible to compute kernels, producing wrong motion scores for UV planes when motion_add_uv=true. Fix: vmaf_sycl_queue_wait(state) after UV copies flushes the primary queue before graph submission. | ADR-1034 | fix/sycl-vif-rd-stride-motion-uv-sync | meson test -C build-sycl --suite=fast — needs SYCL toolchain; CI gates otherwise. | (2026-06-04) |
| T-R5-MEMORY-ORDERING-2026-06-04 | Three HIGH-severity concurrency bugs found and fixed in the r5 audit. (1) vmaf_ref_fetch_decrement in ref.c and ref.cpp used implicit seq_cst; replaced with memory_order_acq_rel to match the canonical C11/C++ last-decrementer pattern and make the acquire-on-zero-transition intent explicit. Header ref.h gained using std::memory_order_* declarations for the C++ branch. (2) vmaf_feature_collector_destroy (feature_collector.cpp) unlocked then immediately destroyed the mutex, leaving a window where a concurrent locker acquired a destroyed mutex (UB). Fix: bool destroyed field added to VmafFeatureCollector; set under lock before the final unlock; all five public entry points test the flag after locking and return -ENODEV if set. (3) vmaf_picture_pool_fetch (picture_pool.c) unlocked before reading pool->pictures[idx]; vmaf_picture_pool_close could free the array in that window. Fix: copy the slot to a stack-local before unlocking. | ADR-1020 | fix/r5-memory-ordering | meson test -C build --suite=fast + TSan build optional. | (2026-06-04) |
| T-Y4M-DST-BUF-READ-SZ-OVERFLOW-2026-06-04 | core/tools/y4m_input.c::y4m_input_open_impl() computed dst_buf_read_sz for five chroma branches (420/420jpeg/420mpeg2, 420p10, 420p12, 422p10, 422p12) using bare int * int arithmetic. A crafted Y4M header with large pic_w/pic_h values causes signed-integer overflow, underallocating dst_buf_read_sz relative to the malloc'd dst_buf_sz. The subsequent fread(_y4m->dst_buf, 1, _y4m->dst_buf_read_sz, _fin) at line 931 could then request a read of a negative-wrapped (huge) size_t number of bytes, overflowing the heap buffer. Fix: add (size_t) casts to all five arithmetic expressions, mirroring the pattern already applied to dst_buf_sz and all other dst_buf_read_sz branches. SEI CERT C INT30-C / INT32-C. | ADR-1022 | fix/y4m-dst-buf-read-sz-overflow | python3 -c "import ctypes; print((65536*65536 + 2*(65536//2)*(65536//2)) < 0)" → True before fix (pre-fix int overflow); (size_t)65536*65536 → 4294967296 (no overflow). | (2026-06-04) |
| T-Y4M-411-POINTER-ARITH-SIZE-T-CAST-2026-06-08 | y4m_convert_411_422jpeg in core/tools/y4m_input.c line 495 computed _dst += _y4m->pic_w * _y4m->pic_h as a plain int * int expression before advancing a unsigned char * pointer. A crafted Y4M header with dimensions near 46340 x 46340 (product exceeds INT_MAX) causes signed-integer overflow — undefined behaviour under C99/C11. The sibling functions y4m_convert_42xmpeg2_42xjpeg (line 220) and y4m_convert_42xpaldv_42xjpeg (line 315) already carry (size_t) casts with explanatory comments. Fix: change to _dst += (size_t)_y4m->pic_w * _y4m->pic_h with a matching comment. SEI CERT C INT30-C / INT32-C. No ADR per CLAUDE §12 r8. | no ADR | fix/y4m-411-pointer-arith-size-t-cast | meson test -C build --suite=fast — all 84 fast tests pass. | (2026-06-08) |
| T-DOC-VULKAN-STALE-POST-ADR0726-2026-05-29 | PR #47 (ADR-0726) deleted the Vulkan source tree but docs/backends/vulkan/overview.md and docs/api/vulkan-image-import.md still described Vulkan as an active backend, misleading operators. Both files now carry a prominent "REMOVED — ADR-0726 (2026-05-28)" banner with pointers to ADR-0726, ADR-0860 (FFmpeg no-op shim), and the active CUDA/SYCL backends. docs/metrics/features.md and docs/development/build-flags.md already carried removal notices. | docs/vulkan-overview-mark-removed-adr0726 | grep "REMOVED.*ADR-0726" docs/backends/vulkan/overview.md docs/api/vulkan-image-import.md → both files return matches. | (2026-06-04) |
| T-CPP-STD-C23-BUMP-INCONSISTENCY-2026-06-04 | core/meson.build had cpp_std=c++11 as the project-wide C++ default while all C++ source files added via the Wave 1–9 migration (ADR-0708/0727) were compiled at C++23 via per-target override_options. ADR-0727 decided to bump the default but the change was never applied to the default_options block. Additionally, test_feature_collector_coverage (fast suite) had been link-failing since it was introduced: it called internal helpers from feature_collector.cpp but only linked against libvmaf which contains the .c version with static helpers. Fixed in this PR (ADR-1003): bump cpp_std=c++11 → cpp_std=c++23; add feature_collector.cpp to test sources following the test_predict pattern. | ADR-1003 | chore/build-cpp-std-c23-bump | meson test -C core/build-cxx23 --suite=fast — 73/78 pass (5 pre-existing failures unrelated to this PR). | (2026-06-04) | | T-HIP-SPEED-INTERNAL-IMPL-MISSING-2026-05-31 | speed_internal_init_dimensions (declared core/src/feature/speed_internal.h:85) and speed_internal_float_stride (line 93) had no .c implementation. Surfaced during ADR-0958 HIP kernel coverage round 4; the link defect blocked speed_chroma_hip + speed_temporal_hip parity gates (and CUDA/SYCL speed-family twins). Fix: ADR-0964 / PR #465 added core/src/feature/speed_internal.c extracting the two helpers (and sibling algorithm functions) from speed.c into a dedicated compilation unit. The parity gates deferred by round 4 now ship in round 5 (ADR-1004). | ADR-0964 | test/hip-parity-round5 | nm core/build/src/libvmaf.a \| grep 'T speed_internal_init_dimensions' resolves; meson test -C build --suite=fast test_hip_speed_chroma_parity test_hip_speed_temporal_parity pass (skip on no-AMD-GPU hosts). | (2026-06-04) |
| T-PORT-SPEED-CHROMA-SIMD-2026-06-03 | Port of upstream Netflix/vmaf commit 30f472b14 (2026-06-01): Speed_chroma: vectorize compute_covariance (AVX2 + AVX-512). New files core/src/feature/x86/speed_avx2.{c,h} and speed_avx512.{c,h} provide 4-wide and 8-wide double FMA accumulator kernels. speed.c gains compute_cov_kernel_fn typedef, scalar reference kernel compute_cov_kernel_scalar, fn-pointer field on SpeedState, and runtime dispatch in speed_init() (AVX-512 > AVX2 > scalar). core/src/meson.build wires both new TUs into the existing x86_avx2_sources / x86_avx512_sources static libs. Parity test core/test/test_speed_simd.c (4 AVX2 + 4 AVX-512 fixtures, 1e-9 relative tolerance) wired into core/test/meson.build under suite [fast, simd]. Reduces future rebase delta against upstream. | no ADR: 1:1 upstream port (CLAUDE §12 r8 exempt) | port/upstream-speed-chroma-simd-30f472b14 | meson test -C build --suite=fast test_speed_simd — AVX2 and AVX-512 lanes both pass at < 1e-9 relative tolerance on x86_64. | (2026-06-04) |
| T-DNN-ORT-INTERNALS-MISSING-ELEM-TYPE-ACCESSORS-2026-06-03 | test/dnn/test_ort_internals.c (introduced commit 84ee37946c) referenced vmaf_ort_internal_input_elem_type, vmaf_ort_internal_output_elem_type, and ELEM_TYPE_* enum constants that were never added to core/src/dnn/ort_backend_internal.h / ort_backend.c. The build-time error (ELEM_TYPE_UNDEFINED undeclared) caused the Netflix CPU Golden Tests (D24) CI job to fail at the "Build (CPU, release)" step on every master push (confirmed across commits c658b3c, 6891b46, 6102f38, 370ddb7, d0acfbad55). Fix: add VmafOrtElemType enum (UNDEFINED=0, FLOAT=1, FLOAT16=10, mirroring ONNXTensorElementDataType values), plus accessor functions in both the VMAF_HAVE_DNN (reads sess->input/output_elem_types[slot]) and !VMAF_HAVE_DNN stub (returns ELEM_TYPE_UNDEFINED) sections. No numeric change — pure test-build wiring fix. | no ADR: bug fix per CLAUDE §12 r8 | fix/dnn-ort-internals-missing-elem-type-accessors | ninja -C core/build-fix-check test/dnn/test_ort_internals — links cleanly (verified locally). Netflix golden gate (D24) unblocked. | (2026-06-03) |
| T-CUDA-DUPLICATE-CSF-R-DEFS-2026-06-03 | core/src/feature/cuda/integer_adm/adm_cm.cu failed to compile with NVCC: inline_i4_csf_r and inline_s0_csf_r were defined twice, and #define I4_FIX_ONE_BY_30 was repeated. Root cause: PR #565 (__ldg() F3 fix) was admin-merged while master already contained the AIM CM block (PR #84 / ADR-0746) that also defined those helpers. The merge duplicated the entire AIM section (315 lines, old version without __ldg() at original lines 742–1055) instead of updating the existing definitions. Fix: remove the duplicate old block; retain the PR #565 __ldg() version (lines 437/465). No numeric change — identical computation, just removes the redundant copy. Unblocks Docker Image Build + CUDA build matrix. | no ADR: bug fix per CLAUDE §12 r8 | fix/cuda-duplicate-csf-r-definitions | grep -c 'inline_i4_csf_r' core/src/feature/cuda/integer_adm/adm_cm.cu → 1 (definition) + N call sites, no duplicates; CUDA build passes. | (2026-06-03) |
| T-HIP-MS-SSIM-UINT-FLOAT-NORMALIZATION-2026-06-03 | ms_ssim_hip_upload_plane() in core/src/feature/hip/integer_ms_ssim_hip.c (introduced commit 681ab99451) called hipMemcpy2DAsync with dpitch = width * bpc_bytes directly into a width * height * sizeof(float) device buffer. This wrote only width*height raw uint8 bytes without any uint→float conversion, leaving the remaining three quarters uninitialized. The decimate and horiz kernels read garbage, producing meaningless MS-SSIM scores on HIP. Fix: add two hipHostMalloc-allocated pinned float staging buffers (h_ref, h_cmp) to MsSsimStateHip, call picture_copy() (uint→float [0,255]) before each H2D upload, and replace hipMemcpy2DAsync with hipMemcpyAsync of the float-sized buffer — mirroring integer_ms_ssim_cuda.c. Parity test test_hip_ms_ssim_parity (CPU vs. HIP at places=3 / 1e-3 per ADR-0883) wired into core/test/meson.build under suite=['fast','gpu']. | no ADR: bug fix per CLAUDE §12 r8 | fix/hip-ms-ssim-picture-copy | meson test -C build --suite=fast test_hip_ms_ssim_parity — skips cleanly on hosts without HIP device; runs parity assertion on AMD GPU. | (2026-06-03) |
| T-SYCL-MOTION-ADD-UV-SILENT-IGNORE-2026-06-03 | integer_motion_sycl.cpp silently ignored motion_add_uv=true: the option was in the options table but never applied, so CHUG/K150K sweeps that set mau=true on motion_sycl received the Y-only score without any error. Fixed by wiring UV plane blur+SAD through the SYCL kernel (ADR-0989): allocates ping-pong UV device buffers, uploads H2D in submit_fex_sycl, launches launch_blur_sad_fused for U+V in enqueue_motion_work, sums per-plane normalized SADs in collect_fex_sycl. CUDA/Vulkan/HIP/Metal backends now surface the option but return -ENOTSUP with a WARNING. Also upgraded motion_five_frame_window rejection messages from ERROR/silent to WARNING for consistency. | ADR-0989 | fix/sycl-motion-add-uv-five-frame-warn | meson test -C build --suite=fast test_sycl_motion_add_uv_parity test_sycl_motion3_parity — skips cleanly on hosts without SYCL device. | (2026-06-03) |
| T-HELM-NODE-DEPLOYMENT-COLLISION-2026-05-30 | deploy/helm/vmafx/templates/node-deployment.yaml and deploy/helm/vmafx/templates/node.yaml both rendered a Deployment named {{ include "vmafx.fullname" . }}-node in the same namespace under .Values.node.enabled=true. helm install rejected it with a duplicate-resource error, leaving Phase 4b distributed scoring (ADR-0709 / ADR-0713) uninstallable. Fix: keep node.yaml (probes + GPU resource injection + metrics port/Service + VMAFX_NODE_ID per ADR-0713) and fold the rclone Secret mount + VMAFX_STORAGE_MODE / VMAFX_RCLONE_CONFIG / VMAFX_VMAF_BINARY / VMAFX_MODEL_DIR env vars from the deleted node-deployment.yaml into it. | no ADR: only-one-way fix (CLAUDE §12 r8) | fix/helm-node-deployment-deduplicate | helm template deploy/helm/vmafx/ --set node.enabled=true renders without error; helm template … --set node.enabled=true \| grep -c '^ name: release-name-vmafx-node$' returns 1. | (2026-05-30) | | T-SYCL-ARC-FLOAT-ANSNR-STALE-ROW-2026-06-03 | float_ansnr row in .workingdir2/cross-backend-arc-20260527/sycl_matrix.md was stale. The float_ansnr feature extractor (CPU + all GPU twins) was removed in PR #38 (commit 70ed8b3ce3). No ANSNR symbol exists in core/src/. The parity matrix row showing a 1.59e-4 divergence is therefore closed by construction. ADR-0985 stub reserved as part of Research-0985 investigation. See docs/research/0985-sycl-parity-divergence-2026-06-03.md §2. Open rows for float_ssim and ssimulacra2 parity on Arc A380 are tracked separately below (T-SYCL-ARC-FLOAT-SSIM-PARITY-2026-06-03 and T-SYCL-ARC-SSIMULACRA2-PARITY-2026-06-03). | Research-0985 | fix/sycl-float-ssim-ssimulacra2-parity-research (this PR) | grep -r "float_ansnr" core/src/ \| wc -l → 0 on master. | (2026-06-03) |
| T-LIBVMAF-SCORE-NEEDS-CTX-2026-05-31 | Closed: pkg/libvmaf.Scorer.Score + pkg/libvmaf.ScoreDirect now take context.Context as their first parameter, plumbed through every production call site. The subprocess path uses exec.CommandContext with a 2-second WaitDelay so a client disconnect (HTTP r.Context() or gRPC handler ctx) propagates SIGKILL to the vmaf binary instead of leaving it running to completion. The cgo direct path (ScoreDirect) checks ctx.Err() at frame boundaries and lets the deferred vmaf_close / vmaf_model_destroy clean up. Five call sites updated: cmd/vmafx-server/{http_server,grpc_server}.go, cmd/vmafx-controller/{http_server,grpc_server}.go, cmd/vmafx-node/executor.go; the MCP direct-cgo dispatcher (cmd/vmafx-mcp/impl_direct.go) passes context.Background() for now (MCP JSON-RPC ctx plumb is a separate ADR). New tests cover the subprocess-cancel-kills-PID path, the HTTP-client-disconnect path on both server + controller, the pre-cancelled context fast path, and the in-loop cancellation of ScoreDirect. | no ADR: bug fix per CLAUDE §12 r8 (closes T-LIBVMAF-SCORE-NEEDS-CTX deferred from ADR-0978) | fix/libvmaf-score-ctx | go test -race -count=1 ./pkg/libvmaf/... ./cmd/vmafx-server/... ./cmd/vmafx-controller/... ./cmd/vmafx-node/... all packages OK; go vet ./... clean. New tests: TestScore_CancelKillsSubprocess, TestScore_PreCancelledContext, TestScoreDirect_PreCancelledContext, TestScoreDirect_CancelDuringLoop, plus TestScoreHandler_ClientDisconnectKillsSubprocess on both server + controller. | (2026-05-31) | | T-GPU-RUNTIME-BUG-AUDIT-ROUND-26-2026-05-31 | Round-26 audit of the four fork-added GPU runtime backends (core/src/cuda, core/src/sycl, core/src/hip, core/src/metal) and the two shared GPU TUs (gpu_picture_pool.c, gpu_dispatch_env.c) found six init/teardown leaks. (1) cuda/drain_batch.c::drain_stream_ensure — cuCtxPopCurrent failure on the success path dropped to fail_after_pop which only NULL'd the stream pointer, leaking the drain stream we just created. (2) cuda/picture_cuda.c::vmaf_cuda_picture_alloc — the unwind at fail only freed priv, leaking the upload stream + ready/finished events + every device pointer already allocated by cuMemAllocPitch when a later allocation failed mid-loop. (3) cuda/common.c::vmaf_cuda_release — cuCtxPopCurrent or cuDevicePrimaryCtxRelease failure dropped to fail_after_pop and returned the error code, leaving the dlopen'd CudaFunctions table allocated for the process lifetime. (4) gpu_picture_pool.c::vmaf_gpu_picture_pool_init — the per-slot alloc loop OR-aggregated callback results and returned mid-initialised state with *pool populated. Callers (e.g. picture_sycl.cpp) saw a non-zero err, ran their own destructor on the wrapper, and leaked per-slot device memory + mutex + p->pic. Fixed by unwinding successful prior slots, destroying the mutex, freeing the slot array, and NULLing *pool. (5) sycl/common.cpp::vmaf_sycl_graph_register — when the lazy compute-queue create failed, the function returned -ENOMEM but had already pushed the extractor entry and incremented num_graph_extractors, leaving a registered extractor with no queue; every subsequent graph_submit asserted then dereferenced a null queue. Fixed by reversing the order. (6) sycl/dmabuf_import.cpp::vmaf_sycl_import_va_surface_readback — a SYCL exception from q->memcpy or q->wait() escaped with the VA image still mapped and still allocated. Wrapped the SYCL submits + wait in try/catch and ran vaUnmapBuffer + vaDestroyImage on the error path. Plus one cleanup: removed a stray // test trailing comment from sycl/common.cpp. Companion regression test test_gpu_picture_pool_partial_init (CPU-only, fast suite) drives the new unwind via pure-C stub alloc/free callbacks and was verified to fail on the pre-fix tree (pool_init must NULL out *pool on failure) and pass post-fix. | ADR-0982 | chore/gpu-runtime-bug-audit | meson test -C core/build --suite=fast — 72/72 pass; pre-fix test_gpu_picture_pool_partial_init fails on pool_init must NULL out *pool on failure; both parsers green (check-adr-numbering.sh, check-copyright.sh). | (2026-05-31) | | T-VMAFX-TUNE-GO-DEEP-BUG-AUDIT-2026-05-31 | Deep audit of cmd/vmafx-tune/ (Stage-1 Go CLI per ADR-0705 / ADR-0713) and its pkg/{report,bisect,encoder,ladder} dependencies, closing five distinct bugs. (1) JSON NaN propagation in bisect_samples — report.EmitJSON and cmd/vmafx-tune/cmd.emitSweepJSON previously sanitised only the top-level row floats, leaving []bisect.Sample declared as raw float64 fields in the wire shape; a single non-finite VMAF / bitrate / encode-time in a sample crashed json.MarshalIndent with "unsupported value: NaN" and broke the Python ↔ Go parser-parity invariant (AGENTS.md rebase-sensitive invariant #2). New public helper report.SanitizeBisectSamples walks the nested floats; mirrored coverage added in emitLadderJSON for Cloud + Hull + Renditions across BitratekBps, VMAF, TargetVMAF. (2) parseVMAFXMLMean accepted "NaN" / "+Inf" / "-Inf" — Go strconv.ParseFloat accepts those tokens without error, so a corrupt vmaf XML mean fed non-finite scores into bisect.Sample and from there into the JSON marshaller failure mode of bug 1. Parser now rejects non-finite means at the source. (3)–(4)–(5) Subprocess hang risk — every exec.Command in pkg/encoder (ffmpeg encode, ffprobe bitrate, codec discovery) and pkg/bisect (vmaf scoring) ran with no context and no timeout, so a hung child pinned the sweep forever. Now exec.CommandContext with per-stage upper bounds overridable via VMAFX_TUNE_ENCODE_TIMEOUT (default 60m), VMAFX_TUNE_SCORE_TIMEOUT (default 30m), VMAFX_TUNE_PROBE_TIMEOUT (default 30s). (6) Codec-discovery cache stale-key — the previous sync.Once gate locked in whichever ffmpeg binary path was probed first, with _ = ffmpegBin masquerading as cache invalidation. Cache key is now the binary path; calling with a different path triggers a re-probe and replaces the cache. Eight new Go regression tests (TestEmitJSON_NaNInBisectSamplesDoesNotCrash, TestEmitLadderJSON_NaNInHullAndRenditions, TestParseVMAFXMLMean_Rejects{NaN,PositiveInf,NegativeInf}, TestProbeAvailableCodecs_CacheRespectsBinaryPath, TestVMAFScoreFunc_DoesNotHangOnMissingBinary, TestProbeBitrateKbps_TimeoutDoesNotHang, plus env-var-default coverage). Existing pkg/encoder/discover_test.go updated to the new per-binary cache shape. | no ADR: bug fixes per CLAUDE §12 r8 | fix/vmafx-tune-go-audit-20260531 | go test -race -count=1 -timeout=180s ./cmd/vmafx-tune/... ./pkg/bisect/... ./pkg/encoder/... ./pkg/ladder/... ./pkg/report/... — all pass; go vet ./... clean; Python pytest tools/vmaf-tune/tests/test_bisect.py tools/vmaf-tune/tests/test_compare.py tools/vmaf-tune/tests/test_compare_rate_quality_sweep.py tools/vmaf-tune/tests/test_compare_no_bisect.py — 88/88 pass (verifies the schema-v1/v2 parser parity still holds across the Python emitter and Go consumer). | (2026-05-31) | | T-GOSEC-FINDINGS-FIX-SWEEP-V2-2026-06-01 | Re-run of the gosec static-security scan against the post-#505 / post-#509-close master tip (PR #509 had source conflicts with #505 on pkg/bisect/bisect.go + pkg/encoder/encoder.go; this v2 sweep applies the same fixes while preserving the ctx + per-stage timeout from #505). 38 raw findings — 10 in protoc-generated / cgo trampoline code (excluded via -exclude-generated), 26 in fork-original source. One real bug: cmd/vmafx-mcp/impl.go::describeModel joined caller-supplied name onto repo root and os.Stat-ed the result, allowing {"name": "../../../etc/passwd"} to escape libvmaf.AllowedRoots() — fixed by routing through libvmaf.ValidatePath, regression test added at cmd/vmafx-mcp/impl_gosec_test.go::TestDescribeModelRejectsTraversal. G306/G301 in cmd/vmafx-tune/cmd/compare.go::writeOutput tightened to 0o600 / 0o750. G104 unhandled outFile.Close() and os.Remove errors in runVmafScore now checked. The remaining 22 G204/G304 findings were verified false positives whose existing //nolint:gosec suppressions were silently ignored by gosec — rewritten to // #nosec G<rule> -- ... per CLAUDE §12 r12. CI gate: new gosec -exclude-generated -quiet ./... step in .github/workflows/go-ci.yml. The bisect.VMAFScoreFunc + encoder.runEncode / probeBitrateKbps paths keep their PR #505 exec.CommandContext + per-stage timeout, just with the #nosec G204 citation attached. | ADR-0983 / Research | chore/gosec-findings-fix-v2 | gosec -exclude-generated -quiet ./... returns 0 findings on the fix tree (was 26); go vet ./... and go build ./... clean; go test ./cmd/vmafx-mcp/ -run TestDescribeModel -v 5/5 pass. | (2026-06-01) | | T-SCHEDULER-NODE-BUG-AUDIT-ROUND1-2026-05-31 | Deep-dive audit of cmd/vmafx-controller/scheduler/ + cmd/vmafx-node/ (distributed scheduling core, Phase 4b.1 / 4b.4) found four reachable defects across the scoring-node + online-feedback paths. (1) cmd/vmafx-node/online_feedback.go::FeedbackClient documented a Close() method on the constructor ("Call Close() to stop the drainer.") that did not exist — the drainer could only be stopped by cancelling the constructor's ctx. Any caller that took the doc at face value and called fc.Close() would have failed to compile. Added Close() that cancels an internal sub-context and synchronously waits on a done channel; multi-Close is idempotent, post-ctx-cancel Close still drains cleanly. (2) FeedbackClient accepted a nil *slog.Logger but the drainer's first log.Warn (on dial failure) and log.Debug (on queue overflow drop) dereferenced it — verified var l *slog.Logger; l.InfoContext(...) panics on stock Go 1.24. Constructor now substitutes slog.Default() when log is nil. (3) cmd/vmafx-node/executor.go::NewExecutor had the same nil-logger trap; the existing TestExecutor_ScoringJobFailsWithBadBinary passes nil to the constructor but skips on hosts where libvmaf.New("false","") cannot resolve the false binary via PATH lookup, hiding the NPE. Constructor now substitutes slog.Default(). (4) classifyJob's "AI heuristic" inline comment said "only one path is set and no model is set" but the code requires the model to be set (the test TestExecutor_AIJobUnsupportedStage1 agrees with the code, not the comment); rewrote the comment to describe the actual behaviour. Test-only follow-up: TestAssignJobBackToQueueOnNodeDisconnect constructed two nodes.Registry instances without calling Close(), leaking two reaper goroutines per test run (the newFixture helper used by the other scheduler tests already had the cleanup pattern). Added matching defer r.Close() calls. Four new regression tests in cmd/vmafx-node/online_feedback_test.go (drainer-stop, idempotent multi-Close, nil-logger send + drop path, ctx-cancel + Close interaction) plus one new test in executor_test.go lock in the nil-logger guard for Executor. | no ADR: bug fixes per CLAUDE §12 r8 | fix/scheduler-node-audit-round1 | go test ./cmd/vmafx-controller/scheduler/... ./cmd/vmafx-node/... -race -count=5 → 4/4 packages pass on all 5 iterations; go vet and go build ./... clean. | (2026-05-31) | | T-TEST-SVM-PARSER-LINK-PLUS-OPERATOR-AUDIT-2026-05-31 | Bundled fix: (a) pre-existing master-tip link break in test_svm_parser (the Meson executable's source list omitted ../src/thread_locale.c, so svm.cpp::svm_load_model / svm_save_model left vmaf_thread_locale_push_c / vmaf_thread_locale_pop as undefined references at link time — the sibling test_svm_api target already lists the file, proving the precedent); and (b) deep audit of cmd/vmafx-operator/ (kubebuilder/controller-runtime k8s operator under Phase 4b). Operator findings: (1) VmafxNode.probeHealthz deferred resp.Body.Close() without draining the body, so each 30-second probe per VmafxNode tore down the TCP connection instead of returning it to Go's HTTP keep-alive pool; fixed by io.Copy(io.Discard, resp.Body) before Close. (2) CRD integer fields used Go int — Kubernetes API conventions require int32 because OpenAPI v3 has no architecture-dependent integer type; widened five fields across the three CRDs. (3) Documented defaults (backend=cpu, priority=0, capacity=1, checkpoint.interval=10m, checkpoint.minSamples=1000) were prose-only — added +kubebuilder:default: markers on the Go types and default: keys in the CRD schemas; resynced deploy/helm/vmafx/crds/ copies per cmd/vmafx-operator/AGENTS.md invariant #2. Three new standalone Go regression tests (TestProbeHealthzDrainsBody, TestProbeHealthzNon200StillDrains, TestProbeHealthzTransportErrorReturnsFalse) gate the body-drain fix without requiring envtest binaries. | no ADR: bug fixes per CLAUDE §12 r8 | fix/test-svm-parser-link-plus-operator-audit | meson test -C core/build-cpu test_svm_parser test_svm_api — 2/2 pass (pre-fix: link error on test_svm_parser); go test -race -run TestProbeHealthz ./cmd/vmafx-operator/internal/controller/ — 3/3 pass; go vet ./... and go build ./... clean. | (2026-05-31) | | T-CORE-LIFECYCLE-MEMORY-AUDIT-2026-05-31 | Deep-dive audit of core/src/ core lifecycle + memory surfaces (picture.c, picture_pool.c, mem.c, ref.c, dict.c, log.cpp, opt.c, model.{c,cpp}, output.c, feature/feature_collector.cpp, predict.c, thread_locale.c) found eight reachable defects. (1) picture_pool.c::pool_preallocate_pictures cleanup loop called vmaf_picture_unref on previously-prepared pool slots whose priv/ref had already been cleared — the unref short-circuited and left the data buffer leaked for every successful slot. Fixed by aligned_free-ing data[0] directly. (2) model.{c,cpp}::vmaf_model_load + vmaf_model_collection_load passed version straight into strcmp without a NULL guard. (3) predict.c::piecewise_segment_apply + piecewise_linear_mapping returned bare positive EINVAL instead of the negated convention used everywhere else; the sign inverted on any caller that propagated the value. (4) predict.c::transform ignored piecewise_linear_mapping's return value and silently overwrote the prediction with the default y_out = 0 on knot validation failure. (5) predict.c::predict_ensure_caches returned the resolver error without rolling back the partially populated predict_feature_names table, so subsequent calls skipped re-init and dereferenced NULL holes. (6) feature_collector.cpp::aggregate_vector_append returned -EINVAL on malloc failure where -ENOMEM is required. (7) output.c::vmaf_write_output_csv + vmaf_write_output_sub lacked the NULL-fc / NULL-outfile guards the sibling XML/JSON writers gained under ADR-0602 — NULL caller crashed instead of returning -EINVAL. (8) dict.c::dict_normalize_numeric used strtof but stored into double and reformatted via %g, silently truncating any value with more than ~7 decimal digits of precision; switched to strtod. Companion regression tests added to test_predict, test_model, test_output. | no ADR: bug fixes per CLAUDE §12 r8 | fix/core-lifecycle-memory-audit | meson test -C core/build-cpu --suite=fast — 29/29 pass (test_svm_parser excluded; pre-existing master link break unrelated). New tests test_csv_sub_einval_guards, test_piecewise_linear_mapping_returns_neg_einval, test_model_load_rejects_null_version all pass. | (2026-05-31) | | T-VMAFX-SERVER-BUG-AUDIT-2026-05-31 | Deep-dive audit of cmd/vmafx-server/ (Go gRPC + HTTP scoring service, ADR-0703) + pkg/score/ (gRPC client wrapper, ADR-0933) found four reachable defects + one defensive cleanup. (1) pkg/observability.NewShutdownContext spawned a goroutine blocked on <-ch with no <-ctx.Done() arm — calling the returned stop() cancelled the parent context but did not unblock the goroutine nor signal.Stop(ch). Each NewShutdownContext / stop() cycle that exited via stop() first (e.g. any os.Exit(1) path in cmd/vmafx-server/main.go that skipped defer stop()) leaked one goroutine + one signal-handler subscription for the process lifetime. (2) pkg/score.Client.OpenScoreStream + ScoreStream.PushFrame wrapped a bare io.EOF from Send instead of draining Recv to retrieve the server's real gRPC status — so a malformed StreamConfig (zero dimensions / wrong oneof, validated by cmd/vmafx-server/grpc_server.go:107-138) surfaced as "send StreamConfig: EOF" rather than the underlying InvalidArgument. (3) cmd/vmafx-server/http_server.handleScore used json.NewDecoder(r.Body).Decode(&req) with no body cap; an unauthenticated POST with a multi-GB body could balloon the decoder's read buffer until OOM. (4) cmd/vmafx-server/grpc_server had no panic-recovery interceptor; a panic in any handler (notably the cgo libvmaf call path) would tear down the worker goroutine and crash the entire server process. (5) Defensive: pkg/score.Recv compared err == io.EOF instead of errors.Is(err, io.EOF). Fixed: NewShutdownContext delegates to signal.NotifyContext; OpenScoreStream + PushFrame use a new recvStatusOnEOF helper that drains Recv on Send-EOF and surfaces the real status; handleScore uses http.MaxBytesReader at 1 MiB and maps *http.MaxBytesError to HTTP 413; runGRPC installs recoveryUnaryInterceptor + recoveryStreamInterceptor that translate panic into codes.Internal; Recv uses errors.Is. Out of scope (deferred to a separate PR — would force a multi-package signature change touching pkg/libvmaf + cmd/vmafx-controller + cmd/vmafx-node): scorer.Score() takes no context.Context so the vmaf CLI subprocess keeps running after a client disconnects — tracked as T-LIBVMAF-SCORE-NEEDS-CTX-2026-05-31 (Open). | ADR-0978 | fix/vmafx-server-audit | CGO_ENABLED=1 go test -race -count=1 -tags cgo ./cmd/vmafx-server/... ./pkg/score/... ./pkg/observability/... all packages OK; go vet clean. Six new regression tests cover each fix. | (2026-05-31) | | T-CORE-TOOLS-INPUT-READER-SAFETY-2026-05-31 | Deep-dive audit of core/tools/ (CLI binary, bench binary, vendored Daala YUV/Y4M readers) found three real defects. (1) y4m_input_open_impl ignored failed malloc() returns — dst_buf = NULL was surfaced to the caller as success, and the next video_input_fetch_frame called fread(NULL, …) faulting inside libc as SIGSEGV. (2) Both y4m_input.c and yuv_input.c computed dst_buf_sz in int / unsigned precision and assigned to size_t, wrapping for headers near the 32-bit ceiling — malloc then succeeded with a too-small buffer and the first fread overflowed the heap allocation. The 4:4:4 paths in y4m_input.c already cast (lines 736 / 743 / 779), proving the precedent. (3) vmaf_bench::bench_feature leaked VmafCudaState / VmafSyclState on every success and most error paths — the state pointers were local to #ifdef blocks. The companion run_feature_collect in the same TU was already fixed under the T5 state-leak audit. Fixed: malloc-NULL check with partial-allocation cleanup; (size_t) cast before multiply in both readers; bench_feature gained function-scope GPU state pointers + a bench_cleanup label. New test_y4m_alloc_failure (POSIX-only, fast suite) drives the parser against a 65535×65535 4:4:4 12-bit header with RLIMIT_AS clamped below the demanded buffer size — verified to fail on pre-fix tree, pass post-fix. | ADR-0977 | fix/core-tools-audit | meson test -C core/build --suite=fast → 71/71 pass; pre-fix test_y4m_alloc_failure failed (exit 1, "regression of dst_buf-NULL bug"), post-fix passes. | (2026-05-31) | | T-MASTER-CI-VERIFIED-2026-05-31 | Two master CI regressions on tip 4948b771c, both verified locally in the vmaf-dev-mcp container before being patched (no guessing). (1) test_metal_float_ms_ssim_parity failing on all 3 macOS jobs with CPU: vmaf_read_pictures failed — fixture FIXTURE_H = 144u was below the 5-level 11-tap MS-SSIM min_dim = 176 floor (core/src/feature/float_ms_ssim.c:131-138, Netflix#1414 / ADR-0153), so CPU init returned -EINVAL before the Metal path ran. Fix: bump FIXTURE_H to 192u. Reproduced via the production CLI (vmaf --feature float_ms_ssim on a 256×144 zero-YUV) since the Metal test itself does not build on Linux. (2) test_ssimulacra2_simd::test_xyb failing on Linux all-backends (icpx 2025.3) with linear_rgb_to_xyb SIMD not bit-identical to scalar — icx ignores both -ffp-contract=off and -fp-model=precise for inline scalar code and emits vfmadd231ps for the kM00*r + m01*g + kM02*b + kOpsinBias chain, diverging from the AVX2 SIMD lib whose explicit _mm256_mul_ps + _mm256_add_ps intrinsics emit no FMA. Reproduced under icx 2026.0.0 in container (test_xyb: fail after test_multiply: pass); 242 vfmadd instructions confirmed in icx -S output. Fix: add file-scope #pragma clang fp contract(off) (with -Wunknown-pragmas GCC suppression) to core/test/test_ssimulacra2_simd.c — empirically the only mechanism that suppresses icx contraction. Production binaries unchanged; no score drift. | ADR-0973 / Research-0973 | fix/master-ci-regressions-verified-2026-05-31 | meson test -C core/build-fix --suite=fast 49/49 OK under GCC; ./core/build-icpx/test/test_ssimulacra2_simd 13/13 OK under icx 2026.0.0. | (2026-05-31) | | T-TEST-GPU-PICTURE-POOL-CLEANUP-ROUND27-2026-05-31 | Round 27 audit bugs D.3 + D.4 in core/test/test_gpu_picture_pool.c. D.3: VmafCudaCookie initialiser set .state = malloc(sizeof(VmafCudaState)) before calling vmaf_cuda_state_init(&my_cookie.state, cu_cfg); the function allocates internally through the double-pointer, so the pre-allocated block was unconditionally leaked on every CUDA-capable run. D.4: dead /* ... */ block containing test_ring_buffer_threaded had two latent compilation errors (duplicate cfg declaration; missing & in vmaf_cuda_state_init call) and was never active — it was copied verbatim from a pre-ADR-0239 prototype in the original file creation commit and never fixed or activated. Fixed by removing the unused malloc (D.3) and deleting the dead block entirely (D.4). | ADR-0970 | fix/test-gpu-picture-pool-cleanup | meson test -C core/build-cpu --suite=fast — 49/49 pass; assertion-density.sh PASS; clang-format --dry-run -Werror clean. | (2026-05-31) | | T-METAL-KERNEL-PARITY-ROUND3-2026-05-31 | PR #351 closed the Metal extractor registration audit (8 extractors discoverable). PR #379 added per-kernel parity tests for the 4 highest-priority extractors (motion_v2, integer_psnr, float_psnr, float_ssim). This round-3 PR fills the remaining 4 gaps: adds parity tests for integer_motion, float_motion, float_moment (4 output keys), and float_ms_ssim. With this round all 8 registered Metal extractors now have a real per-kernel CPU-vs-Metal score gate, eliminating the risk that a future change to a .metal shader or its .mm host bridge silently drifts the score on Apple Silicon CI. Each test runs a synthetic 256x144 YUV420P fixture through both the CPU twin and the Metal extractor and asserts places=4 (1e-4) parity per ADR-0214 — except float_ms_ssim which uses the 1e-3 SSIM-family bound from ADR-0589. Each test skips cleanly via -ENODEV on Linux / Windows / Intel Mac, runs the live kernel on Apple-Family-7+ macOS CI lanes. Wired under the existing enable_metal guard in core/test/meson.build with suite : ['fast', 'gpu']. | no ADR: test-only addition (follow-up to PR #351 / PR #379; tolerances cite existing ADR-0214 / ADR-0589) | test/metal-kernel-coverage-round3 | meson setup core/build-cpu -Denable_cuda=false -Denable_sycl=false -Denable_metal=disabled && ninja -C core/build-cpu (CPU-only build green, new tests gated off on non-Metal). On macOS Apple-Family-7+ CI: meson test -C build --suite=fast test_metal_integer_motion_parity test_metal_float_motion_parity test_metal_float_moment_parity test_metal_float_ms_ssim_parity exercises the live kernels. | (2026-05-31) | | T-MCP-HTTP-NO-AUTH-2026-05-31 | Round 26 audit finding A.1 — MCP HTTP transport shipped three security gaps: (1) no body size limit on _handle_score, (2) no Authorization / token enforcement, (3) default bind 0.0.0.0. Fixed by adding a security middleware that enforces a 4 MiB body limit (Content-Length pre-flight + client_max_size) and Bearer token auth (fail-closed: server rejects all requests with 401 when VMAFX_MCP_HTTP_TOKEN is unset and VMAFX_MCP_HTTP_NO_AUTH is not set). Default bind changed to 127.0.0.1; override via VMAFX_MCP_HTTP_BIND=0.0.0.0. Optional TLS via VMAFX_MCP_HTTP_TLS_CERT + VMAFX_MCP_HTTP_TLS_KEY. Unix-domain-socket / stdio transport unaffected. | ADR-0967 | fix/mcp-http-transport-security | 23/23 tests pass (pytest mcp-server/vmaf-mcp/tests/test_http_transport.py). Breaking change: existing deployments relying on 0.0.0.0 default must add VMAFX_MCP_HTTP_BIND=0.0.0.0. | (2026-05-31) | | T-HIP-KERNEL-COVERAGE-ROUND4-2026-05-31 | HIP kernel parity-test coverage round 4 closes 2 of the 4 originally-planned reachable kernels after PR #443 / ADR-0945: ssimulacra2_hip and float_ssim_hip. Each new test under core/test/ follows the round-1/2/3 template — synthetic 256×144 YUV420P fixture, CPU reference vs. HIP score, skip cleanly with [skip: no HIP device] or [skip: HIP scaffold ENOSYS]. Tolerance places=3 (1e-3) for both (multi-scale pyramid + windowed SSIM pooling sit at the MS-SSIM rounding budget per ADR-0214). The round-3 deferral table mis-claimed the speed-family lacked CPU twins; verified incorrect — vmaf_fex_speed_chroma (core/src/feature/speed.c:1335) and vmaf_fex_speed_temporal (line 1559) ship as stable extractors. However, wiring speed_chroma_hip.c / speed_temporal_hip.c into the HIP archive surfaced a pre-existing latent link defect (see T-HIP-SPEED-INTERNAL-IMPL-MISSING-2026-05-31 in Open). HIP parity coverage lifts from 13/17 → 15/17 (76% → 88%). float_moment_hip remains deferred. | ADR-0958 | test/hip-kernel-coverage-round4 | Both tests build inside vmaf-dev-mcp:cuda13.3 (enable_hip=true enable_hipcc=false) and exercise the [skip: no HIP device] path on hosts without an AMD GPU; full path runs on AMD-equipped CI. | (2026-05-31) | | T-METAL-KERNEL-PARITY-ROUND4-2026-05-31 | Metal kernel parity coverage round 4 — closeout. Round 1 (PR #351) seeded the registration audit for all 8 Metal extractors; rounds 2 (PR #379) and 3 (PR #447) added per-kernel CPU-vs-Metal score parity tests for motion_v2/integer_psnr/float_psnr/float_ssim and integer_motion/float_motion/float_moment/float_ms_ssim respectively. Round 4 ships core/test/test_metal_kernel_coverage_audit.c — a structural regression guard that enumerates the 8 expected .mm kernel basenames under core/src/feature/metal/ and asserts each has a registered <basename>_metal extractor + a row in vmaf_metal_dispatch_supports. Defends against silent gaps the day a 9th kernel ships without registration / dispatch / parity test. Build-wiring audit confirmed all 8 .mm + all 8 .metal sources are referenced in core/src/metal/meson.build — no dormant scaffolds. Sibling of PR #464 (CUDA round 4) + PR #465 (SYCL round 4) audit-by-enumeration precedent. | ADR-0959 / Research-0959 | test/metal-kernel-coverage-round4 | CPU-only build green; meson test -C build test_metal_kernel_coverage_audit runs all 4 sub-tests (registration probe + dispatch probe + phantom-name reject + count cross-check); dispatch + phantom sub-tests skip with -ENODEV on non-Apple-Family-7 hosts. | (2026-05-31) | | T-CUDA-SPEED-TU-REPAIR-2026-05-31 | speed_chroma_cuda.c + speed_temporal_cuda.c: replaced legacy CHECK_CUDA(...) with CHECK_CUDA_GOTO(..., fail), replaced cuMemAllocHost with cuMemHostAlloc(..., 0x01u) (the actual CudaFunctions table member), fixed copyright headers, wired both TUs into core/src/meson.build if is_cuda_enabled, added extern + registry rows in feature_extractor.c under #if HAVE_CUDA. | ADR-0965 | fix/cuda-speed-tu-repair | meson test -C build-cuda test_cuda_speed_chroma_parity test_cuda_speed_temporal_parity passes (or skips cleanly when no CUDA device visible). | (2026-05-31) | | T-MCP-STOP-DOUBLE-JOIN-SEGV-2026-05-31 | vmaf_mcp_stop() SIGSEGV on its third (or any second-after-no-start) invocation. Flagged by PR #460 audit, Known follow-ups item 5. Root cause: atomic_exchange(running, 2) was unconditional on each of the three transport *_running atomics, mutating 0 -> 2 silently on never-started transports; the join branch guard fired for both 1 and 2, so the next stop() call re-entered the branch and invoked pthread_join() on a default-initialised or already-joined pthread_t (UB; SIGSEGV on glibc 2.40). Fix: replace each exchange + dual-value guard with atomic_compare_exchange_strong(expected=1, desired=2) so the join branch fires exactly once per started transport, matching the CAS pattern already used by vmaf_mcp_start_{stdio,uds,sse}. Regression test core/test/test_mcp_stop_idempotent.c exercises triple-stop with and without an active stdio transport. | no ADR (bug fix per CLAUDE §12 r8) | fix/vmaf-mcp-stop-double-join-segv | meson test -C build-mcp-fix test_mcp_stop_idempotent — 2/2 pass; meson test -C build-mcp-fix test_mcp_smoke — 18/18 still pass. | (2026-05-31) | | T-COMPAT-PYTHON-VMAF-SCANF-LOCALE-2026-05-31 | Two latent bugs in upstream-mirror compat/python-vmaf/. (1) tools/scanf.py::makeFormattedHandler.applyWidth had inverted if width is None guard — implicit-width converters (%d / %f / %s / %x) detonated inside CappedBuffer with TypeError: '<' not supported between instances of 'int' and 'NoneType'; explicit-width converters (%5d) silently dropped the cap. (2) __init__.py::ProcessRunner.run set C locale via env.setdefault("LC_ALL", "C") / env.setdefault("LANG", "C") — a no-op on hosts with LANG=de_DE.UTF-8, defeating the locale-forcing intent and producing locale-translated subprocess errors. Fix: swapped the scanf branches; replaced both setdefault calls with unconditional assignment. Embedded scanf test suite improves from 7 errors to 1 (the remaining error is an unrelated file-seek limitation pre-existing on master). | ADR-0955 | fix/compat-python-vmaf-scanf-locale-bugs | pytest python/test/python_harness_scanf_locale_bugs_test.py -v → 11/11 pass. Pre-fix reproducer: PYTHONPATH=python python -c "from vmaf.tools import scanf; scanf.sscanf('42', '%d')" raises TypeError; post-fix returns (42,). | (2026-05-31) | | T-VENDORED-LIBSVM-IQA-COVERAGE-2026-05-31 | Test coverage on vendored libsvm 3.24 runtime API (core/src/svm.cpp) and the IQA helper tree (core/src/feature/iqa/*.c) was 9.6% / 0–41% on master. Two new fast-suite executables — core/test/test_svm_api.c (8 tests; svm_train + inspector family + svm_check_parameter rejection branches not covered by PR #381's parser tests + svm_predict / svm_predict_values / svm_predict_probability + EPSILON_SVR + svm_save_model/svm_load_model round-trip) and core/test/test_iqa_helpers.c (21 tests; math_utils + KBND_ + iqa_filter_pixel + iqa_img_filter + iqa_decimate + iqa_ssim end-to-end). Observation-only — no vendored source modified; libsvm NOLINTBEGIN/NOLINTEND cordon byte-identical. Coverage: svm.cpp 9.6% → 71%; iqa aggregate 16.7% → ≈95%; combined ≈14% → 74% lines. 51/51 fast tests pass. | ADR-0952 | test/vendored-libsvm-iqa-coverage | meson test -C build-cov --suite=fast → 51/51 pass; gcovr -r .. -f '.*svm\.cpp' -f '.*feature/iqa/' reports 74% / 78% / 54.6% (lines / fns / branches) vs 14% / <20% / 8% baseline. | (2026-05-31) | | T-HIP-ADM-PARITY-FEATURE-NAME-AND-ENOSYS-SKIP-2026-05-31 | core/test/test_hip_adm_parity.c had two defects flagged during the PR #451 (motion3 sibling) audit. (1) Symmetric feature-name bug — both CPU and HIP branches called run_extractor("adm", ...), so the HIP comparand silently re-ran the upstream CPU adm extractor; the ADR-0539 integer-ADM CPU-vs-HIP parity claim was not enforced by its named gate. (2) Missing -ENOSYS skip predicate for the enable_hip=true, enable_hipcc=false posture — the adm_hip extractor's init/submit hits the #ifndef HAVE_HIPCC scaffold path and returns -ENOSYS, which the blanket mu_assert("vmaf_read_pictures failed", !err) would have surfaced as a hard test failure once the feature-name was fixed. Fix: switched the HIP call site to "adm_hip"; extracted an adm_submit_one_frame() helper that tears down the partially-initialised context on -ENOSYS and emits [skip: HIP kernels not built (enable_hipcc=false)]. Mirrors the ADR-0949 pattern. | ADR-0950 | fix/test-hip-adm-parity | Container build (enable_hip=true, enable_hipcc=false, enable_cuda=false, enable_sycl=false against vmaf-dev-mcp:local): pre-fix ./build-hip-fix/test/test_hip_adm_parity reported [skip: no HIP device] pass for the wrong reason (CPU re-run on both sides); post-fix it reports [skip: no HIP device] pass cleanly via the runtime-init guard. On a HIP-visible host, the test will now also catch the -ENOSYS path and skip rather than hard-fail. | (2026-05-31) | | T-HIP-MOTION3-PARITY-ENOSYS-SKIP-2026-05-31 | test_hip_motion3_parity reported hard failure (HIP: vmaf_read_pictures failed) on meson test --suite gpu against any enable_hip=true, enable_hipcc=false build — flagged by the PR #443 audit as a pre-existing bug. Root cause: the test's skip predicate only matched vmaf_hip_state_init() failure (no HIP runtime / no device visible), but the fork's HIP build is two-axis. enable_hipcc=false builds embed no HSACO device kernels, so the runtime vmaf_hip_state_init succeeds yet the first vmaf_read_pictures invokes motion_hip's init_fex_hip / submit_fex_hip which hit #ifndef HAVE_HIPCC and return -ENOSYS. Fix: extract hip_submit_one_frame helper, match -ENOSYS on first-frame submit, tear down the partially-initialised VmafContext + VmafHipState, emit [skip: HIP kernels not built (enable_hipcc=false)], pass. CPU baseline + places=4 tolerance unchanged. Real parity failures still surface loudly when kernels are compiled. | ADR-0949 | fix/test-hip-motion3-parity | docker exec hip-fix-build bash -c 'cd /wt && meson configure build-hip-fix -Denable_hipcc=false && ninja -C build-hip-fix test/test_hip_motion3_parity && ./build-hip-fix/test/test_hip_motion3_parity' → test_motion3_cpu_hip_parity: [skip: HIP kernels not built (enable_hipcc=false)] pass; 1 tests run, 1 passed. Pre-fix same command on master 31dce40e1 → [fail], HIP: vmaf_read_pictures failed. | (2026-05-31) | | T-SIMD-BIT-EXACT-ROUND2-2026-05-30 | Round-2 follow-up to PR #339: master tip 83698bd5b2 still reproduced test_ms_ssim_decimate (scalar reference inside libvmaf_feature_static_lib did not get -fp-model=precise, while the SIMD carve-out lib did — the round-1 fix scoped the flag to the SIMD libs only) and test_ssimulacra2_simd::test_ptlr_420_8 (icx auto-fused the AVX2/AVX-512 main-loop _mm256_add_ps(_, _mm256_mul_ps(_, _)) colour-matrix to FMA despite -fp-model=precise, while the gcc-built scalar reference used non-fused mul+add). Fix: (a) extend -fp-model=precise to libvmaf_feature_static_lib + libvmaf_ssimulacra2_static_lib via a new _libvmaf_feature_icx_args helper in core/src/meson.build; (b) unify SSIMULACRA 2 picture_to_linear_rgb on FMA across implementations — _mm256_fmadd_ps / _mm512_fmadd_ps in the main loops, fmaf() in the scalar tails and the test_ssimulacra2_simd.c reference. All compilers (gcc, clang, icx) now perform single-rounded FMA. | ADR-0891 | fix/simd-bit-exact-round2 | All 49/49 fast+simd tests pass locally under gcc 16; test_ms_ssim_decimate, test_ssimulacra2_simd, test_psnr_hvs_simd all green (10/10, 13/13, 5/5 subtests). | (2026-05-30) | | T-FEATURE-COVERAGE-ROUND2-2026-05-31 | Seven CPU-side files under core/src/feature/ carried 17 %-80 % line coverage versus the ADR-0114 90 % per-file trajectory: integer_motion.c (71.8 %), integer_motion_v2.c (17.6 %), integer_psnr.c (70.1 %), integer_vif.h (72.2 %), iqa/convolve.c (41.2 %), barten_csf_tools.h (45.5 %), ms_ssim_decimate.c (80.4 %). Round 1 (PR #344) owned a disjoint 4-file set. Round 2 ships 7 new test_*_coverage.c executables in core/test/meson.build that exercise option-driven init paths (min_sse, enable_apsnr, motion_force_zero, motion_moving_average, motion_five_frame_window), HBD extract for 10/12/16-bit, multi-frame extract+flush with manually-set prev_ref, the integer_vif log2 LUT helpers, the iqa boundary helpers (KBND_*), every supported (resolution, distance) pair of barten_watson_blend_csf* plus the -EINVAL fallthrough, and the ms_ssim_decimate runtime-dispatch wrapper. Post-fix coverage: 48 %-100 % (six of seven files ≥ 88 %). | ADR-0938 / Research-feature-extractor-coverage-round2 | test/core-feature-coverage-round2 | meson setup build-cov core -Db_coverage=true -Denable_cuda=false -Denable_sycl=false && ninja -C build-cov && meson test -C build-cov --suite=fast --suite=simd --suite=dnn — 68/68 green. | (2026-05-31) | | T-PRE-EXISTING-TEST-FAILURES-2026-05-30 | 23 pre-existing test failures across three packages, flagged by prior audits. (A) ai/tests/ — 22 tests (19 runtime + 3 collection) failed with RuntimeError: operator torchvision::nms does not exist because the installed torchvision-0.26.0 wheel was ABI-incompatible with torch-2.12.0 (matched wheel pair is torchvision-0.27.0); the chain is pytorch_lightning → torchmetrics.functional.image.arniqa → torchvision.transforms, raised at module-load time. pytest.importorskip catches only ImportError, not RuntimeError, so the failure was a hard collection error. Fix: added _probe_pytorch_lightning() + cached _PYTORCH_LIGHTNING_ERROR + requires_pytorch_lightning() helper in ai/tests/conftest.py that catches the broader Exception and routes a clean pytest.skip(allow_module_level=True) with the actual error string surfaced; 9 affected test files opt in. Deployment-side fix pip install -U torchvision keeps working unchanged. (B) tools/vmaf-tune/tests/ — 4 tests failed because DEFAULT_SAMPLER_CRF_SWEEP started at CRF 18, below SvtAv1Adapter.quality_range = (20, 50) Phase A lower bound (Bug N-2); corpus.iter_rows called adapter.validate(preset, 18) which raised pre-encode and ladder --encoder libsvtav1 exited 2. Fix: shifted sweep to (20, 25, 30, 35, 40) so the same 5-point grid is valid for every shipped adapter; updated the synthetic R-D test in test_ladder.py to match. (C) mcp-server/vmaf-mcp/tests/ — test_metrics_returns_200 failed because aiohttp 3.13.5 added strict validation rejecting a charset=... fragment in the content_type= kwarg, and prometheus_client.CONTENT_TYPE_LATEST is text/plain; version=1.0.0; charset=utf-8. Fix: pass the header via headers={"Content-Type": ...} so aiohttp does not re-parse it (matches PR #346 pattern); added test_metrics_full_content_type_header_preserved to pin the wire-level invariant. | no ADR: bug-fix per CLAUDE §12 r8 | fix/pre-existing-test-failures | pytest ai/tests/ tools/vmaf-tune/tests/ mcp-server/vmaf-mcp/tests/ — 2141 passed, 31 skipped (was 23 failed before this PR). | (2026-05-30) | | T-TEST-PIXEL-FORMAT-EDGE-COVERAGE-20260531 | The CPU extractor surface had no end-to-end unit-test coverage on YUV422P input (any extractor), at 12 bpc (any extractor), or on 4:4:4 + HBD (test_picture_pool_yuv444 runs the full VMAF model, not a single extractor). Bugs in picture_compute_geometry chroma-stride or HBD scoring loops were caught only by the Python harness or cross-backend parity gate. Added core/test/test_pixel_format_edge_coverage.c — five fast (~40 ms total) smoke tests through the public extractor surface: PSNR on YUV422P 8-bit, PSNR on YUV444P 10-bit, PSNR on YUV420P 12-bit, SSIM on YUV422P 8-bit, CIEDE on YUV422P 8-bit (the last exercises ciede.c::init's chroma-upscale scratch allocation that the 4:4:4 fast-path bypasses). All five pass; the 50-test fast suite stays green. | ADR-0912 / Research-0912 | test/pixel-format-edge-coverage | meson test -C core/build-cpu --suite=fast — Ok: 50, Fail: 0. | (2026-05-31) | | T-CHANGELOG-RENDERER-SPLICE-AND-DRIFT-2026-05-31 | scripts/release/concat-changelog-fragments.sh used ^## [^[] as the "end of Unreleased block" sentinel; 84 of 102 in-tree fragments contained ## headers in their bodies, tripping the sentinel and inflating CHANGELOG.md by ~3 kB per --write cycle. Master tip 544299fae1 had drifted to 59 757 lines (~95 % duplicated). Fixed by anchoring the boundary regex on ^## \[ (the bracketed ## [version] shape release-please always writes), normalising 102 fragments (drop redundant first-line section headers; demote remaining ## to ###), moving 32 fragments out of the silently-skipped changelog.d/perf/ + changelog.d/performance/ directories into changelog.d/changed/perf-*.md, and adding stderr WARNINGs for unknown subdirs / empty fragments. CHANGELOG.md regenerated to 15 030 lines (−44 727 lines); --write is idempotent; future ## [vX.Y.Z] release-please headers are preserved across re-renders. | ADR-0913 / Research-0913 | fix/changelog-renderer-and-drift | bash scripts/release/concat-changelog-fragments.sh --check → exit 0; bash scripts/release/concat-changelog-fragments.sh --write && bash scripts/release/concat-changelog-fragments.sh --check → idempotent. | (2026-05-31) | | T-BASH-STRICT-MODE-SWEEP-2026-05-30 | Bash strict-mode + trap-cleanup + locale-stable-sort sweep across 9 in-tree shell scripts. PR #318 (perf/release scripts) and PR #350 (dev-mcp-entrypoint, sycl-bench-env) had closed adjacent classes; this PR closes the residual gaps in scripts/run_unittests.sh (no strict mode at all → set -eu + guarded pipefail), scripts/ai/fetch-tiny-blobs.sh and dev/scripts/smoke-probe-loop.sh (mktemp without script-wide trap → _*_STAGING_FILES array + trap _cleanup EXIT INT TERM), scripts/ci/check-agent-worktree-drift.sh + its self-test (set -eu → set -euo pipefail), scripts/ci/check-adr-numbering.sh / scripts/ci/check-dispatch-registry.sh / scripts/adr/next-free.sh (added LC_ALL=C to filename-numeric sorts so the ADR-numbering and dispatch-registry collision checks are host-locale-independent), and tools/ensemble-training-kit/_platform_detect.sh (added inline comment documenting why no top-level set -euo pipefail — sourced helper would clobber caller shell options). | ADR-0899 / Research | fix/bash-strict-mode-sweep | bash scripts/adr/test-next-free.sh (12/12), bash scripts/ci/test_check_agent_worktree_drift.sh, bash tools/ensemble-training-kit/tests/test_platform_detect.sh (16/16), shfmt -d -i 2 -ci <9 files> clean. | (2026-05-30) | | T-LIBSVM-VENDORED-AUDIT-ROW-ORDERING-OOB-2026-05-30 | Vendored libsvm 3.24 (core/src/svm.cpp) audit: per-row Malloc(...) calls in parse_header() (rho, label, probA, probB, nr_sv) sized their allocation from model->nr_class but did not assert that nr_class had already been parsed. A crafted model whose header places any of those rows before the nr_class row would therefore Malloc(_, 0) and leave the pointer attached to a zero-size allocation that downstream svm_predict_values / svm_predict_probability dereferences as an array (UB; not exploitable under glibc which returns a non-NULL one-byte allocation for malloc(0), but a SIGSEGV risk on stricter allocators). Fix: added exceptAssert(model->nr_class > 0, ...) precondition before each Malloc, inside the existing NOLINTBEGIN-cordoned vendor block. Audit also confirmed no CVE-grade fix from upstream libsvm 3.25 – 3.36 needs backport (no libsvm CVEs filed since 3.24; upstream FD-cleanup is moot under the fork's SVMModelParser<> RAII refactor); pin remains 3.24 + three fork patches (thread-locale ADR-0137, JSON entry point, MALLOC-OOB hardening). Lands a 9-case fork-local regression suite at core/test/test_svm_parser.c (suite fast) plus a core/src/AGENTS.md invariant section documenting the three fork-patch families so future sync attempts cannot silently regress them. | ADR-0889 / Research-0889 | chore/libsvm-vendored-audit | meson test -C core/build test_svm_parser test_predict test_model — 9/9 + 4/4 + 42/42 pass. | (2026-05-30) | | T-METAL-KERNEL-PARITY-ROUND2-2026-05-30 | PR #351 closed the Metal extractor registration audit but left an explicit follow-up gap: the eight registered extractors had no per-kernel CPU-vs-Metal score comparison. Without parity tests, a future change to a .metal shader or its .mm host bridge could silently drift the score on Apple Silicon CI without any test catching it. Adds 4 new parity tests (core/test/test_metal_motion_v2_parity.c, ..._integer_psnr_parity.c, ..._float_psnr_parity.c, ..._float_ssim_parity.c) running synthetic 256x144 YUV420P fixtures through both the CPU twin and the Metal extractor and asserting places=4 (1e-4) parity per ADR-0214 — except float_ssim which uses 1e-3 per ADR-0589. Each test skips cleanly via -ENODEV on Linux/Windows/Intel Mac, runs the live kernel on Apple-Family-7+ macOS CI lanes. Wired under the existing enable_metal guard in core/test/meson.build with suite : ['fast', 'gpu']. | no ADR: test-only addition (follow-up to PR #351 registration audit; tolerances cite existing ADR-0214 / ADR-0589) | test/metal-kernel-coverage-round2 | meson setup core/build-cpu -Denable_cuda=false -Denable_sycl=false -Denable_metal=disabled && ninja -C core/build-cpu (CPU-only build green, 726/726 targets; new tests gated off on non-Metal). On macOS Apple-Family-7+ CI: meson test -C build --suite=fast test_metal_motion_v2_parity test_metal_integer_psnr_parity test_metal_float_psnr_parity test_metal_float_ssim_parity exercises the live kernels. | (2026-05-30) | | T-CLAUDE-SKILLS-ADR0700-PATH-DRIFT-2026-05-30 | 4 residual libvmaf/ source-tree references in .claude/skills/ survived T-POST-RENAME-DRIFT-SWEEP-2026-05-28 (state.md row 234 claims that sweep covered add-gpu-backend/scaffold.sh, but the live file on master still had src="$repo_root/libvmaf/src" on line 22 — the scaffold would have written generated files to a non-existent directory). Fixed: (1) add-gpu-backend/scaffold.sh line 22 libvmaf/src → core/src (load-bearing — the scaffold was silently broken); (2) build-vmaf/build.sh line 30 cd "$repo_root/libvmaf" → cd "$repo_root/core" (load-bearing — /build-vmaf would cd to a non-existent dir); (3) build-vmaf/SKILL.md line 22 doc-text update to match; (4) regen-docs/SKILL.md line 26 doc-text update; (5) add-simd-path/templates/simd_feature.c.template line 27 comment libvmaf/test → core/test (cosmetic — generated comment text). Verified end-to-end: bash .claude/skills/add-gpu-backend/scaffold.sh testbe now creates core/src/testbe/{common.{c,h},meson.build} + core/src/feature/testbe/{adm,vif,motion}_testbe.c. Verified shellcheck-clean + shfmt -d -i 2 -ci clean across all 5 skill .sh scripts. | no ADR: maintenance fix | chore/claude-skills-audit | bash .claude/skills/add-gpu-backend/scaffold.sh testbe && ls core/src/testbe/ core/src/feature/testbe/ succeeds; find .claude/skills -type f -exec grep -l 'libvmaf/[a-z]' {} + returns only the two intentional install-path refs (core/include/libvmaf/libvmaf.h in sync-upstream + add-gpu-backend SKILL.md). | (2026-05-30) | | T-TSAN-SSIM-DISPATCH-RACE-2026-05-30 | TSan audit of the libvmaf threadpool (--threads 16, float_ssim + float_ms_ssim + psnr + ciede on 1080p checkerboard) surfaced 10 data-race warnings on four process-wide SSIM SIMD dispatch globals (g_ssim_precompute, g_ssim_variance, g_ssim_accumulate, g_iqa_convolve in core/src/feature/iqa/ssim_tools.c). Root cause: every worker thread's per-extractor init() race-writes the same dispatch pointer; value-benign on x86-64 today but UB by the C memory model and torn-pointer risk on weakly-ordered hardware. Fix: gate the four globals behind a single process-wide pthread_once_t owned by ssim_tools.c, shared between float_ssim.c and float_ms_ssim.c via the new iqa_ssim_install_dispatch_once() helper. Post-fix: 0 TSan warnings, 63/63 meson test -C build-tsan cases pass, bit-for-bit identical pooled vmaf.mean=76.66783 on src01_hrc00/src01_hrc01. | ADR-0871 / Research/tsan-race-audit-2026-05-30 | fix/tsan-race-audit | TSAN_OPTIONS="halt_on_error=0 second_deadlock_stack=1 history_size=7" ./build-tsan/tools/vmaf -r python/test/resource/yuv/checkerboard_1920_1080_10_3_0_0.yuv -d python/test/resource/yuv/checkerboard_1920_1080_10_3_1_0.yuv -w 1920 -h 1080 -p 420 -b 8 --threads 16 --feature psnr --feature float_ssim --feature float_ms_ssim --feature ciede → zero WARNING: ThreadSanitizer lines | (2026-05-30) | | T-SANITIZER-PASS-CLEANUP-2026-05-30 | Local -Db_sanitize=address,undefined audit on master tip bbcaa8d127 surfaced two real UB findings missed by the per-PR CI gate. (1) core/src/feature/cambi.c: options table declared window_size / max_log_contrast as VMAF_OPT_TYPE_INT, but underlying CambiState fields were uint16_t. The parser's *(int *)data = ... write produced a UBSan misaligned-store on max_log_contrast (2-byte-aligned offset) and silently clobbered src_window_size on window_size. Mirror read site in vmaf_feature_name_from_options (core/src/feature/feature_name.c:104) repeated the load-side misalignment per frame. Fixed by adding int shadow slots (window_size_opt, max_log_contrast_opt) and copying into the uint16_t runtime fields in init(). (2) core/src/feature/x86/adm_avx{2,512}.c: DWT2 filter-packing used (uint32_t)(filter[k] << 16) with the cast on the shift's RESULT rather than its operand, leaving the inner shift on a signed int — UB whenever filter[k] was negative (every HBD ADM frame). Fixed by moving the cast inside: ((uint32_t)filter[k] << 16). Both fixes are bit-exact with prior behaviour. | ADR-0869 / Research | fix/sanitizer-pass-cleanup | Full ASan+UBSan run of unit-test suite (49 fast + 12 dnn + 2 slow = 63 OK), plus CLI on 4:2:0 8-bit / 4:2:2 10-bit / 4:2:0 12-bit + full feature set + model load — silent under sanitizers. | (2026-05-30) | | T-HELM-NETWORKPOLICY-PSS-2026-05-31 | deploy/helm/vmafx/ shipped a partial pod-security baseline (no seccompProfile, no container-level runAsNonRoot, UID 65534 drifted from the distroless nonroot UID 65532 baked into every production image by ADR-0878) and zero NetworkPolicies, so installs into a pod-security.kubernetes.io/enforce=restricted namespace either failed admission or needed per-install --set overrides. Fix (ADR-0930): flip every pod-security default to PSA restricted compliance (UID 65532, container runAsNonRoot, seccompProfile.type=RuntimeDefault at both pod and container scope); add templates/networkpolicy.yaml rendering a default-deny ingress + egress baseline plus narrow allow-rules for in-namespace HTTP ingress, controller → node gRPC, node → object-store HTTPS (CIDR + except matrix), operator → apiserver, and DNS egress to CoreDNS (gated by networkPolicy.enabled=false); refactor operator-deployment.yaml and tests/test-connection.yaml to inherit from .Values; extend NOTES.txt and docs/development/k8s-deployment.md with the PSA namespace-label command + NetworkPolicy matrix. | ADR-0930 / Research-0930 | chore/helm-networkpolicy-pss | helm lint deploy/helm/vmafx --strict → green; helm template deploy/helm/vmafx --set networkPolicy.enabled=true --set operator.enabled=true --set node.enabled=true --set node.image.repository=ghcr.io/vmafx/vmafx-node \| kubectl create --dry-run=client --validate=false -f - → 8 NetworkPolicy resources created (default-deny x3, allow-http-ingress, allow-controller-to-node, allow-node-egress-object-store, allow-operator-to-apiserver, allow-dns-egress). | (2026-05-31) | | T-SIMD-BIT-EXACT-ROUND2-2026-05-30 | Round-2 follow-up to PR #339: master tip 83698bd5b2 still reproduced test_ms_ssim_decimate (scalar reference inside libvmaf_feature_static_lib did not get -fp-model=precise, while the SIMD carve-out lib did — the round-1 fix scoped the flag to the SIMD libs only) and test_ssimulacra2_simd::test_ptlr_420_8 (icx auto-fused the AVX2/AVX-512 main-loop _mm256_add_ps(_, _mm256_mul_ps(_, _)) colour-matrix to FMA despite -fp-model=precise, while the gcc-built scalar reference used non-fused mul+add). Fix: (a) extend -fp-model=precise to libvmaf_feature_static_lib + libvmaf_ssimulacra2_static_lib via a new _libvmaf_feature_icx_args helper in core/src/meson.build; (b) unify SSIMULACRA 2 picture_to_linear_rgb on FMA across implementations — _mm256_fmadd_ps / _mm512_fmadd_ps in the main loops, fmaf() in the scalar tails and the test_ssimulacra2_simd.c reference. All compilers (gcc, clang, icx) now perform single-rounded FMA. | ADR-0891 | fix/simd-bit-exact-round2 | All 49/49 fast+simd tests pass locally under gcc 16; test_ms_ssim_decimate, test_ssimulacra2_simd, test_psnr_hvs_simd all green (10/10, 13/13, 5/5 subtests). | (2026-05-30) | | T-SIMD-ICX-FP-CONTRACT-2026-05-30 | Three SIMD bit-exactness tests failed in "Build — Linux (GCC, all backends)" CI (build.yml uses CC=icx, Intel oneAPI DPC++ 2025.3 when SYCL is enabled): test_psnr_hvs_simd, test_ms_ssim_decimate, test_ssimulacra2_simd::test_xyb. Root cause: icx defaults to -ffp-contract=on (auto-fuses mul-add → FMA, unlike GCC) AND silently ignores #pragma STDC FP_CONTRACT OFF unless -fp-model=precise is also on the command line. Result: scalar reference auto-FMA'd while SIMD carve-out (with -ffp-contract=off) did not → rel=1.16e-07 > tol=1e-12. Fix: core/src/meson.build detects cc.get_id() == 'intel-llvm' / 'intel-llvm-cl' and adds -fp-model=precise to all four x86 SIMD carve-out static libs (psnr_hvs_avx2, ms_ssim_decimate_avx2, ssimulacra2_avx2, ssimulacra2_avx512); core/test/meson.build introduces _simd_strict_fp_args = [-ffp-contract=off] + [-fp-model=precise when icx] applied to the three test TUs (two were missing -ffp-contract=off entirely). GCC/vanilla Clang unaffected (flag not emitted, GCC already defaults to no FMA contraction). PR #282 follow-up; no dedicated ADR (compiler-flag fix). | PR #282 follow-up (compiler-flag fix; no dedicated ADR) | fix/simd-bit-exact-all-backends-matrix | PR #339: 49/49 fast+simd tests pass under GCC locally; all-backends Linux matrix green under CC=icx. | (2026-05-30) | | T-COVERAGE-GATE-ORT-BACKEND-FLOOR-BREACH-2026-05-30 | core/src/dnn/ort_backend.c measured 79.0% at run 27093693903 (2026-06-07), below the 83% per-file floor set by ADR-0922 ratchet (+5pp over ADR-0114's original 78%). Root cause: ADR-0922 ratcheted the floor to 83% but the EP-device branch paths (try_append_cuda, try_append_openvino, try_append_rocm, try_append_coreml, and the ADR-0113 two-stage CreateSession fallback) are unreachable through any existing test. Fix: added 12 EP-device fallback tests to test_ort_internals.c — each requests a non-CPU device (CUDA/OpenVINO/ROCm/CoreML/variants), which on CPU-only runners either fails EP registration (-ENOSYS) or succeeds EP registration but fails CreateSession (no hardware), both of which fall back to CPU and return 0. Also covers the threads>0 retry path in the two-stage fallback. PR #338 (prior fix for 77.8%→78.5%) is superseded; this PR delivers ~4pp of additional real coverage. | ADR-0114 / ADR-0922 | fix/ci-tests-quality-gate-failures (this PR) | meson test -C core/build-fix-check test_ort_internals PASS (no-DNN build, all tests short-circuit); Coverage Gate expected green at ≥83%. | (2026-06-07) | | T-DOCKER-TRIVY-USER-NONROOT-2026-05-30 | Production container images (docker/Dockerfile.production cli/server final stages + docker/Dockerfile.production-gpu 5 final stages: cpu/cuda12/rocm6/oneapi2026/vulkan) ran as root (UID 0). Trivy v0.69.3 config scan flagged DS-0002 HIGH on both files. Fix: added USER nonroot:nonroot (UID 65532, baked into gcr.io/distroless/cc-debian12) to every final stage. dev/Containerfile keeps USER root (intentional dev sandbox; rationale recorded in ADR-0878). Image-CVE scan blocked: ghcr.io/vmafx/vmafx-cpu:latest returns MANIFEST_UNKNOWN — production image set not yet published; follow-up the moment the first tag fires. | ADR-0878 / Research-0878 | chore/trivy-container-scan | trivy config --skip-version-check docker/Dockerfile.production and …/Dockerfile.production-gpu both report 0 HIGH, 0 MEDIUM, 1 LOW (DS-0026 HEALTHCHECK — superseded by k8s probes in the Helm chart). | (2026-05-30) | | T-SIMD-ICX-FP-CONTRACT-2026-05-30 | Three SIMD bit-exactness tests failed in "Build — Linux (GCC, all backends)" CI (build.yml uses CC=icx, Intel oneAPI DPC++ 2025.3 when SYCL is enabled): test_psnr_hvs_simd, test_ms_ssim_decimate, test_ssimulacra2_simd::test_xyb. Root cause: icx defaults to -ffp-contract=on (auto-fuses mul-add → FMA, unlike GCC) AND silently ignores #pragma STDC FP_CONTRACT OFF unless -fp-model=precise is also on the command line. Result: scalar reference auto-FMA'd while SIMD carve-out (with -ffp-contract=off) did not → rel=1.16e-07 > tol=1e-12. Fix: core/src/meson.build detects cc.get_id() == 'intel-llvm' / 'intel-llvm-cl' and adds -fp-model=precise to all four x86 SIMD carve-out static libs (psnr_hvs_avx2, ms_ssim_decimate_avx2, ssimulacra2_avx2, ssimulacra2_avx512); core/test/meson.build introduces _simd_strict_fp_args = [-ffp-contract=off] + [-fp-model=precise when icx] applied to the three test TUs (two were missing -ffp-contract=off entirely). GCC/vanilla Clang unaffected (flag not emitted, GCC already defaults to no FMA contraction). PR #282 follow-up; no dedicated ADR (compiler-flag fix). | PR #282 follow-up (compiler-flag fix; no dedicated ADR) | fix/simd-bit-exact-all-backends-matrix | PR #339: 49/49 fast+simd tests pass under GCC locally; all-backends Linux matrix green under CC=icx. | (2026-05-30) | | T-MACOS-PYTHON-ANPSNR-RESIDUAL-2026-05-30 | Finishes ADR-0749 ansnr sunset that PR #276 left incomplete: removed the residual VMAF_*_anpsnr_score (anpsnr companion key emitted by the same removed float_ansnr.c) Python assertAlmostEqual assertions in feature_extractor_test.py (20 lines, 11 single-line + 9 multi-line), and skipped 5 SVM/resource-dependent tests still hard-requiring ansnr (test_run_vmaf_runner_v1_model + 4 routine_test.py tests using feature_param_sample*.py resources). Clears all 16 macOS Python KeyError test failures across the 4 RED macOS CI jobs (CPU+Metal, CPU clang, CPU+DNN, T8-1 Metal scaffold); same root cause masked on Linux/Windows by matrix differences also closed. | ADR-0749 | fix/macos-build-failures-bbcaa8d127 | PR #335 (commit e0cf7dfc30): macOS CI green; pytest python/test/feature_extractor_test.py python/test/routine_test.py -v no KeyError: '...anpsnr...' / '...ansnr...'. | (2026-05-30) | | T-PRINTF-FORMAT-PORTABILITY-2026-05-30 | Audit of every fork-added C / C++ source under core/src/, core/tools/, core/test/, cmd/, mcp-server/ for non-portable length-modifier printf specifiers (CERT FIO47-C / MISRA 21.6). Found 2 Class-A (silent-truncation-on-Windows-LLP64) bugs in core/src/sycl/common.cpp printing uint64_t frame_counter / timing_frames with (unsigned long) + %lu; 9 Class-B (portable-but-non-idiomatic (long long) / (unsigned long long) casts on fixed-width types) in core/src/libvmaf.c (tiny-model loader, 6 sites), core/src/sycl/dmabuf_import.cpp (DRM modifier prints, 2 sites), core/test/test_motion_v2_simd.c (SAD divergence log, 1 site). All 11 fix candidates converted to <inttypes.h> PRI macros (PRIu64 / PRId64 / PRIx64). Three call sites verified not-bugs and left alone: off_t + (long long) in core/tools/yuv_input.c (correct CERT idiom for non-fixed-width POSIX types), Windows DWORD + (unsigned long) in core/test/test_public_api_score.c (DWORD is exactly unsigned long on Windows), upstream print_128_64 debug macro in core/src/feature/x86/adm_avx512.c (reserved for upstream sync). | ADR-0876 / Research-0876 | fix/printf-format-portability | grep -rnE '%(lld\|llu\|llx\|lu)\b' core/src/sycl/ core/src/libvmaf.c core/test/test_motion_v2_simd.c returns empty; meson test -C build-cpu --suite=fast 49/49 pass. | (2026-05-30) | | T-MAGIC-NUMBER-AUDIT-CERT-INT07C-2026-05-30 | Sweep of fork-added C surfaces for unnamed numeric literals (CERT INT07-C / MISRA C 4.10). 26 call sites in core/src/mcp/{mcp_internal.h,mcp.c,compute_vmaf.c,transport_sse.c}, core/src/picture.c, core/src/cuda/picture_cuda.c, core/src/libvmaf.c rewritten in terms of ~20 named #define constants (VMAF_MCP_LISTEN_BACKLOG, VMAF_MCP_TRANSPORT_BITMASK_MAX, VMAF_MCP_MAX_DRAIN_PER_FRAME, VMAF_MCP_SSE_PATH_MAX, VMAF_MCP_UDS_PATH_MAX, VMAF_MCP_PIC_DIM_{MIN,MAX}, VMAF_MCP_BITDEPTH_{8,10,12,16}, VMAF_MCP_SSE_* scratch + poll constants, VMAF_PIC_BPC_{MIN,MAX} + VMAF_PIC_DIM_MAX, VMAF_CUDA_PIC_BPC_{MIN,MAX}, VMAF_DNN_NAME_{FALLBACK_BUF,DEDUP_BUF,STRNLEN_CAP}). Rename-only — no numeric value changes; bit-exact CPU golden gate preserved. | ADR-0874 / Research | fix/magic-number-audit | meson setup build-magic core -Denable_cuda=false -Denable_sycl=false && ninja -C build-magic && meson test -C build-magic — 63/63 pass; scripts/ci/assertion-density.sh pass. | (2026-05-30) | | T-EINTR-AND-IO-ERROR-AUDIT-2026-05-30 | POSIX I/O EINTR + return-value audit on fork-added C. Two MCP drain loops (transport_stdio.c:150-156, transport_uds.c:134-139) skipped EINTR retry on the line-too-long drain path — under signal pressure the drain could exit early and desync the next request boundary. Seven close(2) call sites (libvmaf.c:2846, cambi.c:739, dmabuf_import.cpp:323/350, vmaf_vpl.c:117/136/145) discarded the return without (void) cast (Power-of-10 rule 7 violation). All fixed. Primary I/O helpers (read_line, write_all_with_newline, sse_read_n, read_exact) already correct. Vendored svm.cpp:2522 left for future libsvm rebase. | ADR-0872 / Research | fix/eintr-and-io-error-audit | meson setup build-eintr core -Denable_cuda=false -Denable_sycl=false && ninja -C build-eintr && meson test -C build-eintr --suite=fast --no-rebuild — 49/49 OK. | (2026-05-30) | | T-HELM-VALUES-SCHEMA-AND-CONTAINER-AUDIT-2026-05-30 | Two adjacent operational-hygiene gaps closed in one audit cycle. (1) deploy/helm/vmafx/values.yaml had no JSON Schema, so typos like --set gpu.vendor=qualcomm or --set workload=Daemonset were silently accepted and only surfaced as scheduling failures. Added deploy/helm/vmafx/values.schema.json (Draft 2020-12) enforcing enums on workload, gpu.vendor, storage.mode, service.type, image.pullPolicy, persistence.accessMode, operator.logLevel, statefulSet.podManagementPolicy, and monitoring.serviceMonitor.scheme, with additionalProperties: false on every typed sub-object to catch sibling-key typos. (2) dev/Containerfile still had COPY libvmaf/ /build/vmaf/libvmaf/ and cd libvmaf && meson setup build after ADR-0700 renamed the directory to core/, so every fresh docker compose build dev-mcp failed with "libvmaf": not found. Fixed COPY libvmaf/ → COPY core/, added COPY compat/ for the editable Python install, updated both cd libvmaf → cd core, extended .dockerignore with core/build*/ siblings (legacy libvmaf/build*/ retained for pre-rename worktrees). | ADR-0870 | chore/helm-values-schema-and-container-audit | helm lint deploy/helm/vmafx --strict passes; helm template deploy/helm/vmafx --set gpu.vendor=qualcomm fails with at '/gpu/vendor': value must be one of 'nvidia', 'amd', 'intel', 'cpu'; hadolint dev/Containerfile reports zero HIGH-severity findings; grep -rn 'libvmaf/' dev/ returns only the ADR-0700 comment annotation. | (2026-05-30) | | T-LEGACY-RUNNER-ANSNR-BROKEN | VmafFeatureExtractor._generate_result() requested float_ansnr in its features list; the C library dropped that feature per PR #38. ADR-0749 formally sunset the legacy float VMAF runner and its associated assertions; PR #283 then deleted the dead AnsnrFeatureExtractor class (compat/python-vmaf/core/feature_extractor.py) plus its test_run_ansnr_fextractor method. The class is now absent from master; the Netflix golden assertions still cover the integer-path VMAF score (Rule #1 preserved). | no ADR: bug fixed by PR #283 + earlier ADR-0749 sunset | PR #283 (merged at faede7a68c) | git show origin/master:compat/python-vmaf/core/feature_extractor.py \| grep -c 'class AnsnrFeatureExtractor' returns 0. | (2026-05-30) | | T-LEGACY-RUNNER-STUB-MISSING-2026-05-29 | from vmaf.core.quality_runner import VmafLegacyQualityRunner raised ImportError on master because the class was silently removed in ADR-0749 / PR #87. PR #213 (closed without merging 2026-05-30) proposed the stub but was not landed. A follow-up PR (fix/legacy-runner-import-stub-adr0749) added the NotImplementedError-raising stub to compat/python-vmaf/core/quality_runner.py, so import succeeds but instantiation fails fast with an ADR-0749 migration pointer. python/test/quality_runner_test.py was already cleaned by the ADR-0749 sunset PR and does not import the class. | ADR-0749 | fix/legacy-runner-import-stub-adr0749 | PYTHONPATH=compat python3 -c "from vmaf.core.quality_runner import VmafLegacyQualityRunner; print('import OK')" returns import OK. | (2026-06-04) | | T-VK-1.4-BUMP | Vulkan 1.4 API-version bump blocked on FP-contraction regression on NVIDIA driver 595.71+ (ADR-0264, ADR-0269). Superseded by ADR-0726 (Vulkan backend dropped 2026-05-28) — the entire Vulkan source tree, public API header, build options, and CI lanes were removed, eliminating the API-1.4 bump as a fork concern. Native CUDA / HIP / SYCL backends cover every vendor formerly served by Vulkan. | ADR-0726 supersedes ADR-0264 / ADR-0269 | PR #47 (ADR-0726) — Vulkan backend removed | ls core/src/vulkan/ core/src/feature/vulkan/ core/include/libvmaf/libvmaf_vulkan.h 2>&1 \| grep -c 'No such file' returns 3 on master. | (2026-05-30) | | T-VK-CIEDE-F32-F64 | ciede2000 NVIDIA-Vulkan places=4 5/48 mismatch (max abs 8.9e-05, 1.78× threshold) — structural f32 vs f64 colour-space-chain precision gap. Superseded by ADR-0726 (Vulkan backend dropped 2026-05-28) — Vulkan removal closes this documented-debt row by removing the affected code path entirely. CPU / CUDA / SYCL ciede paths are unaffected. | ADR-0726 supersedes ADR-0391 | PR #47 (ADR-0726) — Vulkan backend removed | ls core/src/feature/vulkan/shaders/ciede.comp 2>&1 \| grep -c 'No such file' returns 1 on master. | (2026-05-30) | | T-VK-VIF-1.4-RESIDUAL-ARC | vif residual mismatch at API 1.4 on Intel Arc A380 (Mesa-ANV / DG2) surviving Phase-3's shader memory-model fix that closed the NVIDIA + RADV lanes. Superseded by ADR-0726 (Vulkan backend dropped 2026-05-28) — the residual was one of three long-standing Vulkan blockers cited as motivation for the drop (ADR-0726 §Context); removal of vif.comp and the entire core/src/feature/vulkan/ tree closes this row. Intel users now use CPU or SYCL for vif. | ADR-0726 supersedes ADR-0264 / ADR-0269 | PR #47 (ADR-0726) — Vulkan backend removed | ls core/src/feature/vulkan/shaders/vif.comp 2>&1 \| grep -c 'No such file' returns 1 on master. | (2026-05-30) | | T-GPU-PICTURE-POOL-UAF-INIT-FAILURE-2026-05-30 | vmaf_gpu_picture_pool_init() in core/src/gpu_picture_pool.c published the pool pointer to the caller's *pool argument via the combined assignment *const p = *pool = malloc(...) before any subsequent failure path ran. On goto free_p (pic-array malloc or pthread_mutex_init failure) the function freed p but *pool still held the dangling pointer, and the natural vmaf_close() teardown called vmaf_gpu_picture_pool_close() on the freed memory — UAF + potential double-free, since libvmaf.c:326 stores the handle in the long-lived VmafContext.cuda.ring_buffer. Fix: clear *pool = NULL at every failure label (after !p malloc check and after free(p) at the free_p label) so a non-zero return reliably signals "pool not constructed". CPU-only regression test (test_gpu_picture_pool_uaf) added in suite=fast. | no ADR: bug fix per CLAUDE §12 r8 | fix/gpu-picture-pool-uaf-on-init-failure | meson setup build-cpu core -Denable_cuda=false -Denable_sycl=false -Denable_hip=false && ninja -C build-cpu && meson test -C build-cpu test_gpu_picture_pool_uaf — pass. | (2026-05-30) | | T-K150K-CRASH-RESTART-ROW-LOSS-2026-05-30 | ai/scripts/extract_k150k_features.py silently confirmed .done-vs-parquet row-count mismatch on restart. A prior K150K run was killed after the parquet rename but before the staging-file unlink, leaving the parquet truncated; subsequent invocations entered the no-op early-exit branch and wrote a status=complete-noop manifest without ever comparing row counts. Result: 152 265 clips marked done, only 59 812 rows persisted (~92 K silent loss). Fix: (1) _load_staging_rows surfaces a WARNING with the count of malformed lines skipped; (2) no-op branch raises RuntimeError when len(done_set) > parquet_rows + recovered; (3) end-of-run accounting assert (len(rows) == len(recovered_rows) + ok); (4) new _fsync_path helper called before staging unlink in both paths. Unit test pins the contract. | ADR-0862 / Research | fix/k150k-crash-restart-row-loss | cd /tmp/wt-k150k-bug3 && python -m pytest ai/tests/test_extract_k150k_consistency.py -v — 4 passed. | (2026-05-30) | | T-CUDA-FILTER1D-RES-DISPATCH-CONFLICT-2026-05-29 | Closed as superseded — conflict markers only existed on the unmerged feat/cuda-resolution-dispatch-scaffold-20260529 branch tip (35a1fb6249). Master never carried them: PR #91 (merged 2026-05-29T09:37:48Z) landed ADR-0753 resolution-aware dispatch via the adm_cm_device() consumer without extending dispatch into filter1d_8(), so the build-failing scaffold variant never reached master. PR #214 (the planned conflict-marker cleanup) was CLOSED-not-merged 2026-05-30 once its base scaffold branch was abandoned. core/src/feature/cuda/integer_vif_cuda.c::filter1d_8() on master uses the clean unconditional cuLaunchKernel paths (lines 322–351). | no ADR: superseded by PR #91 dispatch consumer | PR #91 (merged 2026-05-29T09:37:48Z); PR #214 (closed-not-merged 2026-05-30) | grep -nE '^(<{7}\|={7}\|>{7})( \|$)' core/src/feature/cuda/integer_vif_cuda.c on master returns empty. | (2026-05-30) | | T-CI-CONFLICT-MARKERS-PR50-2026-05-29 | Commit 24bb5daf89 (post-merge-train sweep #50) introduced committed git conflict markers in 38 files across .github/, ai/, core/, and docs/. Most files resolved by PR #108 (CUDA integer_vif_cuda.c), PR #174 (CI YAML), PR #243 (70 files). This closeout PR resolved the last 2 residual markers in .semgrepignore (4 blocks, post-ADR-0700 paths kept) and docs/backends/sycl/overview.md (1 block, took the HEAD core/include/libvmaf/libvmaf.h path). | no ADR: maintenance fix | chore/state-md-stale-entries-sweep-20260530 | git grep -nE '^(<{7}\|={7}\|>{7})( \|$)' -- ':(exclude)docs/research/' ':(exclude)docs/adr/_index_fragments/' ':(exclude)*.lock' ':(exclude)CHANGELOG.md' returns empty. | (2026-05-30) | | T-GITHUB-ACTIONS-AUDIT-2026-05-30 | GitHub Actions hardening audit. Verified all 153 uses: lines across 22 in-scope workflows are SHA-pinned (lint-and-format.yml and libvmaf-build-matrix.yml skipped — in-flight under PR #342 and PR #325). Backfilled top-level permissions: contents: read on .github/workflows/go-ci.yml and .github/workflows/rust-ci.yml (only two workflows lacking a top-level perms block). Added persist-credentials: false to 5 actions/checkout steps in sanitizers.yml (2) and supply-chain.yml (3) — those jobs (asan-ubsan, tsan, build-artifacts, sbom, mcp-build) do not push back to git, so they should not persist the GITHUB_TOKEN into .git/config. OIDC adoption already complete for every cloud-auth and signing step. No long-lived secrets remain outside GITHUB_TOKEN. | ADR-0875 / Research-0875 | chore/github-actions-audit | After PR merges: python3 scripts/... audit script (in research digest) prints nothing for the 22 in-scope workflows. | (2026-05-30) | | T-METAL-MT1-DISPATCH-FLOAT-MS-SSIM-2026-05-29 | g_metal_features[] in core/src/metal/dispatch_strategy.c lacked "float_ms_ssim_metal". vmaf_metal_dispatch_supports() returned 0 for the float MS-SSIM Metal extractor even after ADR-0490 / T-VULKAN-METAL-DEAD-SCAFFOLDS-2026-05-18 wired the TU into meson. Callers routing through the dispatch table (ADR-0420 gate, ADR-0421 consumer) silently fell back to CPU on every Apple Silicon run. Fixed by adding the entry in the table, adjacent to "float_ms_ssim". | no ADR: only-one-way fix | fix/metal-pr117-actionable-findings-20260529 | Smoke: vmaf_metal_dispatch_supports(ctx, "float_ms_ssim_metal") == 1 on Apple Silicon. | (2026-05-29) | | T-METAL-MT2-ARC-RETAIN-BALANCE-2026-05-29 | vmaf_metal_state_init_external in core/src/metal/picture_import.mm called CFRetain((__bridge CFTypeRef)device) (or queue) followed immediately by (__bridge_retained void *)device — accumulating +2 retain counts. vmaf_metal_state_free releases via a single __bridge_transfer (-1 retain), leaving one reference permanently live per init/close cycle for both device and queue. On an external-device path called by the FFmpeg libvmaf_metal filter, every filter graph teardown leaked the id<MTLDevice> and id<MTLCommandQueue> references. Fixed by removing both CFRetain calls; __bridge_retained alone is the correct single ownership transfer. | no ADR: only-one-way fix | fix/metal-pr117-actionable-findings-20260529 | Smoke: leaks --atExit -- vmaf --backend metal ... returns 0 Metal object leaks per init/teardown cycle. macOS CI smoke required; Linux host: static-analysis only. | (2026-05-29) | | T-PERF-BENCH-BASELINE-MISSING — No versioned multi-resolution performance baseline existed; per-PR performance numbers were generated ad-hoc in incompatible formats | CLOSED — scripts/perf/bench-multi-resolution.sh + testdata/perf_multi_resolution.json added in PR feat/perf-bench-multi-resolution-20260529 (ADR-0752) | — | Research-0752 | | T-CUDA-READBACK-HOST-PINNED-LEAK-20260529 | vmaf_cuda_kernel_readback_free (kernel_template.h) NULLed rb->host_pinned without calling vmaf_cuda_buffer_host_free, leaking one pinned cuMemHostAlloc allocation per init/close cycle. Present in 9 template-using feature extractors: integer_psnr, integer_ssim (float_ssim), ssim (integer), float_psnr, float_motion, integer_ciede, integer_moment, integer_motion_v2, integer_cambi. Fixed by adding a NULL-guarded vmaf_cuda_buffer_host_free(cu_state, rb->host_pinned) call inside the helper, so all callers are fixed at once. | no ADR (bug fix per CLAUDE §12 r8) | fix/cuda-pinned-host-leak-sweep-20260529 | compute-sanitizer --tool memcheck --leak-check full ./build-cuda/tools/vmaf --backend cuda --reference REF --distorted DIST --width 576 --height 324 --pixel_format 420 --bitdepth 8 --json --output /dev/null → LEAK SUMMARY: 0 bytes leaked | (2026-05-29) | | T-CUDA-SSIM-VERT-COMBINE-LDG-PINNED-LEAK-2026-05-29 | CUDA SSIM vert_combine perf optimisation (ADR-0754). Applied __launch_bounds__(128) register-budget hint (F4) and extracted const float *__restrict__ pointers from 5 VmafCudaBuffer args before the inner loop, using __ldg() for all 55 reads (F2). Per-caller F6 fix dropped — superseded by helper-level fix in PR #94 (T-CUDA-READBACK-HOST-PINNED-LEAK-20260529). Live ncu A/B measured 2026-05-29 (RTX 4090 sm_89, ncu 2026.2.0.0): 1080p duration -4.2% (criterion ≥3% met); DRAM throughput unchanged (-0.01pp, kernel already DRAM-saturated at 89.8%); DRAM elapsed cycles -4.3%. 576p result +4.0% is noise (wave-limited, < 0.4 waves). Registers unchanged (40/thread sm_89). Correctness: bit-identical scores (0.00e+00 max diff). F1/F3 deferred; integer_psnr_cuda.c follow-up named. | ADR-0754 / Research-0754 | PR #93 (branch: perf/cuda-ssim-vert-combine-ldg-launch-bounds-leak-20260529) | places=4 cross-backend parity gate passes. | (2026-05-29) | | T-CUDA-F3-STRUCT-BY-VALUE-AUDIT-2026-05-29* | Fork-wide audit of all __global__ kernels accepting VmafCudaBuffer / VmafPicture / AdmBufferCuda by value (F3 pattern from PR #93). 20 kernel variants across 8 families identified; 5 high-severity (hot inner loop, no smem tile, no existing pointer extraction); 1 already fixed (PR #93 ssim_vert_combine). Top-5 dispatch PRs defined in Research-0756; ms_ssim_vert_lcs is priority-1. ADR-0756 records scope + dispatch order. | ADR-0756 / Research-0756 | research/cuda-f3-struct-by-value-audit-20260529 | Audit-only PR — no code changes. | (2026-05-29) | | T-CUDA-MUL24-AUDIT-2026-05-28 | CUDA __mul24 silent-corruption sweep (Research-0734). Audited all 78 files under core/src/feature/cuda/ and core/src/cuda/ for __mul24 / __umul24 / __mul24hi usage. Zero instances found; no scores affected by the CUDA 11.1–13.3 silent-corruption bug. Prohibition invariant added to core/src/feature/cuda/AGENTS.md; known-issue note added to docs/backends/cuda/overview.md. | Research-0734 | audit/cuda-mul24-corruption-20260528 | grep -rn '__mul24\|__umul24\|__mul24hi' core/src/feature/cuda/ core/src/cuda/ — no output (exit 1). | (2026-05-28) | | T-POST-RENAME-DRIFT-SWEEP-2026-05-28 | Post-ADR-0700 path drift sweep: fixed 9 stale libvmaf/ and python/vmaf/ directory references across Makefile, Dockerfile, .github/codeql-config.yml, .vscode/c_cpp_properties.json, .zed/settings.json, .claude/skills/add-gpu-backend/scaffold.sh, scripts/dev/project_modernization_audit.py, README.md, AGENTS.md, and .claude/skills/add-model/SKILL.md. No ADR (maintenance fix). | no ADR | chore/post-rename-drift-sweep-20260528 | grep -rn 'libvmaf/[a-z]' . --include='*.sh' --include='*.py' --include='Makefile' ... \| grep -vE '(libvmaf\.so\|core/include/libvmaf\|...) returns zero actionable hits after this PR. | (2026-05-28) | | T-CUDA-VIF-FILTER1D-NCU-HOTPATH-2026-05-28 | CUDA VIF filter1d.cu profiled with ncu 2026.2.0.0 on RTX 4090 (sm_89). Primary hotspot: filter1d_8_horizontal_kernel_2_17_9 (35 % of VIF filter time, 20.8 µs avg). Diagnosis: launch-width-limited (0.84 waves), register-pressure ceiling (56 regs → 75 % theoretical occupancy), L2 hot-slice imbalance (+46 %). Next: evaluate val_per_thread 2→4 for scale-0 horizontal pass. | Research-0734 | research/cuda-vif-ncu-hotpath-20260528 | docker run ... ncu ... /workspace/core/build-ncu/tools/vmaf ... reproducer in Research-0734. | (2026-05-28) | | T-CUDA-EXTERN-C-SWEEP-0747-2026-05-28 | Full sweep of all 24 .cu kernel files across core/src/feature/cuda/ and core/src/cuda/ found one broken file: integer_ssim/integer_ssim_score.cu (three __global__ kernels not wrapped in extern "C", causing --feature ssim --backend cuda to silently fail since introduction). Fixed by adding extern "C" { } wrapping. CI audit script added at scripts/dev/check-cuda-extern-c.sh; invariant documented in core/src/feature/cuda/AGENTS.md. All other 23 kernel files confirmed safe. | ADR-0747 / Research-0747 | audit/cuda-extern-c-name-mangling-sweep-20260528 | bash scripts/dev/check-cuda-extern-c.sh exits 0 | (2026-05-28) | | T-CI-REQUIRED-FAILURES-ROUND-3-2026-05-28 | 8 pre-existing required-aggregator failures fixed: (1+2) Windows MSVC CUDA + SYCL build steps referenced libvmaf\build instead of core\build (ADR-0700 rename leftover); (3) Ubuntu HIP smoke test still asserted float_ansnr_hip which was removed in PR #38; (4) Netflix golden CI gate (test_run_vmaf_runner) failed because VmafIntegerFeatureExtractor requested the removed float_ansnr CLI feature — fixed by removing it from the integer-path feature list (the test already expected KeyError for ansnr); (5) CodeQL Python autobuild looked for libvmaf/tools which no longer exists — fixed by updating codeql-config.yml paths to core/; (6) Gitleaks found 2 findings (likely go.sum / protobuf hash false positives) — added allowlist entries; (7) Semgrep vmaf-no-strcpy-strcat-sprintf found compat/python-vmaf/matlab/ (post-rename path not in .semgrepignore) — added path; (8) Tiny AI ImportError (dumps_jsonl_row, dumps_registry_json, write_registry_json missing) — functions implemented. All 8 aggregator checks expected to pass. Legacy runner tests (test_run_vmaf_legacy_runner) remain broken — tracked as T-LEGACY-RUNNER-ANSNR-BROKEN. | Research-0735 | fix/ci-required-failures-round-3-20260528 | Aggregator result on next PR rebase. | (2026-05-28) | | T-CUDA-MUL24-AUDIT-2026-05-28 | CUDA __mul24 silent-corruption sweep (Research-0734). Audited all 78 files under core/src/feature/cuda/ and core/src/cuda/ for __mul24 / __umul24 / __mul24hi usage. Zero instances found; no scores affected by the CUDA 11.1–13.3 silent-corruption bug. Prohibition invariant added to core/src/feature/cuda/AGENTS.md; known-issue note added to docs/backends/cuda/overview.md. | Research-0734 | audit/cuda-mul24-corruption-20260528 | grep -rn '__mul24\|__umul24\|__mul24hi' core/src/feature/cuda/ core/src/cuda/ — no output (exit 1). | (2026-05-28) | | T-VMAFX-NODE-IMPL-2026-05-28 | vmafx-node Go worker binary (Phase 4b.2) shipped: cgo libvmaf scoring, GPU vendor detection (pkg/gpu/), ffmpeg hardware encoder support, ONNX inference registry (pkg/ai/), gRPC VmafxController client (gen/go/controller/), Prometheus metrics, SIGTERM graceful drain, multi-variant Docker images, Helm node Deployment. | ADR-0713 | feat/vmafx-node-go-worker-binary | go test ./pkg/gpu/ ./pkg/ai/ ./cmd/vmafx-node/ -v — all pass; go vet ./... clean. | (2026-05-28) | | T-VMAFX-SIDECAR-TRAINING-RESEARCH-0733-2026-05-28 | Research digest for Phase 4b.7 sidecar online training architecture complete. Evaluates three architecture options; recommends Option A (Python sidecar container per node). Specifies gRPC triple transport, EMA-SGD training loop with replay buffer, atomic checkpoint swap, VmafxModelTraining CRD, and operator reconcile loop. | ADR-0709 / Research-0733 | docs/research-0733-vmafx-sidecar-training-arch | doc-only PR; no test runner applicable — research digest filed. | (2026-05-28) | | T-VMAFX-RUST-PILOT-TAD-2026-05-28 | TAD (Temporal Absolute Difference) feature extractor implemented in Rust and wired into libvmaf.so via cbindgen + Meson custom_target. Proves the Phase 4 Rust-in-libvmaf integration story end-to-end. New --feature tad signal available; does not affect existing VMAF scores. Workspace Cargo.toml established at repo root. | ADR-0707 | feat/tad-rust-pilot | cargo test --manifest-path core/src/feature/rust/tad/Cargo.toml — 5/5 Rust unit tests pass; meson test -C build-tad-pilot test_tad_rust — 4/4 C smoke tests pass | (2026-05-28) | | T-VMAFX-PHASE4B-ADR-0709-2026-05-28 | VMAFX Phase 4b distributed platform umbrella ADR filed. Locks the controller/node/operator architecture, ffmpeg worker integration, rclone zero-copy storage, eBPF research path, Go ONNX Runtime AI inference in the node, Python sidecar continuous training (v1), C ABI break with ffmpeg-patches update, and native build sunset (Docker images + Helm chart only). Nine-phase implementation plan (4b.1–4b.9) established. | ADR-0709 | feat/vmafx-phase4b-distributed-platform-adr-0709 | doc-only PR; no test runner applicable — ADR-0709 filed and index row added. | (2026-05-28) | | T-VMAFX-MCP-GO-PORT-2026-05-28 | vmafx-mcp Go port: single static binary at cmd/vmafx-mcp/ using the official MCP Go SDK v1.6.1; all 15 Python tools ported with byte-for-byte schema parity. Python server preserved. | ADR-0704 | feat/vmafx-mcp-go-port | go test ./cmd/vmafx-mcp/ -v — all pass; go build ./cmd/vmafx-mcp/ — clean build. | (2026-05-28) | | T-VMAFX-TUNE-GO-STAGE1-2026-05-28 | vmafx-tune-go Stage 1 Go port delivered: compare subcommand for libx264/libx265 rate-quality sweeps shipped as vmafx-tune-go alongside the Python binary. pkg/encoder, pkg/bisect, pkg/report packages. JSON output is RFC 8259 strict and schema-compatible with Python report.py. Stubs for all unported subcommands redirect to vmaf-tune. Python tools/vmaf-tune/ unchanged. | ADR-0705 | feat/vmafx-tune-go-stage1 | go test ./... -count=1 — all pass; go build ./cmd/vmafx-tune — clean build. | (2026-05-28) | | T-VMAFX-SERVER-GO-GRPC-2026-05-28 | Added cmd/vmafx-server — production Go binary serving VMAF scoring over gRPC (VmafxScoring) and HTTP/JSON (/healthz, /readyz, /metrics, /v1/score). libvmaf linked via cgo (pkg/libvmaf/), structured JSON logging via log/slog, Prometheus metrics via prometheus/client_golang, SIGTERM 30 s graceful shutdown. Multi-stage distroless Dockerfile.go-server. Python implementation from PR #1583 retained as a compat layer. | ADR-0703 | feat/vmafx-server-go-grpc | go test ./cmd/vmafx-server/... ./pkg/libvmaf/... -v — 12/12 tests pass; go vet ./... clean; gofmt -l . empty. | (2026-05-28) | | T-CI-DRAFT-AUTOMERGE-GATE-2026-05-22 | Draft PRs left Required Checks Aggregator as a skipped required context; GitHub branch protection treats skipped required checks as successful, so marking a draft ready and immediately enabling auto-merge could merge before fresh ready-for-review checks registered. Secondary symptom: ADR collision phase 1 compared against live origin/master, so a fast post-merge job could self-collide against the PR's own ADR. Fix (ADR-0679): the required aggregator runs on drafts and fails intentionally, grants actions: read, ignores sibling check runs older than its current registration window, waits briefly for check registration before path-filter-skipping absent siblings, and the ADR collision guard compares against the event base SHA. | ADR-0679 / Research-0699 | fix/ci-draft-automerge-gate-20260522 | .venv/bin/python - <<'PY' YAML parser smoke over .github/workflows/required-aggregator.yml and .github/workflows/rule-enforcement.yml; scripts/ci/check-adr-numbering.sh; BASE_SHA=$(git merge-base origin/master HEAD) HEAD_SHA=HEAD scripts/ci/deliverables-check.sh < .workingdir2/pr-ci-draft-automerge-gate-body.md; make format-check; .venv/bin/mkdocs build --strict. | (2026-05-22) | | T-VULKAN-MOTION-LAVAPIPE-INIT-2026-05-20 | lavapipe motion parity restored. motion now aliases to the stable integer_motion_vulkan extractor for Vulkan backend comparisons, integer_motion_vulkan emits raw integer_motion by default like CPU/CUDA/legacy Vulkan, CUDA/SYCL/Vulkan motion_v2 high-edge mirror padding now matches the CPU reflect-101 helper (2 * size - idx - 2), and the lavapipe parity matrix runs motion / motion_v2 again at places=4. | ADR-0662 | fix/vulkan-motion-lavapipe-20260520 | docker exec vmaf-dev-mcp bash -lc 'cd /workspace && VK_LOADER_DRIVERS_SELECT="*lvp*" LD_LIBRARY_PATH=/tmp/vmaf-build-vulkan-lvp/src .venv/bin/python scripts/ci/cross_backend_parity_gate.py --vmaf-binary /tmp/vmaf-build-vulkan-lvp/tools/vmaf --reference python/test/resource/yuv/src01_hrc00_576x324.yuv --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --backends cpu vulkan --features adm ciede float_adm float_ansnr float_moment motion motion_v2 float_motion float_ms_ssim float_ms_ssim_lcs float_psnr float_ssim float_vif psnr psnr_hvs ssimulacra2 vif --gpu-id "vulkan:0x10005:0x0" --json-out /tmp/parity-gate-report/parity.json --md-out /tmp/parity-gate-report/parity.md' — full lavapipe matrix green, including motion max_abs_diff=1.000e-06 and motion_v2 max_abs_diff=0.000e+00. | (2026-05-20) | | T-AI-FR-REGRESSOR-V1-REFRESH-2026-05-20 | fr_regressor_v1 checkpoint, sidecar, registry row, and model card refreshed from runs/full_features_netflix_refresh_20260520.parquet (11190 rows, 30 cols). The ADR-0249 recipe and PLCC gate are unchanged. New LOSO PLCC 0.9982 ± 0.0014; BigBuckBunny SROCC caveat documented in the model card. | ADR-0647 | fix/ai-refresh-fr-regressor-v1-20260520 | PYTHONPATH=ai/src .venv/bin/python ai/scripts/train_fr_regressor.py --parquet runs/full_features_netflix_refresh_20260520.parquet --metrics-out runs/fr_regressor_v1_refresh_20260520_metrics.json; bash core/test/dnn/test_registry.sh; onnx.checker.check_model(onnx.load("model/tiny/fr_regressor_v1.onnx")) | (2026-05-20) | | T-DNN-MULTI-OUTPUT-2026-05-20 | vmaf_use_tiny_model() / vmaf_ctx_dnn_attach() rejected ONNX graphs with more than one scalar output even though standalone vmaf_dnn_session_run() already supported multi-output sessions. Fix (ADR-0646): attached rank-2 and rank-4 frame runners now use vmaf_ort_run() with one output slot per graph output, append each scalar to the feature collector, preserve the historical single-output key, and derive multi-output keys from count-matched sidecar output_names[] or ONNX output names with deterministic sanitised fallbacks. Non-scalar attached output tensors remain unsupported and return -ENOTSUP. | ADR-0646 | fix/dnn-attached-multi-output-20260520 | docker exec vmaf-dev-mcp bash -lc 'cd /workspace && rm -rf /tmp/vmaf-dnn-multi-output-build && meson setup /tmp/vmaf-dnn-multi-output-build libvmaf -Denable_dnn=enabled -Denable_cuda=false -Denable_sycl=false -Denable_hip=false -Denable_metal=disabled && meson test -C /tmp/vmaf-dnn-multi-output-build --suite=dnn --print-errorlogs' — 12/12 DNN tests green | (2026-05-20) | | T-INTEGER-ADM-P-NORM-SIMD-GAP-2026-05-20 | integer_adm.c exposed adm_p_norm, but the scalar/x86 contrast-measure callback ABI did not carry the value, so AVX2 / AVX-512 dispatch retained the hard-coded 3.0 exponent. Fix (ADR-0645): thread adm_p_norm through adm_cm and i4_adm_cm for scalar, AVX2, and AVX-512, update the option help/docs, and extend test_integer_adm_simd so p=2 and p=3 produce distinct finite AVX2 results for scale-0 and scale-1 callbacks. | ADR-0645 | fix/integer-adm-pnorm-simd-20260520 | docker exec vmaf-dev-mcp bash -lc 'rm -rf /tmp/vmaf-cpu-pnorm-build && meson setup /tmp/vmaf-cpu-pnorm-build /workspace/libvmaf -Denable_cuda=false -Denable_sycl=false -Denable_dnn=disabled && meson test -C /tmp/vmaf-cpu-pnorm-build test_integer_adm_simd --print-errorlogs' — pass | (2026-05-20) | | T-RULE-ENFORCEMENT-READY-FOR-REVIEW-TRIGGER-2026-05-20 | ADR-0331 says every draft-gated PR workflow must include ready_for_review so activating a draft fires CI once. .github/workflows/rule-enforcement.yml had the per-job draft guards but missed that event, so #1444 showed stale skipped ADR/docs/FFmpeg/state gates after promotion while the other workflows reran. Fix: add ready_for_review to the rule-enforcement pull_request.types list while keeping edited for PR-body-only reruns. | ADR-0331 | fix/integer-adm-pnorm-simd-20260520 | rg -n "types: \\[opened, edited, synchronize, reopened, ready_for_review\\]" .github/workflows/rule-enforcement.yml — match; #1444 rule-enforcement rerun triggered by PR-body edit. | (2026-05-20) | | T-TEST-FEATURE-COLLECTOR-VCS-HEADER-RACE-2026-05-20 | Fresh parallel Meson/Ninja builds can compile test_feature_collector.c before include/vcs_version.h is generated because the test directly includes libvmaf.c but its executable source list did not carry rev_target. This is nondeterministic: a second build passes after the header exists. Fix: list rev_target in the test_feature_collector sources so Meson orders the generated header before compiling the test. | — (test build-graph bug; no ADR) | fix/integer-adm-pnorm-simd-20260520 | docker exec vmaf-dev-mcp bash -lc 'rm -rf /tmp/vmaf-cpu-pnorm-race-build && meson setup /tmp/vmaf-cpu-pnorm-race-build /workspace/libvmaf -Denable_cuda=false -Denable_sycl=false -Denable_dnn=disabled && meson compile -C /tmp/vmaf-cpu-pnorm-race-build test/test_feature_collector' — must pass from a clean build. | (2026-05-20) | | T-DEV-CONTAINER-V16-ENCODER-PROBE-HARDENING-2026-05-20 | BBB v16 dev-container compare/debug failed as an operator workflow: QSV defaulted to the NVIDIA render node, the image lacked libmfx-gen.so, AMF runtime errors were hidden by trailing FFmpeg noise, dev-mcp health checked a socket the stdio entrypoint does not create, compare --format markdown produced raw tables instead of finished profile-card reports, and shared-reference bisects still used a 2× mid-run disk estimate after the reference was pre-decoded. Fix (ADR-0641): Intel VA-API auto-discovery, pinned intel/vpl-gpu-rt source build, actionable hardware probe stderr selection, vmaf --version healthcheck, compare --format html|both, default CPU set libx265,libsvtav1, and 1.1× raw-source headroom. | ADR-0641 | fix/dev-container-encoder-probes | PYTHONPATH=tools/vmaf-tune/src .venv/bin/python -m pytest tools/vmaf-tune/tests/test_hw_devices.py tools/vmaf-tune/tests/test_bbb_e2e_v14_bug_cluster.py tools/vmaf-tune/tests/test_compare_rate_quality_sweep.py tools/vmaf-tune/tests/test_bisect_concurrency_cap.py -q; docker compose -f dev/docker-compose.yml build dev-mcp must install libmfx-gen.so and docker compose ... config must keep vmaf --version healthcheck. | (2026-05-20) | | T-PYTHON-ROUTINE-SWALLOWED-EXCEPTION-2026-05-19 | routine.py:604 swallowed any extended-stats-calculation failure, silently producing uncalibrated normalisation stats and wrong PLCC/SROCC values. Fix (ADR-0620): replaced bare except Exception: print/fallback with raise CalibrationError(...) from exc; new allow_uncalibrated=False parameter on run_test_on_dataset lets callers that genuinely need the fallback opt in explicitly. 6 regression tests in python/test/test_adr0620_scaffold_audit_p0.py. | ADR-0620 | fix/scaffold-audit-p0-silent-correctness | python -m pytest python/test/test_adr0620_scaffold_audit_p0.py::TestRoutineCalibrationError -v — 6/6 green | (2026-05-19) | T-PYTHON-TRAIN-TEST-STD-ZERO-2026-05-19 | train_test_model.py:354 silently substituted np.zeros(len(ys_label)) for ys_label_stddev when absent from the stats dict, producing incorrect error bars in plot_scatter and misleading uncertainty estimates downstream. Fix (ADR-0620): replaced substitution with raise MissingLabelStddevError; assume_unit_stddev=True kwarg on plot_scatter opts in to unit bars. 5 regression tests. | ADR-0620 | fix/scaffold-audit-p0-silent-correctness | python -m pytest python/test/test_adr0620_scaffold_audit_p0.py::TestPlotScatterMissingStddev -v — 5/5 green | (2026-05-19) | T-PYTHON-LOCAL-EXPLAINER-HACKY-2026-05-19 | local_explainer.py:121 silently took model[0] from an ensemble list, computing per-feature importance from seed-0 only with no diagnostic. Fix (ADR-0620): replaced silent pick with raise EnsembleNotSupportedError for len(model) > 1; single-element lists continue to unwrap transparently. 5 regression tests. | ADR-0620 | fix/scaffold-audit-p0-silent-correctness | python -m pytest python/test/test_adr0620_scaffold_audit_p0.py::TestLocalExplainerEnsembleError -v — 5/5 green | (2026-05-19) | T-PYTHON-PERMUTATION-IMPORTANCE-HARDCODED-PATH-2026-05-19 | scripts/dev/permutation_importance.py:22 hard-coded REPO = Path("/home/kilian/dev/vmaf"), causing FileNotFoundError on any machine other than the fork's dev host. Fix: replace with REPO = Path(__file__).resolve().parents[2] (repo-root detection from __file__). | ADR-0621 | chore/scaffold-audit-p3-cleanup | python scripts/dev/permutation_importance.py --help must not FileNotFoundError on a clean checkout. | (2026-05-19) | T-PYTHON-COMPARE-NO-BACKEND-PRECHECK-2026-05-19 | vmaf-tune compare, compare --bisect, and tune-per-shot set score_backend = None if arg == "auto" else arg and passed the raw string to bisect_target_vmaf() without calling select_backend() first. An explicit --score-backend cuda on a CPU-only binary produced a cryptic vmaf binary error mid-bisect instead of the BackendUnavailableError exit 2 produced by corpus, ladder, and fast. Fix (ADR-0613): select_backend() pre-check inserted at the top of _run_compare (before bisect and CRF-sweep dispatch) and at the top of _run_tune_per_shot. The comment block in _run_tune_per_shot that deferred the fix was removed; the _build_per_shot_bisect_predicate and _run_compare_crf_sweep score_backend lines updated with ADR citations. | ADR-0613 | fix/scaffold-audit-p1-feature-plumbing | python -m pytest tools/vmaf-tune/tests/test_backend_precheck.py -v — 6/6 green | (2026-05-19) | T-HIP-PICTURE-ALLOC-ENOSYS-2026-05-19 | vmaf_hip_picture_alloc and vmaf_hip_picture_free in core/src/hip/picture_hip.c returned -ENOSYS unconditionally (full stubs). All 9 HIP extractors using explicit hipMemcpy for picture upload called these functions; every call failed silently. Fix (ADR-0613): implemented real hipMalloc-backed allocation under #ifdef HAVE_HIPCC; non-HIP build retains -ENOSYS in the #else branch. The pitched-allocation follow-up (T7-10c) is documented in the file header. | ADR-0613 | fix/scaffold-audit-p1-feature-plumbing | meson test -C build test_hip_picture --suite fast — round-trip alloc/free on 320×240 returns 0 on a ROCm host | (2026-05-19) | T-MOBILESAL-BPC-EARLY-REJECT-UNDOCUMENTED-2026-05-19 | feature_mobilesal.c returned -ENOTSUP for bpc != 8 but the error message did not name mobilesal as the blocker, making it hard for operators to diagnose HDR / 10-bit scoring failures. Fix (ADR-0613): improved error message explicitly names mobilesal as the component rejecting non-8-bit input and points the operator to the workaround (--bitdepth 8 or omit --feature mobilesal). docs/ai/models/mobilesal.md §Known limitations updated with a verbatim copy of the error and the workaround. | ADR-0613 | fix/scaffold-audit-p1-feature-plumbing | vmaf … --bitdepth 10 --feature mobilesal prints the new message and returns non-zero | (2026-05-19) | T-DNN-MULTI-OUTPUT-UNDOCUMENTED-2026-05-19 | core/src/libvmaf.c at lines 1115 and 1214 returned -ENOTSUP when the ONNX session produced out_n > 1 scalars, but the limitation was not documented in the public API or in docs/api/dnn.md. Fix (ADR-0613): added inline code comments at both sites citing ADR-0613 and the open T-DNN-MULTI-OUTPUT follow-up; updated docs/api/dnn.md §Known limitations with a clear description of the constraint, the standalone vmaf_dnn_session_run() workaround, and the tracking row. T-DNN-MULTI-OUTPUT promoted to Open bugs with the full follow-up scope. | ADR-0613 | fix/scaffold-audit-p1-feature-plumbing | grep -n "ENOTSUP" core/src/libvmaf.c — both sites carry the ADR-0613 citation; docs/api/dnn.md contains the limitation section | (2026-05-19) | T-BBB-V14-QUADRATIC-REDECODE-2026-05-19 | vmaf-tune compare with 56 concurrent workers (14 encoders × 4 target VMAFs) ran for ~9.7 hours without converging on the v14 BBB 1080p sweep. Root cause (ADR-0577 / PR #1354): each bisect worker's finally block deleted the 118 GB shared reference YUV on bisect completion; the next worker to need it re-decoded through the --max-concurrent-decodes 1 semaphore (~3 min per decode). With 56 workers and ~7 bisect iterations each, up to 392 re-decodes were issued instead of the intended 1. Fix (ADR-0607): decode the reference once in _run_compare before opening the thread pool; pass the pre-decoded .yuv path to all workers via pre_decoded_ref on compare_codecs/compare_codecs_sweep; delete in a try/finally block after pool shutdown. Workers see src_is_container=False and skip the per-bisect decode entirely. | ADR-0607 | fix/vmaftune-shared-ref-yuv-decode-once | python -m pytest tools/vmaf-tune/tests/test_bbb_e2e_v15_shared_ref.py -v — 7/7 green | (2026-05-19) | T-MACOS-VMAF-WRITE-OUTPUT-SEGV-DEEP-2026-05-19 | Build — macOS clang (CPU) SIGSEGV in test_write_output_json_path and test_vmaf_write_output persisted after PR #1403 (ADR-0602, commit 798a202fe) merged — macOS CI was cancelled for that PR and the fix was never verified. CI run 26065652545 / job 76635756665 confirmed the regression. Deep root-cause analysis found four additional bugs: (1) seven i > capacity capacity bounds checks in core/src/output.c were off-by-one — index capacity is one past the end of the allocated score array; with MALLOC_PERTURB_=198 the stale byte at score[capacity].written can be non-zero, causing spurious "written" results and downstream invalid-pointer dereferences under Apple Clang's UB optimizations; fixed to i >= capacity. (2) fps computation vmaf->pic_cnt / elapsed produced 0.0/0.0 = NaN when pic_cnt == 0; Apple Clang may SIGFPE on the 0.0/0.0 division itself under a strict FP environment; guarded with explicit if (pic_cnt == 0 \|\| timer_elapsed == 0) fps = 0.0. (3) json_write_pool_score used j > 1 (pool method enum) as the comma-placement heuristic — wrong when the j==1 call is skipped; replaced with bool *first flag. (4) json_write_frames used i > 0 (frame index) as the separator heuristic — wrong when the first written frame has non-zero index; replaced with bool first_frame flag. | ADR-0606 | fix/macos-vmaf-write-output-segv-deep | MALLOC_PERTURB_=198 meson test -C core/build test_output test_public_api_score — 2/2 pass on Linux; macOS CI to verify. | (2026-05-19) | T-WINDOWS-STAT-COMPAT-INCLUDE-ORDER-2026-05-18 | Both Build — Windows MinGW64 (CPU) and Build — Windows MSVC + CUDA CI legs broken by the ADR-0521 stat compat macros in core/tools/yuv_input.c. (1) Macros #define stat __stat64 / #define fstat(fd, st) _fstat64(...) placed before #include <sys/stat.h> caused the preprocessor to macro-expand identifiers inside the system header. Under MinGW64 this redefined struct _stat64 from _mingw_stat64.h (redefinition error). Under MSVC + SDK 10.0.26100.0 + NVCC it triggered cascading C2059/C2143 syntax errors in ucrt/sys/stat.h. (2) Guard was #ifdef _WIN32, which fires for MinGW64, but MinGW already provides POSIX-compatible stat/fstat/S_ISREG natively. Fix (ADR-0575): move #include <sys/stat.h> before the macro block; change guard from #ifdef _WIN32 to #ifdef _MSC_VER. | ADR-0575 | fix/windows-ci-sdk-pin-22621 | CI run 26053401593 step "Build vmaf" (MinGW64) and step "Build libvmaf (CUDA)" (MSVC+CUDA) go green on the PR's CI run | (2026-05-18) | T-HIP-05-AUDIT-FINAL-VERIFY-2026-05-18 | Static-source audit of 9 HIP feature extractors listed by the HIP-05 audit as scaffold-ENOSYS stubs: ciede_hip, float_moment_hip, float_ansnr_hip, integer_motion_v2_hip, float_motion_hip, float_adm_hip, ssimulacra2_hip, integer_adm_hip, float_vif_hip. The audit was stale — a wave of kernel-promotion PRs (#1303–#1307) had promoted every one of them before this verification pass. All 9 have a .hip kernel source registered in hip_kernel_sources and a real #ifdef HAVE_HIPCC init/submit/collect path. hip_hsaco_stubs.c is now effectively empty (all weak stubs removed). Runtime verification evidence: float_ansnr_hip places=4 (ADR-0372), float_moment_hip delta=0.000000 (PR #1304), ssimulacra2_hip bit-exact after -ffp-contract=off fix (ADR-0539 PR #1306), integer_adm_hip diff=0.000000 on all 6 ADM features (ADR-0539 PR #1307). float_vif_hip build + link verified; runtime parity (HIP vs CPU, places=4) is a follow-up once stable ROCm device access is confirmed in the container. No porting work is required; the HIP-05 audit row is fully closed. | ADR-0563 | chore/hip-extractor-audit-verify-9 | grep -c "HAVE_HIPCC" core/src/feature/hip/{ciede,float_moment,float_ansnr,integer_motion_v2,float_motion,float_adm,ssimulacra2,integer_adm,float_vif}_hip.c — all non-zero; cat core/src/feature/hip/hip_hsaco_stubs.c — only the empty-macro comment (no live VMAF_HSACO_WEAK_STUB calls). | (2026-05-18) | T-VCQ-223-LOCAL-EXPLAINER-HANG-2026-05-18 | Root cause: CPU-bound libsvm sampling, not a deadlock. VmafQualityRunnerWithLocalExplainer._run_on_asset constructed LocalExplainer() with the default neighbor_samples=5000, producing ~480 000 svm_predict_values calls per run (wall time 4-8 min; CI timeout). Fix: fallback defaults to neighbor_samples=100; overridable via optional_dict["explainer_neighbor_samples"]. @unittest.skip("[VCQ-223]") removed; score assertions recalibrated. Wall time on dev machine: ~78 s. | cd python && PYTHONPATH=/path/to/vmaf/python python3 -m pytest test/local_explainer_test.py::QualityRunnerTest::test_run_vmaf_runner_local_explainer_with_bootstrap_model -v --timeout=120 — completes in <120 s and passes. | ADR-0562 | fix/vcq-223-local-explainer-hang | (2026-05-18) | T-HIP-VIF-PLACES3-GATE-INCORRECT-2026-05-18 | ADR-0537 documented per-feature places=3 as an "acceptable follow-up" gap in integer_vif_hip. This was incorrect: per-feature places=3 produces VMAF-score places=1 via SVM amplification (VIF scale coefficients 1.2-2.1 x 4 scales; worst-case delta 0.014 x 6.6 = 0.092 VMAF units, 920x the ADR-0214 tolerance of 1e-4). ADR-0566 supersedes the places=3 clause and formalises per-feature places=4 as the non-negotiable gate for all HIP VIF kernels. ADR-0552's wavefront reduction fix satisfies the gate. | ADR-0566 (supersedes ADR-0537 follow-up clause) | fix/hip-vif-svm-amplification-places4-gate | no runtime test needed — gate policy change only; runtime validation via ADR-0552's smoke test | (2026-05-18) | T-HIP-VIF-PARITY-PLACES4-2026-05-18 | integer_vif_hip horizontal accumulation kernels issued one atomicAdd per thread (128 per row per field). Non-deterministic CAS-retry ordering on AMD hardware introduced per-feature jitter 0.001-0.014 that the VMAF SVM amplified via VIF scale coefficients 1.2-2.1 x 4 scales to 0.031 VMAF-score divergence vs CPU — a 200x violation of ADR-0214's places=4 gate. Fix: replace per-thread atomics with a 64-lane __shfl_xor XOR-shuffle wavefront reduction + single lane-0 atomicAdd (ADR-0552). Also removes the early return for out-of-bounds threads (early return diverges the wavefront, causing __shfl_xor to read undefined register state from diverged lanes; out-of-bounds threads now carry zero-initialised accumulators through the reduction, which is neutral under integer addition). After fix: HIP VIF within places=4 of CPU on the BBB testdata fixture. | ADR-0552 | fix/hip-vif-deterministic-reduce | docker exec vmaf-dev-mcp /workspace/build-hip/core/tools/vmaf --backend hip --reference /workspace/testdata/ref_576x324_48f.yuv --distorted /workspace/testdata/dis_576x324_48f.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --json /tmp/hip.json — compare against CPU baseline VMAF=94.32301; delta must be within 1e-4 (places=4). | (2026-05-18) | T-HIP-GFX-TARGETS-FALLBACK-2026-05-18 | The HIP build's no-GPU-probe fallback was gfx90a only (CDNA2 server). Any libvmaf.so compiled in a build sandbox (BuildKit, CI) contained no HSACO blob for RDNA2/RDNA3 consumer GPUs, causing a runtime No compatible code objects found for: gfx1030 failure on the fork's dev host (AMD Raphael APU gfx1036, override-mapped to gfx1030). The fallback is widened to gfx90a,gfx1030,gfx1036,gfx1100 covering CDNA2 server + RDNA2 desktop + Raphael APU iGPU + RDNA3 desktop (ADR-0561). Builds where rocm_agent_enumerator or hipconfig succeeds are unaffected. | ADR-0561 | fix/hip-gfx-targets-fallback-widening | docker compose -f dev/docker-compose.yml build dev-mcp && docker exec vmaf-dev-mcp /workspace/build-hip/core/tools/vmaf --backend hip --reference /workspace/testdata/ref_576x324_48f.yuv --distorted /workspace/testdata/dis_576x324_48f.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --json /tmp/hip_gfx.json — must not emit No compatible code objects found. | (2026-05-18) | T-CODEC-ADAPTER-TWO-PASS-REAL-2026-05-18 | 14 of the 19 vmaf-tune codec adapters inherited the protocol-default two_pass_args body that raised NotImplementedError, so --two-pass against any codec other than libx264 / libx265 / libvpx-vp9 crashed at the contract surface. Fix (ADR-0546): implement two_pass_args for every adapter. libaom-av1 + libvvenc join the true two-invocation 2-pass set with supports_two_pass=True and FFmpeg's generic -pass N -passlogfile <prefix> pair. libsvtav1 returns VBR-mode argv but keeps supports_two_pass=False because SVT-AV1 forbids multi-pass in the harness-default CRF mode (verified against v4.1.0: Svt[error]: CRF does not support multi-pass. Use single pass.). NVENC / QSV / AMF adapters return single-invocation analysis flags (-multipass fullres / -extbrc 1 -look_ahead_depth 40 / -preanalysis true) — callers compose the pass-1 argv into EncodeRequest.extra_params for a quality-boosted single-pass encode. All four VideoToolbox adapters raise the new typed VideoToolboxTwoPassUnsupportedError (a NotImplementedError subclass) documenting the VTCompressionSession API limitation. +74 regression tests under tools/vmaf-tune/tests/test_codec_adapter_two_pass_real.py. | ADR-0546 | feat/codec-adapter-two-pass-real | cd tools/vmaf-tune && PYTHONPATH=src python -m pytest tests/test_codec_adapter_two_pass_real.py tests/test_codec_adapter_x265_two_pass.py tests/test_codec_adapter_libvpx.py (all green) | (2026-05-18)
| T-VULKAN-01-INTEGER-MOTION-VULKAN-IMPL-MISSING-2026-05-18 | vmaf_get_feature_extractor_by_name("integer_motion_vulkan") returned NULL on every Vulkan-enabled build even though the symbol vmaf_fex_integer_motion_vulkan_impl was declared extern in feature_extractor.c (line 99) and the source file core/src/feature/vulkan/integer_motion_vulkan.c stated "feature_extractor.c registers both". The entry was simply never added to feature_extractor_list[]. Fix: add &vmaf_fex_integer_motion_vulkan_impl to the #if HAVE_VULKAN block of feature_extractor_list[] in feature_extractor.c. | ADR-0546 | fix/audit-bundle-vulkan-saliency-modelcard | nm build-vk/libvmaf/libvmaf.so.3 \| grep integer_motion_vulkan must list both vmaf_fex_integer_motion_vulkan and vmaf_fex_integer_motion_vulkan_impl. | (2026-05-18) | T-SALIENCY-TUNE-01-SILENT-FALLBACK-UNSUPPORTED-CODEC-2026-05-18 | vmaf-tune recommend-saliency --saliency-aware --encoder h264_nvenc (and any of the 14 other codecs outside _SALIENCY_DISPATCH) silently produced a plain unbiased encode with only a WARNING log. Callers had no way to distinguish a successful ROI encode from the silent fallback without inspecting logs. Fix: raise SaliencyUnsupportedEncoderError (inherits from SystemExit(2)) by default; add opt-in --saliency-fallback-plain / VMAFTUNE_SALIENCY_FALLBACK_OK=1 to restore old behaviour (logs an ERROR instead of WARNING). | ADR-0546 | fix/audit-bundle-vulkan-saliency-modelcard | python -m pytest tools/vmaf-tune/tests/test_saliency_roi_codec.py::test_saliency_aware_encode_unsupported_encoder_raises_by_default tools/vmaf-tune/tests/test_saliency_roi_codec.py::test_saliency_aware_encode_unsupported_encoder_env_override_allows_fallback -v | (2026-05-18) | T-AI-01-MODEL-CARD-PLACEHOLDER-SIGSTORE-2026-05-18 | predictor_train.py::_write_model_card() emitted the literal string "PLACEHOLDER — the synthetic stub ships unsigned" in the Sigstore signing note of every synthetic-stub card. The word PLACEHOLDER was a development artifact that was never replaced; readers had no indication whether the field was intentionally blank or erroneously unfilled. Fix: replace with "not applicable (synthetic-stub model card; production models are signed via Sigstore — see docs/development/release.md)". Add --emit-stub-card-only smoke-test hook. | ADR-0546 | fix/audit-bundle-vulkan-saliency-modelcard | python -m pytest tools/vmaf-tune/tests/test_predictor_train.py::test_emit_stub_card_only_does_not_contain_placeholder -v | (2026-05-18) | T-DEV-CONTAINER-SYCL-HIP-RUNTIME-2026-05-18 | vmaf-dev-mcp SYCL + HIP backends both silently fell back to CPU on Linux 7.0.x hosts. SYCL: NEO 25.18 from Intel's noble/unified APT repo (newest as of 2026-05-18) does not understand the i915 / xe UAPI shipped by kernel ≥ 7.0 → zeInit() returns ZE_RESULT_ERROR_UNINITIALIZED (0x78000001); sycl-ls shows Platforms: 0 and clinfo shows Number of platforms 0. Additionally the Intel CPU OpenCL ICD (/opt/intel/oneapi/compiler/latest/lib/libintelocl.so) silently fails to load because the previous LD_LIBRARY_PATH omitted ${ONEAPI_ROOT}/tbb/latest/lib (libintelocl dlopens libtbb.so.12 at platform-enumeration). HIP: ROCm 6.4 userspace running against kernel ≥ 7.0 KFD returns Unable to open /dev/kfd read-write: Invalid argument from rocminfo because the KFD ioctl ABI revs across ROCm major versions. Fix (ADR-0543): pin Intel NEO 26.18.38308.1 via GitHub releases (release-notes-mandated IGC v2.34.4 + gmmlib 22.10.0 pinned via Containerfile ARGs); bump ROCm 6.4 → 7.2.3 via the existing AMD apt repo (matches Arch host); add tbb/latest/lib to LD_LIBRARY_PATH; add a runtime-visibility probe in dev-mcp-entrypoint.sh that surfaces missing SYCL/HIP devices in the container banner so future host-kernel ABI mismatches show up in ≤ 30 s instead of as CPU-scores-under-a-GPU-tag. | ADR-0543 | fix/dev-container-sycl-hip-runtime | docker exec vmaf-dev-mcp bash -c 'for B in sycl hip; do vmaf --reference /workspace/python/test/resource/yuv/src01_hrc00_576x324.yuv --distorted /workspace/python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --backend $B --json --output /tmp/be_$B.json; done' — both must report real GPU dispatch (SYCL within places=5 of CPU per ADR-0214; HIP within places=4). | (2026-05-18) | T-HIP-SSIMULACRA2-BLUR-FMAD-2026-05-18 | vmaf --backend hip --feature ssimulacra2 risked drifting past the ADR-0214 places=2 cross-backend parity gate because ssimulacra2_blur.hip was being compiled by hipcc with the default -ffp-contract=fast. hipcc / amdclang++ then silently FMA-fused the recursive Gaussian IIR step (n2 * sum - d1 * prev) on the device side, which shifts the pole cascade away from CPU-exact within a few pyramid levels. The CUDA twin already disables this via cuda_cu_extra_flags : ['-Xcompiler=-ffp-contract=off', '--fmad=false'] (for both ssimulacra2_blur and float_adm_score); the HIP HSACO build pipeline had no per-kernel flag dispatch mechanism — every kernel got the same flat command line. Fix: introduce a hip_cu_extra_flags dict in core/src/meson.build mirroring the CUDA pattern, with first entry 'ssimulacra2_blur' : ['-ffp-contract=off']. Fall-through (.get(name, [])) is byte-identical for any kernel not listed. Verified on AMD gfx1036 (iGPU) inside vmaf-dev-mcp-stdio: on the Netflix golden 576×324 pair, ssimulacra2 / integer_ssim / integer_ms_ssim HIP scores match CPU to display precision (6 decimal places) — well inside the ADR-0214 places=2 gate. The kernel ports themselves are not new (shipped via PR #999, #1000, #1013); the gap closed here is the missing per-kernel flag wiring. | ADR-0539 | feat/hip-ssim-family-kernels-real | docker exec vmaf-dev-mcp-stdio bash -c 'cd /tmp/build-hip && for FEAT in ssimulacra2 integer_ssim integer_ms_ssim; do for BACK in cpu hip; do LD_LIBRARY_PATH=./src ./tools/vmaf -r $SRC -d $DST -w 576 -h 324 -p 420 -b 8 --backend $BACK --feature $FEAT -n --json --output /tmp/${BACK}_${FEAT}.json; done; done' — outputs match across backends. | (2026-05-18) | T-HIP-INTEGER-ADM-KERNELS-REAL-2026-05-18 | ADR-0533 (PR #1292) wired the integer ADM HIP extractor into the dispatch table, but the four .hip kernel sources it depends on did not build standalone via hipcc --genco: adm_dwt2.hip and adm_csf.hip compiled but were never registered in hip_kernel_sources; adm_csf_den.hip had a malformed first kernel (missing template <...> + __device__ __forceinline__ void qualifiers); adm_cm.hip was wrapped in an unterminated #if defined(COMPARE_FUSED_SPLIT) block and referenced CUDA-only helpers (uint64_cu, warp_reduce, VMAF_CUDA_THREADS_PER_WARP, __float2uint_ru, __log2f). ADR-0536 (PR #1296) had papered over the missing strong symbols with four weak HSACO fallbacks in hip_hsaco_stubs.c; at runtime hipModuleLoadData on the empty blobs returned non-zero and the extractor silently fell back to CPU. Fix (ADR-0539): port the four kernels to compile standalone — replace warp_reduce + per-warp atomicAdd with per-thread atomicAdd on the uint64 accumulator (bit-exact since uint64 add is associative; same pattern as vif_statistics.hip ADR-0537), swap __log2f / __float2uint_ru for portable ceilf(log2f(...)), fix the missing template declarations, complete the unterminated #if block. Register the four kernels in hip_kernel_sources and remove their entries from hip_hsaco_stubs.c. End-to-end on AMD gfx1036: vmaf --backend hip --feature adm produces ADM scores bit-exact vs CPU on the Netflix golden src01 pair (diff = 0.000000 across all six emitted features: integer_adm, integer_adm2, integer_adm3, integer_adm_scale[0-3]). | ADR-0539 | feat/hip-integer-adm-kernels-real | LD_LIBRARY_PATH=core/build-hip-adm/src core/build-hip-adm/tools/vmaf --reference python/test/resource/yuv/src01_hrc00_576x324.yuv --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --backend hip --feature adm --json --output /tmp/hip_adm.json → all integer_adm* means match CPU diff=0.000000 | (2026-05-18) | T-DNN-NR-MODEL-AUTO-RESIZE-2026-05-18 | Post-fix probe Finding 11: vmaf --no-reference --tiny-model nr_metric_v1.onnx --distorted 576x324.yuv hard-errored at frame 0 with -ERANGE because the per-frame NCHW dispatch (vmaf_ctx_dnn_run_frame_nchw in core/src/libvmaf.c) required exact dimension match between the user-supplied frame and the model's static input shape. Result: 0 frames scored, empty JSON frames array, silent footer. Fix (ADR-0550): per-frame dispatch auto-resamples the luma plane to the model dims when they differ, using a deterministic separable filter (bilinear / nearest / bicubic). Default is DISABLED (mismatch → -ERANGE); the operator must explicitly pass --tiny-resize bilinear (or nearest/bicubic) to enable auto-resize. DISABLED-as-default avoids a silent free parameter: bilinear/nearest/bicubic produce scores differing by ~2% on the same input. Matched-dims path forwards verbatim to vmaf_tensor_from_luma so the Netflix golden gate is bit-identical. +5 C unit tests in test_tensor_io.c. Verify: vmaf --no-reference --tiny-model model/tiny/nr_metric_v1.onnx --distorted testdata/dis_576x324_48f.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --tiny-resize bilinear --json --output /tmp/nr.json -> 48 frames, vmaf_tiny_model mean=3.0888 (bilinear), 3.0536 (nearest), 3.1079 (bicubic). Without --tiny-resize: 0 frames + "problem reading pictures" (DISABLED default). | ADR-0550 | feat/tiny-model-auto-resize | meson test -C core/build --suite=dnn (12/12 green incl. 5 new test_resize_*) | (2026-05-18) | T-VMAF-TUNE-COMPARE-PREMIUM-VMAF-TARGETS-2026-05-18 | vmaf-tune compare shipped streaming-tier --target-vmafs 75,80,85,90,93 defaults in ADR-0534 / PR #1293 (just merged), but the fork's primary user encodes archival masters at VMAF >= 95 exclusively ("I never have encoded stuff below 95"); the streaming sweep produced an R-Q chart with no points in the user's actionable range. ADR-0534's "VMAF >= 95 is unreachable" caveat was itself a bisect-harness bug: bisect_target_vmaf defaulted its search window to the codec adapter's narrow quality_range (e.g. libx265 = (15, 40), libsvtav1 = (20, 50)) and adapter.validate rejected CRFs outside that window outright, so even with the search window widened the adapter validator throttled the bisect to the informative band. Fix (ADR-0538, supersedes ADR-0534's target defaults): (A) flip --target-vmafs default to premium-archival 94,96,97,98; (B) introduce _ABSOLUTE_CRF_RANGE_BY_NAME and _absolute_crf_range(adapter) in bisect.py; the bisect now defaults to the encoder's accepted CRF range (libx264 / libx265 -> 0..51, libvpx-vp9 / libaom-av1 / libsvtav1 -> 0..63) when the caller passes no crf_range; (C) bypass adapter.validate's CRF gate inside _encode_and_score in favour of an explicit absolute-range check (preset validation unchanged); (D) corpus.py's adapter.validate path is untouched — only the bisect search loop is widened. +4 regression tests across test_bisect.py (libx264 / libx265 / libsvtav1 premium-archival targets ok=true with achieved >= target - 0.5; default-search-window assertion) + 1 in test_compare_rate_quality_sweep.py (defaults are 94,96,97,98). | ADR-0538 (supersedes ADR-0534 target-VMAF defaults) | fix/premium-vmaf-target-defaults | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_bisect.py tools/vmaf-tune/tests/test_compare_rate_quality_sweep.py | (2026-05-18) | T-ADR-0498-ENFORCEMENT-HARDENING-2026-05-18 | ADR-0498's explicit-backend gate in core/tools/vmaf.c was partially enforced — the refusal message printed but had three sharp edges that left downstream consumers without clean signals: (1) init_gpu_backends returned -1 which POSIX-truncated to exit byte 255, indistinguishable from generic non-zero returns; CI gates had to grep stderr for the ADR-0498 marker string to tell backend failures from other errors. (2) The --output X.json file was either never written or left as a 0-byte file (consumer pre-touched the path) — tooling that expected a JSON descriptor crashed with a parse error instead of a structured signal. (3) Features named *_cuda / *_sycl / *_vulkan / *_hip / *_metal are GPU-pinned variants; when the matching backend wasn't active, vmaf_use_feature silently registered the CPU twin and produced scores that looked identical to an explicit-backend invocation but were computed on the wrong silicon — exactly the silent-fallback bug ADR-0498 banned for --backend NAME. Fix (ADR-0543, extends ADR-0498): define VMAF_EXIT_BACKEND_INIT_FAILED = 100 + sentinel VMAF_INIT_GPU_EXPLICIT_FAIL = -100 so explicit-backend failures exit with a dedicated public-contract code; add write_backend_error_json helper that overwrites the --output path with a single-line {"error", "backend_requested", "errno", "adr", "exit_code"} JSON descriptor when format is JSON; add feature_backend_suffix + backend_active helpers and wire a per-feature gate into the feature-loading loop that hard-fails GPU-pinned feature names when the matching backend isn't active; propagate CUDA's active flag out of init_gpu_backends via a new bool *cuda_active_out parameter so the per-feature gate and the existing backend_used JSON echo can see it directly. 13 new Python integration tests under tools/vmaf-tune/tests/test_adr_0543_backend_enforcement.py (parametrised exit-code + JSON-descriptor coverage across SYCL / HIP / Vulkan / CUDA / Metal, dedicated per-feature gate test, plus two source-level guards that grep vmaf.c for the helper invocation on every backend keyword so a refactor can't silently revert one). | ADR-0543 (extends ADR-0498) | fix/adr-0498-enforcement | VMAF_BIN_FOR_TESTS=core/build/tools/vmaf PYTHONPATH=tools/vmaf-tune/src python3 -m pytest tools/vmaf-tune/tests/test_adr_0543_backend_enforcement.py (13/13 green); CPU-only repro: core/build/tools/vmaf --backend sycl --reference REF --distorted DIST --width 576 --height 324 -p 420 -b 8 --json --output /tmp/x.json → rc=100, JSON contains {"error": "backend not compiled into this libvmaf", "backend_requested": "sycl", "errno": 0, "adr": "ADR-0498", "exit_code": 100}. | (2026-05-18) | T-VULKAN-METAL-DEAD-SCAFFOLDS-2026-05-18 | Seven Vulkan .c files (float_moment_vulkan.c, integer_vif_vulkan.c, integer_cambi_vulkan.c, integer_moment_vulkan.c, integer_ssim_vulkan.c, integer_ms_ssim_vulkan.c, integer_psnr_hvs_vulkan.c) and 11 Metal .mm files (float_adm_metal, float_vif_metal, integer_adm_metal, integer_cambi_metal, integer_ciede_metal, integer_moment_metal, integer_ms_ssim_metal, integer_psnr_hvs_metal, integer_ssim_metal, integer_vif_metal, ssimulacra2_metal) plus their paired .metal / .comp kernels lived in core/src/feature/{vulkan,metal}/ but were never wired into core/src/{vulkan,metal}/meson.build. Some duplicated already-wired siblings' VmafFeatureExtractor symbols (float_moment_vulkan.c vs moment_vulkan.c; integer_vif_vulkan.c vs vif_vulkan.c) — silent duplicate-definition if anyone added them to meson. Others were pure orphans with no extern declaration. Critically, float_ms_ssim_metal.mm (ADR-0490 Accepted) had its vmaf_fex_float_ms_ssim_metal symbol referenced in feature_extractor_list[] while the defining TU was missing from meson — a latent link-time failure on macOS Metal builds. The dead extern vmaf_fex_integer_adm_metal in feature_extractor.c had no registry slot either (orphan extern). Fix: wire float_ms_ssim_metal.mm + float_ms_ssim.metal into core/src/metal/meson.build (closes the ADR-0490 wiring gap); delete the 18 dead source files, their 14 orphan paired shaders, and the orphan extern; refresh core/src/feature/{vulkan,metal}/AGENTS.md with the rebase-sensitive "no dead scaffolds" invariant. Net: ~3500 LOC removed, +12 LOC wiring. | ADR-0545 | chore/wire-or-delete-dead-extractor-files | ls core/src/feature/vulkan/*.c \| xargs -n1 basename \| sort > /tmp/dir.txt && grep -oE '[a-z0-9_]+_vulkan\.c' core/src/vulkan/meson.build \| sort -u > /tmp/wired.txt && comm -23 /tmp/dir.txt /tmp/wired.txt returns empty (all sources in dir are wired). Same check on core/src/feature/metal/*.mm. | (2026-05-18) | T-HIP-INTEGER-MOMENT-HSACO-UNRESOLVED-2026-05-18 | enable_hipcc=true HIP build failed to link integer_moment_score_hsaco — the symbol that integer_moment_hip.c consumes via hipModuleLoadData. The corresponding kernel source core/src/feature/hip/integer_moment/moment_score.hip was already on disk with the real kernel implementation, but the meson hip_kernel_sources dict did not register a key that emits the integer_moment_score_hsaco symbol (the existing moment_score key resolves to hip/float_moment/moment_score.hip for the float twin; the two .hip files contain distinct kernel entry points and are not interchangeable). No weak stub backed it either — hip_hsaco_stubs.c only covers the four ADM kernels per ADR-0536. Fix: add 'integer_moment_score' : feature_src_dir + 'hip/integer_moment/moment_score.hip' with a clarifying inline comment alongside the float-twin entry. Verified psnr (psnr_y/cb/cr), psnr_hvs (psnr_hvs / psnr_hvs_y/cb/cr) and float_moment (*_ref1st/dis1st/ref2nd/dis2nd) HIP scores bit-exact vs CPU on the Netflix src01_hrc00 ↔ src01_hrc01 576x324 pair (delta = 0.000000 on every emitted feature). All three integer-domain PSNR / PSNR-HVS / moment HIP extractors now resolve via real HSACO blobs — no weak stubs. | ADR-0539 | feat/hip-psnr-moment-kernels-real | meson setup /tmp/build-hip /home/kilian/dev/vmaf/libvmaf -Denable_hip=true -Denable_hipcc=true && ninja -C /tmp/build-hip (839/839 targets link clean); /tmp/build-hip/tools/vmaf --reference python/test/resource/yuv/src01_hrc00_576x324.yuv --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --backend hip --feature float_moment --json --output /tmp/hip_fm.json → float_moment_ref1st/dis1st/ref2nd/dis2nd bit-exact vs CPU. | (2026-05-18) | T-DEV-CONTAINER-FULL-GPU-PLUMBING-2026-05-18 | After ADR-0509 / ADR-0514 / ADR-0528 / ADR-0540 the dev-mcp container still produced silent CPU fallback or software emulation on three of the four GPU backends when empirically measured against the dev machine (NVIDIA RTX 4090 + Intel Arc A380 + AMD gfx1036). (A) vmaf --backend vulkan --vulkan_device 0 landed on mesa's lavapipe software ICD because lavapipe sorted before the real GPU ICDs in the loader's directory walk; (B) vmaf --backend sycl reported Platforms: 0 because the Intel compute-runtime's VA-API dlopen chain needed intel-media-va-driver-non-free / mesa-va-drivers that were not installed; (C) vmaf --backend hip failed at hsa_init() (HSA_STATUS_ERROR_OUT_OF_RESOURCES) because gfx1036 is not on the ROCm 6.x supported-GPU allowlist; (D) the NVIDIA_DRIVER_CAPABILITIES=…,graphics,… requirement for the NVIDIA Vulkan ICD bind-mount was undocumented. Fix (ADR-0542): (A) dev/scripts/dev-mcp-entrypoint.sh rewrites VK_DRIVER_FILES at startup to the colon-separated list of non-lavapipe ICD JSONs, filtering lavapipe out whenever any real ICD is visible; (B) dev/Containerfile stage 1 picks up intel-media-va-driver-non-free + mesa-va-drivers; (C) dev/docker-compose.yml common-env pins HSA_OVERRIDE_GFX_VERSION=10.3.0 + HSA_ENABLE_SDMA=0 + ROCR_VISIBLE_DEVICES=0; (D) inline documentation comment on NVIDIA_DRIVER_CAPABILITIES in the compose file + new dev/AGENTS.md invariant section. All five --backend {cpu, cuda, sycl, vulkan, hip} lanes now return a real GPU score with no silent CPU / lavapipe fallback. | ADR-0542 | fix/dev-container-full-gpu-plumbing | docker compose --project-directory $(git rev-parse --show-toplevel) -f dev/docker-compose.yml build dev-mcp && docker compose --project-directory $(git rev-parse --show-toplevel) -f dev/docker-compose.yml up -d then docker exec vmaf-dev-mcp bash -c 'SRC=/workspace/python/test/resource/yuv/src01_hrc00_576x324.yuv; DST=/workspace/python/test/resource/yuv/src01_hrc01_576x324.yuv; unset VK_ICD_FILENAMES; for B in cpu cuda sycl vulkan hip; do vmaf --reference $SRC --distorted $DST --width 576 --height 324 --pixel_format 420 --bitdepth 8 --backend $B --json --output /tmp/be_$B.json; done' → no Using CPU line on any backend. | (2026-05-18) | T-FEATURE-EXTRACTOR-LIST-DUPLICATE-REGISTRATIONS-2026-05-18 | core/src/feature/feature_extractor.c's static feature_extractor_list[] registered 61 extractor symbols more than once: the HAVE_VULKAN block held 67 entries instead of 18 (vmaf_fex_psnr_hvs_vulkan and vmaf_fex_float_ms_ssim_vulkan 11× each, seven other Vulkan twins 6× each); the HAVE_SYCL block held 17 entries instead of 11 (six twins registered 2× each). vmaf_get_feature_extractor_by_name() is first-match and hid the bug from CLI callers, but the ctx-pool's get_fex_list_entry allocates one entry per registered pointer when the by-name iterator dispatches through the registry, causing each affected extractor's init/extract/flush to run 2–11× per picture. Plausible root cause for the v9 CHUG "VMAF=99 across all bitladders" anomaly previously attributed to operational misuse (closed in PR #1270 against extract_k150k_features.py). Fix: hand-edit feature_extractor_list[] so each &vmaf_fex_* appears exactly once; add vmaf_feature_extractor_list_audit() (O(N²) one-shot pointer + name equality walk; N≈60) called from vmaf_init() so the bug class fails fast on every future build; new meson test test_feature_extractor::test_feature_extractor_list_no_duplicates exercises the audit on the live registry. CUDA / HIP / Metal / CPU blocks were already clean and were left untouched. Touched-file lint clean (clang-format dry-run --Werror). | ADR-0544 | fix/feature-extractor-list-dedup | meson setup core/build-dedup libvmaf -Denable_cuda=false -Db_lto=false && ninja -C core/build-dedup && meson test -C core/build-dedup test_feature_extractor (1/1 pass; full suite 64/64 pass); audit log shows 0 duplicates after the dedup. | (2026-05-18) | T-HIP-FLOAT-VIF-STUB-REMOVAL-2026-05-18 | PR #1303 added a __attribute__((weak)) const unsigned char float_vif_score_hsaco[1] = {0}; stub to core/src/feature/hip/hip_hsaco_stubs.c so the link succeeded when the HSACO blob was not built. The real HIP kernel for float_vif has shipped at core/src/feature/hip/float_vif/float_vif_score.hip since ADR-0379 / PR #1025 (2026-03-29) and is registered in hip_kernel_sources in core/src/meson.build. With enable_hipcc=true, both definitions exist: a strong xxd-embedded blob from the real .hip plus the weak 1-byte stub. The linker resolves to the strong symbol but emits -Wlto-type-mismatch on every build. User direction was "no stubs anywhere" for kernels with real backends. Fix (ADR-0539): delete the VMAF_HSACO_WEAK_STUB(float_vif_score_hsaco) line from the stubs TU; replace with a one-line comment citing the ADR. The remaining 11 stubs (4 ADM + 2 ssimulacra2 + 5 others) stay until their .hip sources port. Verified: meson setup /tmp/build --wipe -Denable_hip=true -Denable_hipcc=true && ninja src/float_vif_score.hsaco produces a 17 824-byte HSACO; full lib link succeeds; no more float_vif_score_hsaco LTO warning (the four other stub-vs-real mismatches for cambi_score_hsaco, vif_statistics_hsaco, psnr_score_hsaco, ms_ssim_score_hsaco remain — tracked as separate follow-ups). End-to-end runtime parity against CPU could not be verified on the dev container (HIP vmaf_hip_state_init returns -19/-ENODEV regardless of kernel — the container lacks ROCm device access — but build + link is the unit of work this PR delivers). | ADR-0539 | feat/hip-float-vif-score-kernel-real | docker exec vmaf-dev-mcp bash -c 'cd /workspace/.claude/worktrees/agent-af027d20cd7c88136/libvmaf && meson setup /tmp/build-hip-wt --wipe -Denable_hip=true -Denable_hipcc=true && ninja -C /tmp/build-hip-wt 2>&1 \| grep -E "float_vif_score_hsaco.*type-mismatch" \| wc -l' → 0 | (2026-05-18)
| T-HIP-INTEGER-MOTION-FLAG-PROMOTION-2026-05-18 | Follow-up to T-HIP-IMPORT-STATE-ENOSYS / ADR-0519 and T-HIP-INTEGER-MOTION-UNREGISTERED / ADR-0523 (PR #1283). PR #1283 registered the extractor and added the source file to meson; this PR promotes the flag bit, adds the dispatch wiring needed for the model-driven path to actually pick it, and lands the four runtime fixes required for vmaf --backend hip --feature integer_motion to produce a valid VMAF score: (1) VMAF_FEATURE_EXTRACTOR_HIP set on vmaf_fex_integer_motion_hip, (2) compute_fex_flags() returns the HIP slot when a HIP state is imported, (3) vmaf_get_feature_extractor_by_feature_name() falls back to unflagged extractors when the preferred-flag pass misses (so partial HIP coverage doesn't break the default model), (4) flush_context_serial() drains HIP gpu_pending final-frame collect (mirrors flush_context_sycl), (5) integer_motion_hip collect/flush writes route through vmaf_feature_collector_append_with_dict() so the encoded key matches what vmaf_predict_score_at_index looks up, (6) VMAF_PICTURE_BUFFER_TYPE_HIP_DEVICE enum entry added for the future HIP picture pool. vmaf_fex_integer_vif_hip was speculatively flagged in batch-1 but crashes with a GPU memory access fault on the first frame when the dispatch actually picks it; un-flagged with an inline citation pending a kernel-level fix. End-to-end on AMD gfx1036: VMAF=76.7125 (CPU baseline 76.6678, delta=0.045 — within ADR-0214 places=4 cross-backend gate); AMD_LOG_LEVEL=3 shows 48 hipModuleLaunchKernel(calculate_motion_score_kernel_8bpc) launches per 48-frame clip, confirming the HIP kernel actually executes. | ADR-0530 (extends ADR-0519 + ADR-0523) | feat/hip-feature-flag-promotion | docker exec vmaf-dev-mcp-stdio /tmp/build-hip-worktree/tools/vmaf --reference /workspace/python/test/resource/yuv/src01_hrc00_576x324.yuv --distorted /workspace/python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --backend hip --feature integer_motion --json --output /tmp/hip.json → VMAF=76.7125; meson test test_hip_smoke → 24/24 pass (added test_integer_motion_hip_extractor_registered + test_integer_motion_hip_dispatch_picks_hip). | (2026-05-18) | T-HIP-DEAD-CODE-EXTRACTORS-2026-05-18 | Six HIP feature extractors (vmaf_fex_float_vif_hip, vmaf_fex_integer_adm_hip registering as adm_hip, vmaf_fex_integer_ms_ssim_hip, vmaf_fex_psnr_hvs_hip, vmaf_fex_integer_ssim_hip, vmaf_fex_ssimulacra2_hip) shipped as VmafFeatureExtractor definitions under core/src/feature/hip/*.c and mirrored their CUDA twins call-graph-for-call-graph, but were missing from core/src/hip/meson.build's hip_sources list and from the extern + registry blocks in core/src/feature/feature_extractor.c. vmaf_get_feature_extractor_by_name(<name>) returned NULL for every name; the CLI's --feature <name> plumbing, the vmaf_use_feature C-API path, and the MCP pick_features surface all failed at the registry-lookup step before reaching the backend runtime. Generalisation of T-HIP-INTEGER-MOTION-UNREGISTERED (ADR-0523) — same bug class, six more TUs. The three legacy plumbing TUs (adm_hip.c, vif_hip.c, motion_hip.c) carry no VmafFeatureExtractor struct and were correctly excluded; two rename-scaffold duplicates (integer_ciede_hip.c, integer_moment_hip.c) are stale and would cause a duplicate-symbol link error if included (their canonical TUs ciede_hip.c / float_moment_hip.c already register the same extractors). Fix: add the six files to hip_sources and add the matching extern + &vmaf_fex_* rows inside the #if HAVE_HIP blocks; pin the registration in core/test/test_hip_smoke.c with one assertion per extractor. Scaffold-init posture preserved — init() returns -ENOSYS unless enable_hipcc=true. | ADR-0533 | feat/hip-register-all-extractors | docker exec vmaf-dev-mcp bash -c 'cd /build/vmaf && meson test --suite=fast --no-rebuild test_hip_smoke -v 2>&1 \| tail -20' | (2026-05-18) | T-VMAF-TUNE-COMPARE-RATE-QUALITY-CHART-FROM-BISECT-SAMPLES-2026-05-18 | Two follow-on bugs to T-VMAF-TUNE-COMPARE-RATE-QUALITY-SWEEP-2026-05-18 (ADR-0516) surfaced in the BBB 4K v10 report. (A) Default --target-vmafs 85,90,92,95 was a premium-archival sweep that ignored the broadcast / low-bandwidth streaming operating range (VMAF 70-90) and frequently produced "unreachable" failure rows at 95+ because the codec's CRF ceiling cannot push 4K high-motion content past ~94-95. (B) The rate-quality chart connected per-codec picked-CRF points across (codec, target) pairs, but the bisect overshoots / undershoots each target by a different amount — producing physically impossible downward slopes (e.g. libx265 BBB v10: target 92 → achieved 90.5, then target 95 → achieved 95.3, drawing a downward line that read as "more bitrate → less VMAF"). Fix (ADR-0534): (A) flip the default to 75,80,85,90,93; preserve v1 single-target back-compat via a _TrackedDefaultAction sentinel that detects "--target-vmaf NN explicit + --target-vmafs default" and routes to the legacy v1 path. (B) extend the v2 schema with an optional additive bisect_samples: [{crf, bitrate_kbps, vmaf_score, encode_time_ms}, ...] row field carrying every probe the underlying bisect already computed; _sweep_plot_fn aggregates samples per codec across all targets, dedupes by CRF, sorts by bitrate, and plots a genuine monotonic R-Q curve with the picked-CRF rows overlaid as larger circled markers. Pareto frontier stays as the dashed overlay. Old v2 JSONs without samples (and v1 JSONs) render via the legacy connect-the-dots line with a caveat note in the chart title. CSV intentionally drops the structured column. +10 regression tests under test_compare_rate_quality_sweep.py (bisect records samples, to_row gating, JSON round-trip, CSV drop, ingest parse + back-compat, chart phrasing both modes, default value, v1 back-compat). | ADR-0534 | fix/compare-rate-quality-chart-from-bisect-samples | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_compare_rate_quality_sweep.py tools/vmaf-tune/tests/test_bisect.py tools/vmaf-tune/tests/test_compare.py tools/vmaf-tune/tests/test_report.py (all green) | (2026-05-18) | T-FFMPEG-PATCH-0007-AOM-ROI-FIELDS-NONEXISTENT-2026-05-18 | The fork's ffmpeg-patches/0007-libvmaf-tune-qpfile-unified.patch adds a qpfile_aom_apply_roi helper to libavcodec/libaomenc.c that references aom_roi_map_t fields (enabled, skip, ref_frame, delta_qp_enabled) that DO NOT EXIST in any released libaom version. Verified 2026-05-18 against the libaom v3.11.0 source at aom/aomcx.h:1609 — the public struct only carries roi_map, rows, cols, delta_q, delta_lf, static_threshold. The patch was likely written against a libaom development branch whose ROI API never shipped. Latent because no prior dev-image enabled --enable-libaom; surfaced during ADR-0543's first libaom-enabled FFmpeg build. Disposition (this PR): skip libaom in the in-image FFmpeg (--enable-libaom deliberately omitted; SVT-AV1 covers the production AV1 lane). Follow-up (not in scope): fix ffmpeg-patches/0007 to either (a) target a libaom version that actually ships the assumed ROI fields, or (b) gate the ROI bridge behind a libaom-version probe (mirror the SVT-AV1 >= 1.6.0 log-and-continue pattern already in the patch). Once 0007 is fixed, re-enable --enable-libaom in dev/Containerfile and re-introduce the source build at a libaom version compatible with the chosen ROI surface. | none yet (follow-up tracked) | feat/dev-container-ffmpeg-av1-hwaccel (skipping libaom only) | docker exec vmaf-dev-mcp ffmpeg -hide_banner -encoders 2>&1 \| grep -E "libaom\|libsvtav1" shows only libsvtav1 (libaom intentionally absent). | (2026-05-18 — surfaced, scope deferred) | T-HIP-INTEGER-MOTION-HSACO-WIRING-2026-05-18 | core/src/feature/hip/integer_motion_hip.c referenced motion_score_hsaco (declared extern in integer_motion_hip.h) but the matching .hip kernel source at core/src/feature/hip/integer_motion/motion_score.hip was never wired into the hip_kernel_sources dict in core/src/meson.build. Every build with enable_hip=true + enable_hipcc=true (the dev-MCP container's default) failed at link time with undefined reference to motion_score_hsaco, blocking the entire libvmaf build before either the ffmpeg stage or the build-time backend probes could run. Surfaced by ADR-0543's container rebuild verification — pre-existing master regression that ADR-0523 introduced (the registration was added but the kernel was not). Fix: add the missing 'motion_score' : feature_src_dir + 'hip/integer_motion/motion_score.hip' entry to the dict immediately after the motion_v2_score entry; meson's custom_target already iterates the dict so no new wiring is needed. | ADR-0523 follow-up bundled into ADR-0543 | feat/dev-container-ffmpeg-av1-hwaccel | docker compose --project-directory $(git rev-parse --show-toplevel) -f dev/docker-compose.yml build dev-mcp — proceeds past stage 3 libvmaf link (was previously FAILED: src/libvmaf.so.3.0.0 ... undefined reference to motion_score_hsaco). | (2026-05-18) | T-DEV-CONTAINER-FFMPEG-AV1-HWACCEL-ENCODERS-2026-05-18 | vmaf-dev-mcp container's in-image FFmpeg was built with only four video encoder flags (--enable-libx264 --enable-libx265 --enable-libvpx --enable-libdav1d) — libdav1d is an AV1 decoder, not an encoder, so the effective set was libx264 / libx265 / libvpx-vp9. The BBB e2e v10 vmaf-tune compare sweep dropped every other encoder with the stable hardware encoder not available: <enc> not compiled into ffmpeg skip string, leaving the three GPUs (RTX 4090 + Intel Arc A380 + AMD gfx1036) unused for encode even after ADR-0514 had unlocked their kernel dispatch for libvmaf scoring. The probe is correct: tools/vmaf-tune/src/vmaftune/compare.py::probe_encoder_available greps ffmpeg -encoders and emits the row-level skip rather than aborting; the fix is on the container side. Fix: stage 3.5 of dev/Containerfile adds libvpl-dev (apt), builds SVT-AV1 v2.1.0 + Fraunhofer VVenC v1.12.0 from source and vendors AMD AMF v1.4.36 headers under /usr/local, and the FFmpeg configure line is extended with --enable-libsvtav1 --enable-libvvenc --enable-nvenc --enable-cuda-nvcc --enable-libvpl --enable-amf --disable-filter=amf_capture. A build-time encoder probe at the end of stage 3.5 walks the 14 promised encoders and prints WARN <encoder> missing lines on any compile-in regression (mirrors the ADR-0514 backend-probe pattern). dev/AGENTS.md documents new invariants ("FFmpeg encoder exposure invariants"); docs/development/dev-mcp.md carries the new encoder matrix + per-encoder runtime-failure-mode table + full-sweep reproducer. Notable omissions versus a hypothetical "all encoders" ideal: --enable-libaom is deferred behind the patch-0007 libaom-ROI fix (see sibling row T-FFMPEG-PATCH-0007-AOM-ROI-FIELDS-NONEXISTENT), --enable-libnpp is omitted because FFmpeg n8.1.1's NPP support tops out at CUDA 12.x and the image tracks CUDA 13.x, --disable-filter=amf_capture because AMF v1.4.36's DisplayCapture.h uses C++ extern "C" blocks that FFmpeg compiles as plain C. Runtime availability of *_amf still requires the proprietary libamfrt64.so from amdgpu-pro userspace bind-mounted at run-time — documented as a per-encoder skip case, not a container regression. | ADR-0543 | feat/dev-container-ffmpeg-av1-hwaccel | docker exec vmaf-dev-mcp ffmpeg -hide_banner -encoders 2>&1 \| grep -E "libsvtav1\|libvvenc\|libvpx-vp9\|nvenc\|qsv\|amf\|vpl" \| head -20 should list 14 encoders (CPU lanes always present; per-vendor hardware lanes present iff the matching userspace driver was bind-mounted at container start). | (2026-05-18) | T-CLI-PARSE-TEST-STDERR-PIPE-DRAIN-2026-05-18 | core/test/test_cli_parse_long_only_args::test_threads_invalid_optarg_does_not_assert (regression test for ADR-0316 / ADR-0438 long-only short-option synthesis) was failing on master. Product code itself was correct — invoking vmaf --threads abc from a shell exits rc=1 with an "Invalid argument" diagnostic. The failure was a test-only bug: the fork-harness parent allocated a 4 KiB stderr buffer and stopped reading once full, but usage()'s help text has grown past 4 KiB (many --tiny-*, --vulkan-*, --hip-*, --metal-*, --backend, --precision, --dnn-ep, --no-reference, --tiny-codec/-preset/-crf flags accreted over the last year). The child then blocked writing into the full pipe; once the parent closed its end, the child either took SIGPIPE (signal 13) or SIGABRT (signal 6, via aborting stdio in vfprintf); the test's WIFEXITED check rejected the run either way. Fix: refactor the parent into a read_head_drain_tail() helper that captures the first 511 bytes (the "Invalid argument …" needle always precedes the usage block) and drains the remainder so the writer never blocks. Extract child-side dup2 + cli_parse + _exit into child_parse_via_pipe(). Defence-in-depth in error(): replace assert(long_opts[n].name) (banned macro per principles.md §1.2 rule 30; -DNDEBUG silently no-ops the check) with an explicit usage() fallback that emits a clean diagnostic; replace two sprintf(optname, …) calls on a 256-byte buffer with snprintf. | ADR-0528 | fix/cli-threads-parse-safety-v2 | ./core/build/test/test_cli_parse_long_only_args (4/4 green); vmaf --threads abc -r ref -d dis --width 576 --height 324 --pixel_format 420 --bitdepth 8 → rc=1, stderr: Invalid argument "abc" for option --threads; should be an integer, no SIGABRT. | (2026-05-18) | T-PER-SHOT-BITRATE-PREDICATE-CHAIN-2026-05-18 | PR #1290 (ADR-0531) introduced ShotRecommendation.bitrate_kbps and the _shot_bitrate null-serialiser, and documented the bitrate_sidecar wiring pattern in the ADR. However _build_per_shot_bisect_predicate was left incomplete: the function still returned only _predicate and the inner closure discarded result.bitrate_kbps before returning (best_crf, measured_vmaf). All 12 shots in the BBB v11 4K per-shot plan showed bitrate_kbps: null. Fix (ADR-0538): change _build_per_shot_bisect_predicate to return (predicate, bitrate_sidecar) where the closure writes result.bitrate_kbps into the sidecar dict keyed by (start_frame, end_frame). The call site in _run_tune_per_shot unpacks the tuple, initialises an empty sidecar for the --predicate-module path, and after tune_per_shot returns patches each ShotRecommendation via dataclasses.replace. PredicateFn type alias unchanged. +1 regression test test_cli_tune_per_shot_bitrate_kbps_propagates_from_bisect; 3 existing tests updated to include bitrate_kbps on their fake bisect return. | ADR-0538 | fix/per-shot-bitrate-predicate-chain | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_per_shot.py (24 tests, all green) | (2026-05-18) | T-HIP-INTEGER-MOTION-UNREGISTERED-2026-05-18 | vmaf_fex_integer_motion_hip (extractor name "motion_hip") was defined in core/src/feature/hip/integer_motion_hip.c since PR #1167 but was never declared extern in nor inserted into feature_extractor_list[] in core/src/feature/feature_extractor.c. Every call to vmaf_use_feature(vmaf, "motion_hip", NULL) returned a non-zero error because vmaf_get_feature_extractor_by_name("motion_hip") returned NULL. test_hip_motion3_parity (added in PR #1167) hit this failure at line 162 and had been silently reporting failure on every HIP-enabled build since it was added. Surfaced by the ADR-0519 HIP import-state audit. Fix: add the extern VmafFeatureExtractor vmaf_fex_integer_motion_hip; declaration and &vmaf_fex_integer_motion_hip list entry inside #if HAVE_HIP in feature_extractor.c, immediately after the integer_motion_v2_hip entries, mirroring the CUDA twin registration pattern. The init() function still returns -ENOSYS in scaffold builds — posture is unchanged; only the name lookup is now correct. | ADR-0523 | fix/hip-motion-extractor-register | docker exec vmaf-dev-mcp bash -c 'cd /build/vmaf && meson test --suite=fast --no-rebuild test_hip_motion3_parity -v 2>&1 \| tail -20' | (2026-05-18) | T-HIP-IMPORT-STATE-ENOSYS-2026-05-18 | vmaf_hip_import_state in core/src/hip/common.c:149 returned -ENOSYS with a stale comment promising a follow-up PR that had since landed (ADR-0468 added the first real HIP feature kernel float_adm_hip). The CLI's --backend hip path constructed a VmafHipState successfully, then called vmaf_hip_import_state(vmaf, hip_state) which immediately bailed; the CLI emitted "problem during vmaf_hip_import_state" and aborted with exit 255 — even on a healthy AMD gfx1036 host with ROCm 6.4 (HIP state init, stream create, device probe all succeeded). ADR-0514 had closed every other gap; only the library-side state-binding stub remained. Fix moves the function from core/src/hip/common.c into core/src/libvmaf.c and implements it as a SYCL / Vulkan / Metal-style "stash the borrowed state pointer on the VmafContext and return 0" wrapper. Adds a hip.state substruct on VmafContext (gated by #ifdef HAVE_HIP), updates vmaf_close to clear without freeing (caller owns the state). The HIP-flagged extractors keep VMAF_FEATURE_EXTRACTOR_HIP cleared for now (dispatch routes through their CPU twins, so HIP scores match CPU bit-exactly); promoting the flag bit + adding VMAF_PICTURE_BUFFER_TYPE_HIP_DEVICE is tracked as a separate follow-up. Smoke test (test_hip_smoke) updated to verify the new contract (NULL → -EINVAL; device-bound happy path covers vmaf_init → vmaf_hip_import_state → vmaf_close → vmaf_hip_state_free). | ADR-0520 | fix/hip-import-state-implementation | docker exec vmaf-dev-mcp /tmp/hip-build/tools/vmaf --reference /workspace/python/test/resource/yuv/src01_hrc00_576x324.yuv --distorted /workspace/python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --backend hip --hip_device 0 --json --output /tmp/hip.json → VMAF = 76.66783 (CPU = 76.66783, delta = 0, well within the places=4 cross-backend gate from ADR-0214). Smoke: /tmp/hip-build/test/test_hip_smoke → 22/22 pass. | (2026-05-18) | T-DNN-SYMBOLIC-BATCH-DIM-2026-05-18 | vmaf_ctx_dnn_attach in core/src/libvmaf.c rejected every shipped NR tiny checkpoint (model/tiny/nr_metric_v1.onnx, nr_metric_v1.int8.onnx) at attach time with -ENOTSUP (errno 95) because the NCHW gate required in_shape[0] == 1; the NR models declare their input as ['batch', 1, 224, 224] where the first dim is the ONNX dim_param='batch' token that ORT surfaces through the C API as -1. Surfaced by the --no-reference agent (PR #1280 / ADR-0520) as the next blocker after the FR feature-vector loader work in ADR-0518. Fix: dnn_attach_nchw and dnn_attach_feature_vector (and the rank-2 second-input shape probe) accept in_shape[0] ∈ {1, -1} and fold symbolic batch to 1; a fixed batch > 1 stays rejected (no batched-inference scheduler exists); symbolic H/W stays rejected with a sharpened diagnostic. Per-frame inference is unchanged — both run paths already emit shape[0] = 1 on the ORT Run call, so symbolic batch is purely a load-time concern. Regression test test_attach_accepts_symbolic_batch_rank4 in core/test/dnn/test_vmaf_use_tiny_model.c exercises a new 166-byte Identity-graph fixture model/tiny/smoke_v0_symbolic_batch.onnx with dim_param='batch'. test_cli.sh step 5b now uses nr_metric_v1.onnx end-to-end instead of the prior dists_sq.onnx placeholder that load-failed for unrelated reasons. | ADR-0524 | fix/dnn-symbolic-batch-dim | docker exec vmaf-dev-mcp bash -c 'vmaf --no-reference --tiny-model /workspace/model/tiny/nr_metric_v1.onnx --distorted /workspace/python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --json --output /tmp/nr.json' — produces a JSON with the NR model's feature column (no more -95 rejection). | (2026-05-18) | | T-CLI-NO-REFERENCE-NOOP-2026-05-18 | vmaf --no-reference was a documented no-op (docs/usage/cli.md line 238; help text line 249) since the tiny-AI surface landed. cli_parse.c set CLISettings::no_reference from --no-reference / --no_reference and the help text correctly described the intent, but the unconditional if (!settings->path_ref) validation at the end of cli_parse() rejected every NR invocation with Reference .y4m or .yuv (-r/--reference) is required before downstream code ever consulted the flag. Surfaced in .workingdir/bbb_reports/E2E_TEST_MATRIX_v9.md Finding 8 (severity Medium). Fix: gate the reference-required check on !settings->no_reference; require --tiny-model in NR mode (no classic NR scorer exists in the fork); force --no_prediction so the built-in vmaf_v0.6.1 SVM is not auto-injected (it consumes FR feature columns). vmaf.c opens the distorted source twice — two video_input handles backed by the same file — so vmaf_read_pictures receives a non-null picture pair (public-API contract preserved) while the rank-4 DNN dispatch in vmaf_ctx_dnn_run_frame_nchw reads the distorted frame in the "ref" slot it consults. +2 C unit tests covering the success path and the underscore-alias variant; +2 shell smokes asserting the rejection diagnostic for missing --tiny-model and the absence of the legacy Reference required message when both are present. No public-API change; ffmpeg-patch stack untouched. | ADR-0520 | fix/cli-no-reference-wire | ./build/test/test_cli_parse (20 tests, all green) + VMAF_BIN=build/tools/vmaf bash core/test/dnn/test_cli.sh (PASS) + manual: vmaf --no-reference --tiny-model model/tiny/dists_sq.onnx --distorted python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --json --output /tmp/nr.json no longer emits "Reference .y4m or .yuv (-r/--reference) is required"; instead reaches the tiny-model loader. | (2026-05-18) | T-PER-SHOT-SCENE-THRESHOLD-2026-05-18 | vmaf-tune tune-per-shot returned a single shot on a 5 s BBB 4K segment ({"shots": [{"start_frame": 0, "end_frame": 300, ...}]}), defeating per-shot tuning. Root cause: the vmaf-perShot C binary uses a mean-absolute-luma-delta heuristic with a compiled-in default cutoff of 12.0 (8-bit), too conservative for short clips; the Python wrapper had no way to dial sensitivity and no fallback when the detector missed every cut. Fix: tune-per-shot exposes --scene-threshold X (forwards as --diff-threshold to the C binary; default unset preserves the C-side default) and --max-shot-duration S (default 2.0 s; 0 disables) — the second flag is a uniform-time-window splitter that slices any shot longer than S into equal-length sub-shots so the per-shot tuner always sees a multi-shot timeline on clips of duration > S. Splitter preserves contiguity (out[i].end_frame == out[i+1].start_frame) and distributes the remainder so partition lengths differ by at most one frame. +4 regression tests under test_per_shot.py. | ADR-0513 | fix/per-shot-scene-threshold-and-1-shot-chart | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_per_shot.py (27 tests, all green) | (2026-05-18) | T-PER-SHOT-REPORT-1-SHOT-CHART-2026-05-18 | The vmaf-tune profile-card HTML report's per-shot timeline chart was visually empty when len(shots) == 1 — axes, legend, and title rendered, but the chart canvas had no line, point, or band. Observed in .workingdir/bbb_reports/bbb_2160p60_v9_PROPER_20260518_1031.html. Root cause: _shot_plot_fn called ax.step([start], [crf], where="post") on the single-shot path, which matplotlib emits as a zero-length path the SVG backend silently drops. Fix: replace the step call with explicit ax.hlines(...) bands spanning each shot's [start_frame, end_frame) range plus midpoint markers + explicit set_xlim / set_ylim bounds to guard against autoscale degeneracy. Same rendering path now handles 1-shot, 2-shot, and N-shot data identically. +2 regression tests under test_report.py asserting the rendered SVG contains a non-empty <path> / <line> element on the 1-shot input. | ADR-0513 | fix/per-shot-scene-threshold-and-1-shot-chart | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_report.py (6 tests, all green) | (2026-05-18) | T-VMAF-TUNE-COMPARE-RATE-QUALITY-SWEEP-2026-05-18 | vmaf-tune compare historically ran one target VMAF per call and rendered a 2-bar + 2-dot chart — a "useless deliverable" per the user, since two data points in two dimensions communicate nothing about how codecs compare across the operating range an ABR ladder actually spans. The output also had no per-codec rate-quality curve, no pareto-frontier summary, and no usable view of hardware encoders (NVENC / QSV / AMF rows either silently dropped when the encoder wasn't compiled in or returned the generic "encode failed" wording, hiding the operational fact from operators). Fix (ADR-0516): add --target-vmafs 85,90,92,95 and a v2 JSON schema (schema_version: 2, target_vmafs: [...], one row per (codec, target_vmaf) pair). New compare_codecs_sweep() API flat-dispatches the cross-product. New probe_encoder_available() greps ffmpeg -encoders + runs a 1-frame lavfi dummy encode for hardware encoders, marking unavailable rows with a stable hardware encoder not available: … error string. vmaf-tune report detects v1 vs v2 via the schema-version discriminator and renders the new rate-quality line chart (log-bitrate / VMAF axes, one polyline per codec, pareto frontier highlighted as a dashed overlay) + per-codec / per-target summary table for v2; the legacy bar+dot chart is preserved for v1 ingestion so existing automation does not break. Default --encoders is now the CPU set (libx264,libx265,libsvtav1,libvpx-vp9). +24 regression tests under test_compare_rate_quality_sweep.py. | ADR-0516 | feat/compare-rate-quality-sweep | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_compare_rate_quality_sweep.py (24 tests, all green) | (2026-05-18) | | T-BBB-E2E-V7-1-COMPARE-FRAMERATE-PROBE-2026-05-18 | BBB end-to-end v7 probe (2026-05-18) found that vmaf-tune compare --src container.mp4 returned catastrophically wrong VMAFs against any container source whose native rate ≠ 24 fps (e.g. BBB 60 fps reported libx264 CRF=6 / VMAF=90.43 — physically impossible, CRF 6 at 1080p is near-lossless and must score ≥ 98). Root cause: the compare CLI threaded the argparse default --framerate=24.0 into make_bisect_predicate verbatim; the per-iteration frame_skip_ref / frame_cnt derived from 24 fps then mis-indexed the reference YUV (decoded at the container's native 60 fps), comparing misaligned content frame-by-frame and collapsing VMAF to the 4-90 band regardless of CRF. Sister bug to ADR-0505 (ladder): the compare encode plumbing already passed source_is_container=True correctly through bisect._encode_and_score, so the v5 fix wasn't enough on its own. Fix: _run_compare now calls _resolve_compare_source_geometry, which auto-probes container sources via vmaftune.report.probe_source and substitutes the probed framerate / duration when the user left those flags at their argparse defaults. New _TrackedDefaultAction argparse action + _stamp_tracked_default_sentinels post-parse pass distinguish "user explicitly passed 24" from "argparse default 24"; explicit overrides win with a stderr warning on probed-vs-user mismatch. +7 regression tests under test_compare.py. Verified end-to-end against the dev-mcp BBB 60 fps MP4: libx264 CRF=26 → VMAF=94.9 / 528 kbps, libx265 CRF=32 → VMAF=93.6 / 365 kbps. | ADR-0511 | fix/compare-source-is-container-plumbing | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_compare.py (22 tests, all green) | (last ~3 months) | T-CHUG-EXTRACT-VMAF-ALIGNMENT-2026-05-18 | The 2026-05-18 CHUG re-extract at .workingdir/dev-mcp-probes/chug_reextract/full_features_chug.parquet shipped 5992 rows × 72 cols with VMAF clustered tightly around 99 across every bitrate-ladder rung — including 360p @ 0.2 Mbps which should physically score in the 30-60 band. Identity-pair fingerprint was unambiguous: adm2_mean == vif_scale*_mean == 1.0, psnr_y_mean == 60, ciede2000_mean / psnr_hvs_mean == NaN for all 5992/5992 rows. Manual re-derivation of one 360p_0.2M_ row against its real 1080p reference via chug_extract_features.py's scaling policy produced adm2=0.775, vif_scale0=0.276, vmaf=27.98 — confirming the underlying corpus is fine and the FR-aware pipeline is correct. Root cause: parquet was produced by extract_k150k_features.py (FR-from-NR adapter, ref==distorted, intended for KoNViD-150k-A), not chug_extract_features.py. The misuse was silent — no exit code, no warning, no per-row provenance flag distinguished "identity-pair feature dump" from "genuine FR feature pair", and the operator only noticed after pandas inspection several GPU-hours into the run. Fix: new detect_fr_corpus_misuse(meta_by_clip) helper in extract_k150k_features.py inspects the --metadata-jsonl sidecar for the FR signature (any chug_content_name group containing both chug_ref==1 and chug_ref==0 rows); main() exits 2 before spawning any worker process and points the operator at ai/scripts/chug_extract_features.py. --allow-fr-from-nr opt-in for genuine identity-pair studies. +3 detector unit tests + 2 pairing regression tests on chug_extract_features.py (ref_path != dis_path for every emitted pair; orphan distorted rows without a matching reference are dropped) + a synthetic-YUV end-to-end smoke (test_chug_extract_features_smoke.py) that asserts adm2_mean < 0.95 on a deliberately destroyed distorted clip when the vmaf binary is available. Same-family precedent: ADR-0503 (BBB v5 source-is-container propagation) — different code path, same symptom shape. | ADR-0510 | fix/chug-extract-vmaf-alignment | python3 -m pytest ai/tests/test_extract_k150k_features.py::test_detect_fr_corpus_misuse_flags_chug_pairs ai/tests/test_chug.py::test_chug_pairing_never_uses_identity_pairs_for_distorted_rows -v (5 tests, all green) | (last ~3 months) | T-MCP-LADDER-BACKEND-2026-05-18 | Triage of the vmaf-dev-mcp container surfaced three small but compounding defects on the user-facing dev surfaces: (A) MCP list_backends returned cuda=false despite a working vmaf --backend cuda in the same container because _list_backends grepped the --version banner which does NOT advertise compiled-in GPU backends on this fork; (B) MCP default allowlist excluded /workspace/python/test/resource/yuv/ so every container-side vmaf_score call against the Netflix golden YUVs failed with "path not under an allowlisted root" unless the caller set VMAF_MCP_ALLOW; (C) vmaf-tune ladder lacked the --score-backend {cpu,cuda,sycl,vulkan,auto} flag its sibling subcommands (compare, tune-per-shot) accept, defeating cross-backend ladder runs on multi-GPU hosts. Fix: replace the version-grep with a --help-based _probe_backends() helper that looks for --no_<backend> flags (per-process cached); add /workspace/python/test/resource as a default allowlist root alongside the host-relative entry; add --score-backend (default auto) to vmaf-tune ladder and resolve it up-front via score_backend.select_backend() so unavailable backends error out before any encodes start; thread the resolved value through make_default_sampler → CorpusOptions.score_backend → vmaf --backend $name. tune-per-shot deliberately keeps its auto → None → libvmaf-picks predicate contract (asymmetry documented inline). | ADR-0511 | fix/mcp-backend-probe-allowlist-ladder-score-backend | PYTHONPATH=mcp-server/vmaf-mcp/src:tools/vmaf-tune/src python -m pytest mcp-server/vmaf-mcp/tests/test_backend_probe_and_allowlist_0509.py tools/vmaf-tune/tests/test_ladder_score_backend_0509.py | (2026-05-18) | T-DEV-MCP-BACKEND-EXPOSURE-2026-05-18 | vmaf-dev-mcp container only surfaced cpu + cuda backends at run-time on the dev host — sycl reported "No device of requested type available" on Intel Arc despite the device node passthrough, vulkan enumerated only Intel Arc while hiding the host-mounted nvidia_icd.json and the mesa intel/radeon ICDs, and hip reported "built without hip support" even though dev/Containerfile carried -Denable_hip=true. Root causes (Research-0138, four independent gaps): (1) the level-zero UR adapter dlopens libhwloc.so.15 at adapter-load time and the library lives at /opt/intel/oneapi/tcm/latest/lib/libhwloc.so.15 but tcm/latest/lib was not on LD_LIBRARY_PATH; (2) VK_ICD_FILENAMES was pinned to /usr/share/vulkan/icd.d/lvp_icd.x86_64.json (a non-existent path — mesa ships lvp_icd.json without the .x86_64 suffix) which made the Vulkan loader return zero devices; setting the env var to empty in the Containerfile is NOT a fix because the loader treats empty the same as missing — dev/scripts/dev-mcp-entrypoint.sh now unsets the env vars on container start; (3) Docker's devices: directive passes leaf device-nodes but drops the udev-managed /dev/dri/by-path/pci-XXXX:YY:ZZ.W-render symlinks the Intel compute-runtime needs to enumerate Arc; (4) core/tools/meson.build conditionally added -DHAVE_CUDA=1 / -DHAVE_SYCL=1 / -DHAVE_VULKAN=1 to vmaf_tool_cflags but had no matching -DHAVE_HIP=1 branch, so the CLI's #ifdef HAVE_HIP guards (around libvmaf_hip.h include, VmafHipState init/cleanup, --backend hip strict-mode arm in tools/vmaf.c) were compiled out even when libvmaf itself had HIP enabled. Fix: append ${ONEAPI_ROOT}/tcm/latest/lib to LD_LIBRARY_PATH in dev/Containerfile; drop the bogus VK_ICD_FILENAMES=… ENV and unset both VK ICD env vars in dev/scripts/dev-mcp-entrypoint.sh; bind-mount /dev/dri/by-path read-only to both dev-mcp and smoke-probe-cron services in dev/docker-compose.yml; add if get_option('enable_hip') ... vmaf_tool_cflags += ['-DHAVE_HIP=1'] endif to core/tools/meson.build; add a build-time backend probe loop scanning for the built without X support regression signal; document the four invariants in dev/AGENTS.md. Closes finding 8 of .workingdir/bbb_reports/SESSION_FINDINGS_v9_GPU_PROBE.md. Pairs with ADR-0492 (fix/vulkan-fp64-gate-relax). Residual: vmaf --backend hip now compiles in and initialises HIP state, but vmaf_hip_import_state returns -ENOSYS — tracked separately as T-HIP-IMPORT-STATE-ENOSYS-2026-05-18. | ADR-0514 | fix/dev-container-backend-exposure | docker run --rm --runtime nvidia -e NVIDIA_DRIVER_CAPABILITIES=compute,graphics,utility,video --device /dev/dri --device /dev/kfd -v /dev/dri/by-path:/dev/dri/by-path:ro -v /home/kilian/dev/vmaf:/workspace:ro --entrypoint /bin/bash vmaf-dev-mcp:local -c 'for B in cpu cuda sycl vulkan hip; do vmaf --reference /workspace/python/test/resource/yuv/src01_hrc00_576x324.yuv --distorted /workspace/python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --backend $B --json --output /tmp/p_$B.json; done' — cpu=76.66783, cuda=76.667829, sycl=76.667767, vulkan=76.667752 (all 5-place-equal to CPU per ADR-0214), hip fails at vmaf_hip_import_state per the separate Open row. Verified 2026-05-18 on RTX 4090 + Intel Arc A380 + AMD gfx1036. | (last ~3 months) | T-WINDOWS-MSVC-UNISTD-H-2026-05-18 | Build — Windows MSVC + CUDA and Build — Windows MSVC + oneAPI SYCL matrix legs remained red on master after PR #1274 (ADR-0515) fixed the MinGW64 leg. Two independent portability gaps: (1) core/src/feature/x86/vif_avx512.c used bare __attribute__((noinline, noclone)) on the ADR-0503 noinline helpers vif_subsample_rd_8_vert_j and vif_subsample_rd_8_horiz_j; MSVC cl.exe does not support __attribute__ syntax and raised C2143: syntax error: missing ')' before '(' cascading into ~80 downstream C2065: undeclared identifier errors for every function parameter. (2) core/tools/yuv_input.c::yuv_check_file_size called fstat() and S_ISREG() — POSIX names absent from MSVC <sys/stat.h>; Intel's oneAPI icx-cl treated the implicit call as a hard error: call to undeclared function 'S_ISREG'. Fix: VMAF_NOINLINE_NOCLONE portability macro (__declspec(noinline) on MSVC, __attribute__((noinline, noclone)) on GCC/Clang); _WIN32 shims in yuv_input.c mapping fstat→_fstat64, struct stat→struct __stat64, S_ISREG, and typedef __int64 off_t. | ADR-0521 | fix/msvc-unistd-gating | CI: Build — Windows MSVC + CUDA and Build — Windows MSVC + oneAPI SYCL jobs green on the PR's CI run; Build — Windows MinGW64 (CPU) stays green | (2026-05-18) | T-CI-MINGW64-TEST-PUBLIC-API-SCORE-MKSTEMP-2026-05-18 | Build — Windows MinGW64 (CPU) matrix leg perpetually red on master since the 2026-05-16 coverage-audit closure that added core/test/test_public_api_score.c. Root cause: test_vmaf_write_output hardcoded "/tmp/vmaf_test_output_XXXXXX" and called mkstemp(3); MSYS2/MinGW64 inside the GitHub Actions windows-latest runner does not expose a usable /tmp from the MINGW64 shell, so mkstemp failed with ENOENT (test_vmaf_write_output: fail, mkstemp failed). Compile, link, install, and the other 60 tests all passed; only this case wedged the leg red. Fix: extract a make_temp_output_path() helper that uses GetTempPathA() + a <pid>-suffixed filename on #ifdef _WIN32 (mirroring the precedent in core/test/dnn/test_model_loader.c::test_sidecar_parses) and keeps mkstemp on POSIX. unlink → remove for Win32 portability. Conservative, test-only scope; the helper is private to this test file. | ADR-0515 | fix/windows-mingw64-build-repair | meson test -C build test_public_api_score (3/3 green on Linux); Windows MinGW64 matrix job green on the PR's CI run | (last ~3 months) | T-MCP-RUN-BENCHMARK-BROKEN-E2E-V9-5E | MCP run_benchmark tool returned exit_code=1 with empty stdout and stderr on every invocation (E2E v9 Finding 5e, 2026-05-18). Three root causes: (1) spurious -r/-d/--width/--height positional args passed to bench_all.sh corrupted $@ inside the sourced Intel oneAPI setvars.sh, causing a silent set -euo pipefail abort before any output; (2) VMAF_BIN was not injected into the subprocess environment, forcing a fallback to the absent in-tree binary path; (3) set -u (nounset) aborted the shell when setvars.sh referenced unset variables (SETVARS_ARGS, ia32), bypassing the || true guard. Secondary fixes: OUTDIR now defaults to /tmp/vmaf-bench-$$ (worktree-safe, read-only /workspace-safe); run() in bench_all.sh emits SKIP when vmaf exits non-zero or produces no output file (Vulkan fallback path). Fixture-root discovery added to _run_benchmark() so it finds YUVs under $VMAF_ROOT, _repo_root(), or /workspace in that order. Tool schema now declares empty input {} (per-pair scoring uses vmaf_score). Full benchmark JSON returned with all three fixture pairs × four backends, all PASS. | ADR-0517 | fix/mcp-run-benchmark-repair | 47 MCP tests green; E2E repro returns exit_code=0 with CPU=76.667830, CUDA PASS, SYCL PASS, Vulkan PASS for all three fixture pairs | (last ~3 months) | T-TINY-MODEL-LOADER-FEATURE-RANK-2026-05-18 | vmaf --tiny-model <path> failed at attach time with errno -95 (ENOTSUP) for every shipped FR-regressor tiny model (fr_regressor_v1, fr_regressor_v2, vmaf_tiny_v4). Root cause: vmaf_ctx_dnn_attach only accepted ONNX input rank 4 (NCHW image); every FR-regressor is rank-2 feature-vector. ONNX Runtime itself loaded the files (including the external-data v1/v2 sibling-.onnx.data layout) — the rejection was purely in libvmaf. Fix: branch vmaf_ctx_dnn_attach and vmaf_ctx_dnn_run_frame on rank; rank-2 path materialises the canonical-6 features (adm2, vif_scale0..3, motion2) from the live feature collector at inference time, applies the sidecar's optional StandardScaler, and dispatches single- or multi-input ORT inference (pre-seeding the optional codec block to the "unknown" encoder one-hot for fr_regressor_v2). The sidecar parser learns the new feature_order / feature_mean / feature_std fields (plus the vmaf_tiny_v* aliases features / input_mean / input_std). +3 sidecar parsing tests in test_model_loader.c and +1 CLI smoke gate in test_cli.sh exercising all three shipped models against the Netflix CPU reference YUVs. | ADR-0518 | fix/tiny-model-loader-external-data-and-feature-rank | docker exec vmaf-dev-mcp meson test -C /tmp/build-fix --suite=dnn (11 tests, all green); per-model load+run: vmaf --tiny-model model/tiny/fr_regressor_v1.onnx … emits vmaf_tiny_model in JSON instead of failing with -95 | (last ~3 months) | T-TINY-CODEC-PRESET-CRF-CLI-2026-05-18 | Follow-up to ADR-0518: the loader pre-seeded fr_regressor_v2's second-input codec block to the "unknown" encoder one-hot, so every vmaf --tiny-model fr_regressor_v2.onnx … invocation returned the same score regardless of the encoder used to produce the distorted YUV — the conditioning vector was constant. Three new CLI flags (--tiny-codec, --tiny-preset, --tiny-crf) plus a new public C-API vmaf_dnn_set_codec_context() populate the block from the user-supplied parameters. Sidecar loader gains encoder_vocab[]; new vmaf_dnn_codec_block_fill() mirrors train_fr_regressor_v2.py's ENCODER_VOCAB + PRESET_ORDINAL + CRF_MAX (preset normalised by 9.0, CRF by 63.0). Unknown encoder names hard-fail at attach time so typos are caught. Common ffprobe aliases (h264, hevc, av1, vp9, vvc) are accepted. +8 unit tests under test_model_loader.c covering sidecar parsing + the codec-block fill helper. | ADR-0522 | feat/tiny-codec-preset-crf-flags-v2 | Netflix golden 576x324 pair on dev-mcp container: --tiny-model fr_regressor_v2.onnx default → 25.72; --tiny-codec libx264 --tiny-preset medium --tiny-crf 28 → 52.45; --tiny-codec libsvtav1 --tiny-preset 5 --tiny-crf 30 → 49.58 (three distinct codec contexts yield three distinct scores). 11/11 dnn-suite green; 49/50 fast-suite green (1 pre-existing unrelated fail). | (2026-05-18) | | T-BBB-E2E-V8A-LADDER-PASS1-DURATION-2026-05-18 | BBB end-to-end v8 probe (2026-05-18) found that vmaf-tune ladder --duration N still re-encoded the full source during the ffmpeg pass-1 stats sweep, even after ADR-0506 V6-1 wired the duration into build_ffmpeg_command. Root cause: codec adapters declaring supports_encoder_stats=True (libx264, the corpus default) route through run_encode_with_stats, whose pass-1 invocation uses the sibling build_pass1_stats_command argv-builder — the V6-1 patch did not reach it. Net effect: a ladder --duration 5 smoke run against a 10-minute BBB source still burned ~10 min of wall time per cell on pass 1 before pass 2 honoured the requested 5-second window. Fix: mirror the V6-1 fallback in build_pass1_stats_command — emit input-side -t req.duration_s when the caller did not opt into sample-clip mode. Sample-clip precedence preserved. +4 regression tests pinning container / raw / sample-clip-precedence / no-clip-no-duration argv shapes. | ADR-0508 | fix/ladder-duration-clip-ffmpeg-t-flag | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_bbb_e2e_v8_bug_cluster.py (4 tests, all green) | (last ~3 months) | T-VK-NO-SHADERFLOAT64-REFUSAL-2026-05-18 | ADR-0492's hard shaderFloat64 refusal regressed vmaf --backend vulkan to -ENOTSUP on Intel Arc A380, AMD gfx1036, and older NVIDIA GPUs — entire GPU generations excluded for an unmeasured precision concern. Empirical numbers on Netflix golden 576x324 (src01_hrc00 <-> src01_hrc01, yuv420p 8-bit): CPU 76.66783, Vulkan fp64 RTX 4090 76.66776 (-7e-5), Vulkan fp32 Intel Arc A380 76.66775 (-8e-5), Vulkan fp32 AMD gfx1036 76.66774 (-9e-5) — fp32 lands within 2e-5 of fp64 and within ~1e-4 of CPU. Fix: ship the VIF compute shader as two SPIR-V variants (vif_fp64.comp + vif_fp32.comp); runtime auto-picks based on VkPhysicalDeviceFeatures::shaderFloat64. Inverse opt-in --vulkan-require-fp64 / VmafVulkanConfiguration::require_fp64 re-enables the old strict refusal for bit-exact-strict CI workflows. Supersedes ADR-0492. | ADR-0512 (supersedes ADR-0492) | fix/vulkan-two-variant-vif-shader | docker exec vmaf-dev-mcp bash -c 'VK_DRIVER_FILES=/usr/share/vulkan/icd.d/intel_icd.json vmaf --reference /workspace/python/test/resource/yuv/src01_hrc00_576x324.yuv --distorted /workspace/python/test/resource/yuv/src01_hrc01_576x324.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --backend vulkan --json --output /tmp/x.json' → vmaf = 76.66775 ± 1e-4 on Intel Arc; identical RTX 4090 score under the fp64 path | (last ~3 months) | T-VK-VIF-FP32-PRECISION-GAP | Vulkan VIF shader computed g = sigma12 / sigma1_sq in precise float (fp32); CPU reference uses double. At sub-1080p the fp32-vs-double divergence accumulated to ~2×10⁻⁴ per-frame VMAF delta, exceeding the ADR-0214 places=4 gate. Fixed by promoting g, sv_sq, and gg_sigma to double via GL_EXT_shader_explicit_arithmetic_types_float64; device capability probed at init time. Note (2026-05-18): the hard-refusal half of ADR-0492 was replaced by the two-variant compile in ADR-0512 — see new row above. | ADR-0492 (superseded by ADR-0512) | fix/vulkan-vif-shader-fp64-for-bit-exact | docker exec vmaf-dev-mcp vmaf --backend vulkan → integer_vif_scale3 delta ≤ 1×10⁻⁴ vs CPU baseline | (last ~3 months) | T-FLOAT-VIF-SYCL-HIP-METAL-SKIP-SCALE0 | float_vif_sycl, float_vif_hip, and float_vif_metal silently dropped vif_skip_scale0; scale-0 always emitted regardless of setting — diverging from float_vif.c CPU | no ADR: only-one-way fix | fix/float-vif-skip-scale0-hip-metal | vmaf --feature float_vif_sycl:vif_skip_scale0=true emits VMAF_feature_vif_scale0_score=0.0 | (last ~3 months) | T-INT-ADM-METAL-MISSING-CSF-OPTIONS | integer_adm_metal held adm_csf_scale, adm_csf_diag_scale, and adm_noise_weight in its struct and used them in kernel dispatch, but the fields were hardcoded in init_fex_metal and not exposed in the options table — users could not tune them via vmaf --feature adm_metal:adm_csf_scale=... | no ADR: only-one-way fix | fix/adm-metal-missing-options | vmaf --feature adm_metal:adm_csf_scale=2.0 overrides the multiplier on Metal | (last ~3 months) | T-INT-ADM-CUDA-VK-SKIP-SCALE0-OPTION | integer_adm_cuda and adm_vulkan silently dropped adm_skip_scale0; scale-0 was always accumulated regardless of caller setting, diverging from integer_adm.c CPU | — | fix/adm-skip-scale0-gpu-parity | vmaf --feature integer_adm_cuda:adm_skip_scale0=true → integer_adm_scale0=0.0 | (last ~3 months) | T-INT-ADM-SYCL-HIP-METAL-SKIP-SCALE0-OPTION | integer_adm_sycl, integer_adm_hip, and integer_adm_metal silently dropped adm_skip_scale0 and adm_min_val; scale-0 always accumulated, floor never applied — diverging from CPU/CUDA/Vulkan | no ADR: only-one-way fix | fix/adm-skip-scale0-sycl-metal-hip-parity | vmaf --feature adm_sycl:adm_skip_scale0=true emits integer_adm_scale0=0.0 | (last ~3 months) | T-GPU-MS-SSIM-ENABLE-DB-SILENT | float_ms_ssim_cuda silently ignored enable_db / clip_db; float_ms_ssim_sycl silently ignored enable_lcs, enable_db, clip_db — GPU emitted linear scores regardless | ADR-0460 / Research-0137 | fix/ms-ssim-gpu-enable-db-lcs-sycl-2026-05-16 | — | (last ~3 months) | T-GPU-PSNR-ENABLE-CHROMA-SILENT | psnr_cuda / psnr_sycl / psnr_vulkan silently ignored enable_chroma=false, emitting full chroma on non-YUV400 sources and diverging from CPU | ADR-0453 / Research-0136 | fix/psnr-enable-chroma-gpu-parity-2026-05-16 | — | (last ~3 months) | T-SANITIZER-CAMBI-UBSAN-DESELECT | test_cambi UBSan deselect was stale — PR #761 (2026-05-11) added __builtin_cpu_supports("avx2") runtime gate, eliminating the SIGILL that justified the exclusion | — | fix/sanitizer-cambi-framesync-deselect-2026-05-16 | UBSAN_OPTIONS=halt_on_error=1 ./build-ubsan/test/test_cambi passes clean | (last ~3 months) | T-SANITIZER-FRAMESYNC-TSAN-DESELECT | test_framesync TSan deselect was stale — PR #548 (2026-05-09) fixed the SAN-FRAMESYNC-MUTEX-DOMAIN mutex-domain mismatch; nightly TSan job was green 2026-05-09 and 2026-05-10 | — | fix/sanitizer-cambi-framesync-deselect-2026-05-16 | nightly TSan green per state.md 2026-05-10 update | (last ~3 months) | T-CAMBI-HIP-NOT-STARTED | cambi_hip.c scaffold that returned -ENOSYS from vmaf_hip_cambi_run was replaced by a full CAMBI HIP kernel in PR #996 (9b5e23488). The stale -ENOSYS stubs are removed; ADR-0345 Phase 3 is complete. | — | #996 9b5e23488 feat(hip): add CAMBI banding-detection extractor on HIP backend | vmaf --feature cambi_hip runs without -ENOSYS on a HIP-enabled build | (last ~3 months) | T-BBB-E2E-V2-CLUSTER-2026-05-18 | BBB end-to-end v2 probe (2026-05-18) surfaced five defects in the layer below ADR-0497: (1) score._decode_to_raw_yuv ignored caller --duration, materialising ~58 GB of raw YUV for a 10 s probe against a 634 s 1080p source (#v2-A); (2) ladder default sampler used rung target dims as source dims, crashing every cross-resolution rung against a raw YUV source (#v2-B); (3) vmaf-tune report required matplotlib but dev-mcp container didn't ship it (#v2-C, ADR-0496 violation); (4) report's <details> JSON appendix re-emitted bare NaN literals even when input JSON was clean (#v2-D); (5) vmaf --backend NAME silently fell back to CPU on init failure with exit code 0 and no JSON status (#v2-E). Fix: thread duration_s through ScoreRequest → _decode_to_raw_yuv with ffmpeg -t clamp; ladder takes separate src_width / src_height and injects -vf scale=W:H; container pip-installs matplotlib + report fallback; report JSON appendix uses allow_nan=False + NaN→None coercion; libvmaf CLI errors hard on explicit-backend init failure and amends JSON with backend_used echo. Plus operational follow-ups: bisect distinguishes "encoder unavailable" from genuine failures, encoder-version fallback for libx264 / libsvtav1. | ADR-0498 | fix/bbb-e2e-v2-bug-cluster-2026-05-18 | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_bbb_e2e_v2_bug_cluster.py (9 tests, all green) | (last ~3 months) | T-BBB-E2E-V3-LADDER-REFERENCE-DECODE-2026-05-18 | BBB end-to-end v3 probe (2026-05-18) surfaced one blocker (V3-B): vmaf-tune ladder --src bbb.mp4 … exited 1 with RuntimeError: default sampler produced no scorable encodes because vmaftune.corpus.iter_rows decoded only the distorted leg before the vmaf CLI call. The reference (.mp4 / .y4m) was handed to libvmaf as-is, tripped raw_input_open's file-size guard, and every cell scored as failed. Compounded by _VMAF_RAW_SUFFIXES = {".yuv", ".y4m", ""} listing .y4m as "no decode needed" — vmaf-tune always passes --width/--height/--pixel_format/--bitdepth which flips the CLI's use_yuv flag, so .y4m never reaches the Y4M parser. Fix: add _maybe_decode_reference mirror of _maybe_decode_distorted, decode once per iter_rows and reuse the .ref.decoded.yuv sidecar across cells; drop .y4m from both suffix tables; short-circuit cells when reference decode fails (no wasted encode + score). Bisect path was already correct per ADR-0498 — pinned as invariant. V3-C informational: dev-mcp container's ffmpeg lacks libsvtav1 per ADR-0496; compare's bisect predicate already classifies as encoder unavailable correctly (pinned as regression). | ADR-0499 | fix/vmaf-tune-ladder-reference-decode-v3 | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_bbb_e2e_v3_bug_cluster.py (9 tests, all green) | (last ~3 months) | T-BBB-E2E-V4-CLUSTER-2026-05-18 | BBB end-to-end v4 probe (2026-05-18) surfaced three findings in the layer below ADR-0499: (V4-A) vmaf --backend vulkan against a Vulkan-less runtime had no regression test pinning the ADR-0498 strict-mode non-zero exit propagation through main() — the contract held but a future refactor of the ret-chain could silently re-introduce the failure; (V4-B) vmaf-tune ladder --resolutions 1920x1080,1280x720 collapsed the grid to one rendition with VMAF ~21 because _maybe_decode_reference decoded the reference at the source's native geometry while the libvmaf CLI was told to read both legs at the rung target — a 1080p reference was mis-parsed as a 720p frame; the JSON descriptor also lacked the top-level samples[] array that vmaf-tune report --ladder-json needs to render the Pareto cloud overlay; (V4-C) vmaf-tune report aggregated ok=false purely because the dev-mcp container's ffmpeg ships without libsvtav1 — a row marked error="encoder unavailable (libsvtav1): …" (infrastructure gap, not a quality regression) flipped the report red. Fix: extend _maybe_decode_reference with optional target_width / target_height kwargs that append -vf scale=W:H and embed dims in the per-rung sidecar filename; wire iter_rows to pass the rung target whenever CorpusJob.src_width/src_height differs from width/height; extend emit_manifest / _emit_json with a samples= kwarg and thread the pre-hull cloud through build_and_emit; split _run_report's row aggregation so encoder-unavailable rows raise a new degraded=true flag without gating ok. Pinned by 9 new regression tests + the existing V2 cross-res scale-filter test extended to assert the reference-side scale invocation. | ADR-0501 | fix/bbb-e2e-v4-bug-cluster-2026-05-18 | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_bbb_e2e_v4_bug_cluster.py (9 tests, 1 skipped on Vulkan-less hosts) | (last ~3 months) | T-BBB-E2E-V6-CLUSTER-2026-05-18 | BBB end-to-end v6 probe (2026-05-18) surfaced three follow-ups against the v5 fixes, all confined to the vmaf-tune ladder orchestration: (V6-1) ladder --duration N was metadata-only (used only for kbps math, never wired into the ffmpeg encode pipe), so a 10-second smoke run against a 9-minute container source re-encoded the full 9 min at every CRF in the sweep — the v6 probe ate ~10 min wall time on a single 3-cell sweep before timing out; (V6-2) cross-resolution ladders against a raw-YUV source failed on every rung whose target differed from the source dims because _decode_source_to_yuv called ffmpeg without input-side -f rawvideo -s SRCWxSRCH -r FR flags — the rawvideo demuxer refused the input and the sampler raised "default sampler produced no scorable encodes"; (V6-3) vmaf-tune ladder returned exit code 0 even when the sampler raised RuntimeError, defeating CI gates and shell-script error handling. Fix: extend EncodeRequest with a duration_s field that build_ffmpeg_command translates to an input-side -t when the caller did NOT opt into sample-clip mode; iter_rows plumbs CorpusJob.duration_s into it. _decode_source_to_yuv gains source_is_raw / source_width / source_height / source_framerate kwargs that synthesise the demuxer-side raw-input block when set; _maybe_decode_reference and iter_rows wire the source geometry through. _run_ladder wraps build_and_emit in try/except and returns 2 on RuntimeError/ValueError/OSError. Pinned by 9 new regression tests covering encoder argv shape, raw-YUV cross-res reference decode, and a subprocess-driven CLI exit-code check. | ADR-0506 | fix/bbb-e2e-v6-bug-cluster-2026-05-18 | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_bbb_e2e_v6_bug_cluster.py (9 tests, all green) | (last ~3 months) | T-BBB-E2E-V5-CLUSTER-2026-05-18 | BBB end-to-end v5 probe (2026-05-18) surfaced three follow-ups against the v4 fixes: (V5-1) the V4-A integration test was gated on shutil.which("vmaf") and silently skipped on every developer host without the binary on $PATH; the V5 replacement also probes $VMAF_BIN_FOR_TESTS and build/tools/vmaf so the gate fires whenever a built binary is reachable; (V5-2) vmaf-tune ladder against a container source (bbb_sunflower_1080p_30fps_normal.mp4) produced VMAF in the 4-9 band at a uniform ~50 Mbps regardless of CRF — root cause: corpus.iter_rows never set EncodeRequest.source_is_container=True, so the encode argv builder emitted -f rawvideo -pix_fmt yuv420p -s WxH -i src.mp4, re-interpreting the container's compressed bytes as planar YUV pixels; (V5-3) the v4 emit path used per-target picks (one row per (resolution, target_vmaf) cell) as the JSON samples[] cloud, which dropped every non-winning CRF in the sweep and double-listed any rendition whose target-VMAFs converged on the same CRF. Fix: derive container shape from the source suffix and propagate source_is_container=True, always appending the rung-target scale filter for container sources; extend make_default_sampler and _default_sampler with an optional cloud_sink list that captures every successfully-scored CRF row before pick_target_vmaf collapses the cell; extend build_and_emit with an extra_samples kwarg that supersedes the per-target cloud and runs a new _dedup_samples pass keyed by (width, height, crf). Pinned by 7 new regression tests including a docker exec-driven end-to-end probe against the dev-mcp BBB MP4 corpus. | ADR-0505 | fix/bbb-e2e-v5-bug-cluster-2026-05-18 | PYTHONPATH=tools/vmaf-tune/src python -m pytest tools/vmaf-tune/tests/test_bbb_e2e_v5_bug_cluster.py (7 tests; 1 skipped without vmaf binary, 1 skipped without docker) | (last ~3 months) | T-MCP-PROBE-2026-05-17-CLUSTER | MCP server probe (2026-05-17) surfaced five defects: (1) vmaf_score silently fell back to CPU when caller requested an unavailable backend; (2) tool-schema backend enum dropped vulkan/hip/metal; (3) run_benchmark returned exit_code=1 with empty stdout AND stderr on set -euo pipefail silent abort; (4) ref==dis yields ~97.43 not 100 (documented as vmaf_v0.6.1 model artefact — Netflix golden gate forbids modifying coefficients); (5) vmaf_4k_v0.6.1 on 576×324 saturates at 100 every frame with no warning. Fix: refuse-and-echo backend, schema enum extended, bench wrapper surfaces silent-pipefail with error field + bash -x hint and bench script exits 2 on missing vmaf binary, response gains mismatched_model_warning for resolution-preset disagreement. | ADR-0495 | fix/mcp-probe-findings-2026-05-17 | PYTHONPATH=mcp-server/vmaf-mcp/src python3 -m pytest mcp-server/vmaf-mcp/tests/test_probe_findings_2026_05_17.py (9 tests, all green) | (last ~3 months)
Bugs closed in the last ~90 days. Older entries roll off into git log and the per-PR ADRs.
| Bug | Closed by | ADR | Verification |
|---|---|---|---|
T-MASTER-CI-2026-05-20 — master CI timeout / parity repair cluster — Five independent defects kept master / PR #1437 red. (1) The CLI direct-reader path borrowed preallocated picture-pool slots before reading from the input streams and failed to vmaf_picture_unref() them on EOF/error; one-sided EOF after the opposite picture had been read leaked that slot too, so scoring completed and output was written but vmaf_close() waited forever. (2) The lavapipe parity gate mapped feature adm + backend vulkan to the retired adm_vulkan name after ADR-0586 canonicalised the extractor as integer_adm_vulkan. (3) convolution_avx512.c used _mm512_load_ps / _mm512_store_ps in vertical scanlines even though MAX_ALIGN == 32; AVX-512 requires 64-byte alignment, so float_vif could SIGSEGV on AVX-512-capable CPU runners. (4) run_test_on_dataset() unconditionally requested bootstrap score keys from normal VmafQualityRunner / PsnrQualityRunner results, failing the macOS tox run_testing.py cases with get_bagging_score_key lookup errors before score assertions ran. (5) Python doctests in vmaf.tools.misc and vmaf.tools.stats expected bare NumPy scalar reprs / old assertion traceback formatting; macOS tox exposed np.float64(...) and assertion-detail output differences. | PR #1437 (fix/master-ci-2026-05-19) | no ADR: only-one-way CI/runtime bug fixes | timeout --kill-after=10s 5m core/build/tools/vmaf ... --feature float_vif --backend cpu --json exits 0; meson test -C core/build test_vif_simd test_output test_public_api_score test_model --print-errorlogs passes; .venv/bin/python -m pytest scripts/ci/test_calibration.py scripts/ci/test_cross_backend_feature_names.py -q passes; PYTHONPATH=python .venv/bin/python -m pytest python/test/routine_test.py::TestTrainOnDataset::test_test_on_dataset python/test/routine_test.py::TestTrainOnDataset::test_test_on_dataset_mle python/test/routine_test.py::TestTrainOnDataset::test_test_on_dataset_raw python/test/routine_test.py::TestTrainOnDataset::test_test_on_dataset_split_test_indices_for_perf_ci python/test/routine_test.py::TestTrainOnDataset::test_test_on_dataset_bootstrap_quality_runner python/test/command_line_test.py::CommandLineTest::test_run_testing_psnr python/test/command_line_test.py::CommandLineTest::test_run_testing_vmaf python/test/command_line_test.py::CommandLineTest::test_run_cleaning_cache_psnr -q passes; PYTHONPATH=python .venv/bin/python -m pytest -q -p no:warnings --doctest-modules python/vmaf/tools/misc.py python/vmaf/tools/stats.py passes. |
T-METAL-FLOAT-MS-SSIM-PORT — float_ms_ssim lacked a Metal backend twin; the Metal dispatch table only covered float_ssim in the float-SSIM family, leaving the 5-scale MS-SSIM path falling back to CPU on Apple Silicon. | feat/metal-float-ms-ssim-port | ADR-0490 | ninja -C build && ./build/tools/vmaf --reference src.yuv --distorted dis.yuv --model vmaf_v0.6.1.json --feature float_ms_ssim_metal on a macOS Metal build (-Denable_metal=enabled). |
T-VK-T7-29-PART-2-IMPORT-NOT-IMPL — Vulkan zero-copy image import (vmaf_vulkan_import_image, vmaf_vulkan_wait_compute, vmaf_vulkan_read_imported_pictures) had stale @return -ENOSYS until T7-29 part 2 lands comments in libvmaf_vulkan.h. All three entry points are fully implemented (async pending-fence ring, ADR-0251). The stale comments were the only remaining gap; corrected to document real error codes. | fix/sycl-motion-fps-weight-vulkan-import-status-2026-05-16 (2026-05-16) | ADR-0186 / ADR-0251 | vmaf_vulkan_import_image(...) returns 0 (not -ENOSYS) on a Vulkan-enabled build. |
FINDING-7 — pthread_*_init return values unchecked in vmaf_thread_pool_create — pthread_mutex_init and two pthread_cond_init calls at lines 167-169 of thread_pool.c silently ignored non-zero returns (possible under ENOMEM on constrained systems). On failure, *pool pointed at a partially-initialised struct; the next pthread_mutex_lock call was undefined behaviour and p->workers / p leaked. Fix: staged init with per-step teardown; on any failure the function frees p->workers and p, sets *pool = NULL, and returns -ENOMEM. | fix/memory-safety-thread-pool-adm-picture-bounds | no alternatives: only-one-way fix | meson test -C build test_thread_pool — test_thread_pool_create_guards exercises NULL/zero-thread rejection and happy-path create+destroy. ASan+TSan clean. |
FINDING-8 — NULL deref on OOM in adm_dwt2_* per-frame aligned_malloc — adm_dwt2_s, adm_dwt2_lo_s, adm_dwt2_d, and adm_dwt2_lo_d in feature/adm_tools.c called aligned_malloc for tmplo/tmphi row buffers without NULL checks. On OOM the next array write dereferenced a null pointer (ASan-detected). Fix: add if (!tmplo) return -ENOMEM; / if (!tmphi) { aligned_free(tmplo); return -ENOMEM; } guards; change signatures from void to int; update callers in adm.c to propagate via the existing goto fail path. | fix/memory-safety-thread-pool-adm-picture-bounds | no alternatives: only-one-way fix | Happy path covered by meson test -C build (ADM extractor exercises all four DWT2 variants). Per-frame allocation minimisation (hoisting to init) deferred per task scope. |
FINDING-10 — Unsigned overflow in picture_compute_geometry when w >= 0xFFFFFFC1 — (pic->w[0] + DATA_ALIGN - 1u) & ~(DATA_ALIGN - 1u) wrapped to 0 for near-UINT_MAX widths, producing a zero-byte allocation that passed silently and caused OOB on any pixel read. CERT INT30-C. Fix: add if (w == 0 \|\| w > 32768u \|\| h == 0 \|\| h > 32768u) return -EINVAL; at the top of vmaf_picture_alloc, before picture_compute_geometry. | fix/memory-safety-thread-pool-adm-picture-bounds | no alternatives: only-one-way fix | test_picture_alloc_rejects_overflow_dimensions in test_picture.c asserts -EINVAL for w=0, h=0, w=32769, h=32769 and success for w=h=32768. ASan+UBSan clean. |
Issue #857 — cambi_cuda SIGSEGV on every input — cambi_cuda segfaulted on every invocation. The three kernel dispatch helpers in integer_cambi_cuda.c passed (void *)buf (host address of VmafCudaBuffer struct) as cuLaunchKernel kernel parameters instead of &buf->data (address of the CUdeviceptr field). The CUDA driver read buf->size (a byte count) as the device pointer, producing an invalid GPU address that faulted on the first memory access. Secondary: UB pointer arithmetic on CUdeviceptr through uint8_t * fixed to use integer arithmetic directly. | fix/cambi-cuda-segfault-857 | no alternatives: only-one-way fix | compute-sanitizer ./tools/vmaf --feature cambi_cuda ... exits 0 with no invalid-access reports. |
T-CAMBI-CUDA-HOST-PREPROCESSING-SEGV (Issue #857) — cambi_cuda SIGSEGV in submit_fex_cuda on every frame. submit_fex_cuda called vmaf_cambi_preprocessing(dist_pic, ...) directly on the CUDA picture; dist_pic->data[0] is a device pointer, so the host dereference inside decimate_generic_uint8_and_convert_to_10b (cambi.c:819) caused SIGSEGV (exit 139). Fix: download dist_pic GPU→host via vmaf_cuda_picture_download_async + cuStreamSynchronize on the picture's private stream before calling vmaf_cambi_preprocessing. | PR #870 (fix/cambi-cuda-host-preprocessing, 2026-05-16) | — (bug fix; only-one-way correction; no ADR per CLAUDE §12 r8) | LD_LIBRARY_PATH=core/build-cuda/src core/build-cuda/tools/vmaf --reference /tmp/freshtest.yuv --distorted /tmp/freshtest.yuv --width 1920 --height 1080 --pixel_format 420 --bitdepth 8 --feature cambi_cuda --threads 1 --backend cuda --output /tmp/c.json --json → exit 0 (was 139). Cross-backend diff vs CPU cambi at places=4: 0/N mismatches per ADR-0214. |
T-METAL-HEADER-INSTALL-GAP — libvmaf_metal.h absent from meson install output — The Metal backend header was missing from platform_specific_headers in core/include/core/meson.build, causing meson install -Denable_metal=enabled to omit the header from the installed tree. Downstream FFmpeg --enable-libvmaf-metal configure probes (check_pkg_config libvmaf_metal ... libvmaf/libvmaf_metal.h vmaf_metal_state_init) therefore silently failed. docs/api/gpu.md Metal section also used three wrong symbol names and was missing the entire IOSurface zero-copy sub-API. | fix/saliency-per-mb-eval-2026-05-15 (Batch 4) | ADR-0437 | meson install --destdir /tmp/vmaf-install on a macOS build with -Denable_metal=enabled; verify ls /tmp/vmaf-install/usr/local/include/libvmaf/libvmaf_metal.h exists. Compile-time symbol check via test_metal_install_header (macOS CI). |
F1-CLI-CPUMASK-SHORT-OPT — vmaf -c <bitmask> silently discarded. cli_parse.c declared 'c' in short_opts[] so getopt_long consumed -c <value> from the command line, but the switch statement had only case ARG_CPUMASK: (long-option enum 264) with no case 'c': arm. The switch fell into default: and discarded the value silently. Users passing -c 0xff received no ISA restriction and no error. The fix adds case 'c': as a fall-through before case ARG_CPUMASK: and adds a comment documenting the invariant. Companion: doc error in --tiny-model-verify <path> corrected (flag is boolean, no path argument). | fix/cli-short-opt-cpumask-and-bench-atoi-2026-05-15 (Batch 5) | ADR-0438 | test_cpumask_short_opt in test_cli_parse.c asserts -c 0xff → cpumask == 255 and -c 3 → cpumask == 3; meson test -C build test_cli_parse passes. |
F2-BENCH-ATOI — banned atoi() in vmaf_bench.c --device parser. vmaf_bench.c:833 used atoi(argv[++i]) to parse the GPU device index. atoi is on the banned-function list (CLAUDE.md §6 / docs/principles.md §1.2 r30) because it returns 0 silently on any invalid input, making non-numeric or out-of-range arguments indistinguishable from device index 0. Replaced with strtol + endptr/bounds check following the parse_unsigned() pattern from cli_parse.c; invalid values now print a clear error to stderr and exit non-zero. | fix/cli-short-opt-cpumask-and-bench-atoi-2026-05-15 (Batch 5) | ADR-0438 | Manual check: vmaf_bench --device abc → Invalid --device value: abc, exit 1. No other banned functions found in vmaf_bench.c after full scan. |
FINDING-21 (AUDIT-DEEP-2026-05-15) — saliency_student_v2 pending production-flip and learned_filter_v1 / nr_metric_v1 model cards missing — The gap-fill audit found that saliency_student_v2 (IoU 0.7105) had been staged as a parallel artefact but never promoted to the production default; separately, model cards for learned_filter_v1 and nr_metric_v1 were absent from docs/ai/models/ despite the models being shipped and registered. | PR feat/tiny-ai-registry-ci-and-saliency-v2-promotion-2026-05-15 (2026-05-15) — registry.json v1 description/notes updated to record supersession; v2 promoted to production in description/notes; both model cards created. | ADR-0444 | python3 ai/scripts/validate_model_registry.py → OK: 24 registry entries valid; CI registry-validate job passes; docs/ai/models/learned_filter_v1.md and docs/ai/models/nr_metric_v1.md created to the ADR-0042 five-point bar. |
T-VMAFTUNE-LIBAOM-SALIENCY-ROI — recommend-saliency --encoder libaom-av1 fell back to plain encode — The fork FFmpeg patch stack already exposes the libaom-av1 -qpfile ROI bridge, but vmaf-tune's saliency dispatcher omitted libaom-av1 and therefore skipped ROI bias for AV1/libaom runs. | This PR (fix/backlog-gap-pass-26-2026-05-14) wires libaom-av1 into vmaftune.saliency, reuses the existing 16×16 qpfile writer, and appends -qpfile <path> via libaom adapter metadata. | — (bug fix; only-one-way adapter dispatch closure) | Tests cover argv emission, 16×16 qpfile grid shape, adapter metadata, and ephemeral qpfile cleanup. |
T-SANITIZER-SCORE-POOLED-EAGAIN-CLEAN — test_score_pooled_eagain no longer needs sanitizer deselect — the test was bundled into the broad T-SANITIZER-DEFECTS-REVEALED-758 exclusion after PR #758 made the sanitizer matrix enumerate the real C test set. Re-enabling it exposed a separate UBSan failure in adm_decouple_s123_avx2: the AVX2 lane helper called __builtin_clz() for direct-LUT-range ADM values, including zero, before the later blend selected the scalar direct-LUT result. The helper now returns the unshifted value with shift 0 for temp < 32768, matching the scalar ADM path and avoiding invalid clz / negative-shift inputs. | this PR (fix/sanitizer-score-pooled-eagain-2026-05-14) | — (bug fix; no ADR per CLAUDE §12 r8) | ASAN_OPTIONS=halt_on_error=1:abort_on_error=1:print_summary=1 ./build-asan-score/test/test_score_pooled_eagain, UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1:print_summary=1:print_stacktrace=1 ./build-ubsan-score/test/test_score_pooled_eagain, and TSAN_OPTIONS=halt_on_error=1 ./build-tsan-score/test/test_score_pooled_eagain all pass. The workflow removes only test_score_pooled_eagain from the ASan / UBSan / TSan EXCLUDE regexes; test_feature_collector, test_pic_preallocation, test_cli_parse, and test_predict remain tracked. |
T-SANITIZER-FEATURE-COLLECTOR-CLEAN — test_feature_collector no longer needs sanitizer deselect — the test was bundled into the broad T-SANITIZER-DEFECTS-REVEALED-758 exclusion after PR #758 made the sanitizer matrix enumerate the real C test set. Current master no longer reproduces the original leak / abort signature: the test passes cleanly under ASan+LSan, UBSan, and TSan. | this PR (fix/sanitizer-feature-collector-leak-2026-05-14) | — (stale sanitizer deselect cleanup; no ADR per CLAUDE §12 r8) | ASAN_OPTIONS=detect_leaks=1:halt_on_error=1 ./build-asan-score/test/test_feature_collector, UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 ./build-ubsan-score/test/test_feature_collector, and TSAN_OPTIONS=halt_on_error=1 ./build-tsan-score/test/test_feature_collector all pass. The workflow removes only test_feature_collector from the ASan / UBSan / TSan EXCLUDE regexes; test_pic_preallocation, test_cli_parse, and test_predict remain tracked. |
T-SANITIZER-CLI-PARSE-CLEAN — test_cli_parse no longer needs sanitizer deselect — the test was bundled into the broad T-SANITIZER-DEFECTS-REVEALED-758 exclusion after PR #758 made the sanitizer matrix enumerate the real C test set. Current master no longer reproduces the recorded test_backend_cpu non-zero sanitizer exit: the full test_cli_parse binary passes cleanly under ASan+LSan, UBSan, and TSan. | this PR (fix/sanitizer-cli-parse-deselect-2026-05-15) | — (stale sanitizer deselect cleanup; no ADR per CLAUDE §12 r8) | ASAN_OPTIONS=detect_leaks=1:halt_on_error=1:abort_on_error=1:print_summary=1 ./build-asan-cli/test/test_cli_parse, UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1:print_summary=1:print_stacktrace=1 ./build-ubsan-cli/test/test_cli_parse, and TSAN_OPTIONS=halt_on_error=1 ./build-tsan-cli/test/test_cli_parse all pass. The workflow removes test_cli_parse from the ASan / UBSan / TSan EXCLUDE regexes; test_predict is retired by the companion cleanup. |
T-SANITIZER-PREDICT-CLEAN — test_predict no longer needs sanitizer deselect — the test was bundled into the broad T-SANITIZER-DEFECTS-REVEALED-758 exclusion after PR #758 made the sanitizer matrix enumerate the real C test set. Current master no longer reproduces the recorded UBSan / TSan findings: the full test_predict binary passes cleanly under ASan+LSan, UBSan, and TSan. | this PR (fix/sanitizer-predict-deselect-2026-05-15) | — (stale sanitizer deselect cleanup; no ADR per CLAUDE §12 r8) | ASAN_OPTIONS=detect_leaks=1:halt_on_error=1:abort_on_error=1:print_summary=1 ./build-asan-predict/test/test_predict, UBSAN_OPTIONS=halt_on_error=1:abort_on_error=1:print_summary=1:print_stacktrace=1 ./build-ubsan-predict/test/test_predict, and TSAN_OPTIONS=halt_on_error=1 ./build-tsan-predict/test/test_predict all pass. The workflow removes test_predict from the ASan / UBSan / TSan EXCLUDE regexes; test_cli_parse was retired by the companion cleanup. |
T-VK-VIF-1.4-RESIDUAL-NVIDIA-DEFERRED — Vulkan VIF API-1.4 residual on NVIDIA RTX 4090 + driver 595.71.05 — Phase 3b left integer_vif_scale2 failing 45/48 frames at max abs 1.527e-02 and 5-run non-deterministic int64 accumulator magnitudes after stronger fences failed. Phase 3c replaces the seven subgroupAdd(int64_t) accumulator reductions in vif.comp with an explicit subgroupShuffleXor butterfly helper, avoiding the NVIDIA int64 subgroup-add lowering path. | PR #787 (fix/vulkan-vif-int64-subgroup-reduction) | ADR-0269 Phase-3c status update; research-0108 | glslc --target-env=vulkan1.3 -O core/src/feature/vulkan/shaders/vif.comp -o /tmp/vif.spv; ninja -C build-vulkan-int64 tools/vmaf; cross-backend VIF gate at places=4: NVIDIA device 0 0/48 on all scales (scale2 max 2.000000e-06, repeated 5 times), Arc device 1 0/48, RADV device 2 0/48. |
T-SANITIZER-PIC-PREALLOCATION-CLEAN — test_pic_preallocation no longer needs sanitizer deselect — the test was bundled into the broad T-SANITIZER-DEFECTS-REVEALED-758 exclusion after PR #758 made the sanitizer matrix enumerate the real C test set. PRs #765 (PREV_REF refcount leak in batch func) and #769 (dict leak via is_initialized guard in flush_context_threaded) together fixed all underlying bugs. All 8 sub-tests now pass cleanly under ASan+LSan, UBSan, and TSan. | DONE — PR this branch (fix/dev-container-cuda-passthrough, 2026-06-06). test_pic_preallocation removed from all 5 EXCLUDE regexes across .github/workflows/sanitizers.yml (2 lanes) and .github/workflows/tests-and-quality-gates.yml (3 lanes). | — (stale sanitizer deselect cleanup; no ADR per CLAUDE §12 r8) | 8/8 sub-tests pass: ASAN_OPTIONS=detect_leaks=1:halt_on_error=1:abort_on_error=1 ./core/build-asan/test/test_pic_preallocation, UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 ./core/build-pic/test/test_pic_preallocation, TSAN_OPTIONS=halt_on_error=1 ./core/build-tsan/test/test_pic_preallocation. GCC debug build: 8/8. 20 consecutive GCC runs: all pass. |
SAN-FLOAT-MS-SSIM-MIN-DIM-LEAK — test_float_ms_ssim_min_dim::invoke_init was excluded from the ASan deselect list based on a reported 240-byte / 6-allocation leak. Re-verification under ASAN_OPTIONS=detect_leaks=1 (2026-05-13) shows zero leaks: invoke_init already calls fex->close(fex) + free(priv) on every code path (both early-reject and success). The exclusion was never needed after the teardown was added to the test body. | ASAN_OPTIONS=detect_leaks=1 ./build-asan-test/test/test_float_ms_ssim_min_dim → 3 tests run, 3 passed, no leak report. | — | Removed test_float_ms_ssim_min_dim$ from EXCLUDE= in .github/workflows/tests-and-quality-gates.yml (ASan lane). |
T-VMAFTUNE-RECOMMEND-FROM-CORPUS-FILTER — vmaf-tune recommend --from-corpus bypassed the library row filter — The programmatic recommend() API dropped rows with exit_status != 0, missing / non-finite vmaf_score, and non-matching encoder / preset, but the CLI --from-corpus path called pick_target_vmaf / pick_target_bitrate directly. A failed encode row with high VMAF, a NaN score row, or a row for a different encoder could therefore win from the CLI even though the library API rejected it. | PR #781 (fix/backlog-gap-pass-7-2026-05-14) | — (bug fix; no ADR per CLAUDE §12 r8 — only-one-way fix) | _run_recommend_from_corpus now builds a RecommendRequest and delegates to recommend(). Regression tests cover failed-row filtering, NaN filtering, and encoder filtering. Smoke: PYTHONPATH=tools/vmaf-tune/src .venv/bin/python -m pytest tools/vmaf-tune/tests/test_recommend.py -q — 20/20 passed. |
T8-1b — Metal (Apple Silicon) backend runtime (ADR-0420) — replaces the T8-1 scaffold's -ENOSYS C stubs with three Obj-C++ .mm TUs (common.mm, picture_metal.mm, kernel_template.mm) driving Metal.framework via Obj-C++ ARC. MTLCreateSystemDefaultDevice / MTLCopyAllDevices, Apple-Family-7 gate, MTLResourceStorageModeShared zero-copy buffers, private MTLCommandQueue + two MTLSharedEvent handles per consumer. | PR #764 (feat/metal-runtime-t8-1b, 2026-05-11). | ADR-0420 | macOS Metal CI lane green; subsequent PRs #765 (kernels T8-1d–j), #766 (CLI selectors, ADR-0422), #767 (IOSurface zero-copy import, ADR-0423) layered on top. Homebrew tap flipped from MoltenVK to native Metal (formula). |
T-MACOS-ARM-SVE2-PROBE-FALSE-POSITIVE — macOS-arm contributor build fails with SVE vector type 'svbool_t' cannot be used in a target without sve — the meson SVE2 probe in core/src/meson.build calls cc.compiles() against a declarations-only TU including <arm_sve.h>. Recent Apple Clang ships the header and silently accepts -march=armv9-a+sve2 against this minimal probe, so is_sve2_supported = true on Apple Silicon — but the real SSIMULACRA 2 SVE2 TU then fails to compile under Apple Clang's incomplete SVE intrinsics surface. Apple Silicon (M1–M4) is ARMv8.x without SVE2 and the runtime detection in core/src/arm/cpu.c was already __linux__-gated, so the compiled SVE2 object would never have executed on Darwin anyway. GHA macos-latest (Apple Silicon since late 2024) didn't catch this because its image's Apple Clang version makes the probe fail outright — only a newer local Xcode triggers the inconsistency. | PR #762 (fix/sve2-probe-darwin-gate, 2026-05-11): short-circuit is_sve2_supported = false on host_machine.system() == 'darwin', mirroring the runtime __linux__ gate. | ADR-0419 | Local macOS-arm contributor build (Apple Silicon, recent Xcode) no longer compiles ssimulacra2_sve2.c. Linux ARMv9 builds (Graviton 4, Ampere AmpereOne) unaffected — probe still runs and HAVE_SVE2 is still set. |
T-CAMBI-AVX2-CI-SIGILL — test_calculate_c_values_scalar_avx2_parity SIGILLs under TSan on Ubuntu 24.04 CI runner — the test called calculate_c_values_avx2 (and its inner helpers cambi_increment_range_avx2, calculate_c_values_row_avx2) unconditionally under #if ARCH_X86. Those helpers build AVX2 SIMD constants (_mm256_setr_epi32, _mm256_set1_epi32) at function entry, so on any x86-64 host without AVX2 the first AVX2 instruction raises SIGILL. The non-test dispatch path in cambi.c::init() already runtime-gates via vmaf_cpu_features() & VMAF_X86_CPU_FLAG_AVX2; the direct-call parity test was missing the equivalent gate. | PR #761 (fix/cambi-tsan-zero-init, 2026-05-11): wrap the AVX2 call in if (__builtin_cpu_supports("avx2")). Test still asserts bit-exact scalar/AVX2 parity on runners with AVX2; AVX2 leg is skipped on runners without. | — (test-only one-line gate; no ADR per CLAUDE §12 r8) | Local TSan + UBSan: meson setup build -Db_sanitize=thread --buildtype=debug -Db_lto=false -Db_lundef=false && ninja -C build test/test_cambi && ./build/test/test_cambi → 23 tests run, 23 passed. CI: Sanitizers — ASan + UBSan + MSan (thread) flips from SIGILL → green. |
T-MACOS-PY-TEST-RECAL-POST-VIF-SYNC — 9+ macOS-CI Python test assertions still reference pre-bf9ad333 VIF/ADM values — PR #758 cherry-picked Netflix's 142c0671 / 7209110e / d93495f5 / fe756c9f recalibration fixtures for the test_run_vmaf_* assertions, but Netflix had not (and as of 2026-05-11 has not) shipped companion fixtures for local_explainer_test::test_explain_vmaf_results, vmafexec_test::test_run_vmafexec_runner_akiyo_multiply* (3 cases), or 5 × vmafexec_feature_extractor::test_run_float_adm_fextractor_adm_*. macOS-clang (CPU), +DNN, Metal jobs failed because brew ships a Python 3.11 that actually runs tox -c python (Ubuntu's runner has Python 3.14 so tox skips via envlist = py311). The macOS CI lane is fork-added (Netflix CI is Linux-only); the test files are Netflix-owned but the failure mode is purely fork-platform. | PR #760 (fix/macos-test-recal-post-vif-sync, 2026-05-11): port upstream 4dcc2f7c + 8c645ce3 C-side wholesale (adm.c/h/_tools.c/h/_options.h/_csf_tools.h, float_adm.c, float_vif.c). Adds the 4 missing ADM options + 2 VIF prescale options. Reverts PR #732's fork-local AIM recal and PR #760 round-1's fork-recal back to upstream-canonical values; for the akiyo-multiply + local_explainer score-drift tests the values stay fork-recalibrated against macOS-libm precision (Ubuntu tox skips via envlist=py311 mismatch). | ADR-0418 | macOS clang (CPU) / + DNN / Metal lanes flip green; Ubuntu unaffected (tox skips); Netflix Golden D24 unaffected (already covered by upstream-fixture cherry-picks in PR #758). |
T-NETFLIX-GOLDEN-VIF-HALF-PORT — Master Netflix-Golden D24 CI red on feature_extractor_test.py after PR #754 reverted PR #723 — The fork's VIF C-side was a partial port of upstream Netflix's bf9ad333 (on-the-fly filter computation): PR #723 ported the mirror-tap-h fix and a slice of the filter-dispatch rewrite, but not the full vif_tools.c machinery and not the companion test recalibrations (142c0671, 7209110e, d93495f5, fe756c9f). PR #754 then reverted #723 entirely after seeing 7+ vifks tests fail, restoring the vifks tests but reintroducing 8 fextractor failures with vif_num/vif_den deltas of ~8.4/~9.3 on values ~713 K/~1.6 M (relative drift ~1e-5, places=0 strictness). | PR #758 (fix/vif-upstream-onthefly-sync, 2026-05-10): full upstream sync — vif.{c,h}, vif_tools.{c,h}, vif_options.h taken verbatim from upstream/master; compute_vif() gains int vif_skip_scale0 parameter; float_vif.c::extract() updated to thread it; companion test cherry-picks adopt upstream's recalibrated golden values. | ADR-0416 | Local-pytest: feature_extractor_test.py 8 → 0 failures; quality_runner_test.py 9 → 0 failures (modulo niqe_runner skimage env issue, unrelated). meson test -C core/build 54/54 OK. vmafexec_feature_extractor_test.py + result_test.py failures 87 → 27 (remaining 27 are pre-existing prescale + dataframe tests not introduced by PR #758). |
T-MASTER-BUILD-FAILURES-RUN-25633425586 — Three distinct master Build Matrix CI failures introduced after PR #733 merged: (1) Ubuntu SYCL build: integer_adm_sycl.cpp:67:25: error: expected unqualified-id — adm_options.h defines #define ADM_BORDER_FACTOR (0.1) (C preprocessor macro); integer_adm_sycl.cpp simultaneously declares static constexpr double ADM_BORDER_FACTOR = 0.1; — the macro expands in the constexpr declaration position producing invalid syntax static constexpr double (0.1) = 0.1;. (2) Ubuntu Vulkan build: VK_API_VERSION_1_4 undeclared at vulkan/common.c:54,311,446 — Ubuntu 22.04 CI ships Vulkan SDK 1.3.204 which predates the constant added in Vulkan Headers 1.3.280. (3) macOS cambi test: test_run_cambi_fextractor_full_reference, test_run_cambi_fextractor_full_reference_scaled_ref, test_run_cambi_runner_fullref all raised KeyError: 'Cambi_FR_feature_cambi_encbd_8_score' — CambiFullReferenceFeatureExtractor used atom feature name "cambi" (prefix "cambi_"), which matched both cambi_source (12 chars) and cambi_encbd_8 (13 chars) in the _discover_feature_wildcard shortest-match logic; the shorter key cambi_source was selected for the distorted-CAMBI slot, producing a wrong result key and leaving the expected key absent. | this PR (fix/master-build-failures-sycl-vulkan, 2026-05-10): (1) #ifdef ADM_BORDER_FACTOR / #undef ADM_BORDER_FACTOR guard added to core/src/feature/sycl/integer_adm_sycl.cpp before the constexpr declaration; (2) #ifndef VK_API_VERSION_1_4 fallback to VK_API_VERSION_1_3 added to core/src/vulkan/common.c after includes; (3) atom feature renamed from "cambi" to "cambi_encbd" in CambiFullReferenceFeatureExtractor (prefix "cambi_encbd_" is specific enough to avoid matching cambi_source); fork-added test assertion in python/test/cambi_test.py::test_run_cambi_runner_fullref updated from Cambi_FR_feature_cambi_score to Cambi_FR_feature_cambi_encbd_score. | — (build and test bug fix; no ADR per CLAUDE §12 r8 — all three fixes are one-way corrections with no meaningful alternative) | (1) ninja -C build-sycl core/src/libvmaf.a exits 0 with SYCL enabled; (2) ninja -C build-vulkan core/src/libvmaf.a exits 0 on Ubuntu 22.04 SDK; (3) python3 -m pytest python/test/cambi_test.py -k "full_reference or fullref" -v → 3/3 passed. make lint clean. |
T-ROUND9-THREAD-POOL-PTHREAD-CREATE — vmaf_thread_pool_create did not check the return value of pthread_create; additionally, vmaf_thread_pool_destroy read n_threads without the mutex before broadcasting stop — A failed pthread_create (e.g. EAGAIN under ulimit -u or container thread caps, or EPERM under a no-new-threads seccomp policy) left p->n_threads counting workers that never started. vmaf_thread_pool_wait then entered its stop-path branch (while (pool->stop && pool->n_threads)) waiting for those ghost threads to decrement the counter via exit signalling, causing an infinite wait (process hang) on vmaf_close(). The secondary race: const unsigned n_workers = pool->n_threads in destroy was read without holding pool->queue.lock, while runner threads decrement n_threads under the lock on exit — a C11 data race even if benign in practice on this architecture. Surfaced by round-9 angle-5 resource-limit audit (Research-0097). | this PR (fix/thread-pool-pthread-create-unchecked, 2026-05-10): pthread_create return checked; partial-spawn case adjusts n_threads / n_workers_created and breaks the loop; zero-spawn case tears down primitives and returns -EAGAIN/-EPERM to the caller. New n_workers_created field (immutable after create) used in destroy instead of mutable n_threads. | — (bug fix; no ADR per CLAUDE §12 r8 — only one-way fix) | meson test -C /tmp/build-tp → 54/54 OK. pre-commit run --files core/src/thread_pool.c → all checks pass. |
T-ROUND8-MCP-TMPDIR-LEAK — describe_worst_frames MCP tool leaked PNG files indefinitely — each call to describe_worst_frames created /tmp/vmaf-mcp-worst-{pid}/frame_NNNNNN.png files but never cleaned them up; only a bare pass existed in the finally block despite the comment "only clear the dir on next invocation." On a long-running MCP server handling many describe_worst_frames requests, PNG files from all prior calls accumulated without bound (up to ~5–10 MB per frame × 32 frames per call). | This PR (fix/round8-mcp-tmpdir-leak, 2026-05-10): shutil.rmtree(tmp_root) added at the start of each _describe_worst_frames invocation, before new PNGs are generated. PNGs remain accessible for the duration of the single turn (until the next call). Regression test test_describe_worst_frames_tmpdir_cleared_on_next_call added to mcp-server/vmaf-mcp/tests/test_server.py. | — (bug fix, no ADR per CLAUDE §12 r8) | Sentinel-file test: plant a stale file in the tmp dir, call _describe_worst_frames, assert sentinel is gone. Test PASSES against the patched server and FAILS against the pre-fix server, confirming correctness. |
T-ROUND8-MCP-TMPDIR-LEAK — describe_worst_frames MCP tool leaked PNG files indefinitely — each call to describe_worst_frames created /tmp/vmaf-mcp-worst-{pid}/frame_NNNNNN.png files but never cleaned them up; only a bare pass existed in the finally block despite the comment "only clear the dir on next invocation." On a long-running MCP server handling many describe_worst_frames requests, PNG files from all prior calls accumulated without bound (up to ~5–10 MB per frame × 32 frames per call). | PR #741 (fix/round8-mcp-tmpdir-leak, 2026-05-10): shutil.rmtree(tmp_root) added at the start of each _describe_worst_frames invocation, before new PNGs are generated. PNGs remain accessible for the duration of the single turn (until the next call). Regression test test_describe_worst_frames_tmpdir_cleared_on_next_call added to mcp-server/vmaf-mcp/tests/test_server.py. | — (bug fix, no ADR per CLAUDE §12 r8) | Sentinel-file test: plant a stale file in the tmp dir, call _describe_worst_frames, assert sentinel is gone. Test PASSES against the patched server and FAILS against the pre-fix server, confirming correctness. |
T-ROUND8-OPT-NAN-BYPASS — set_option_double in core/src/opt.c silently accepted NaN as a valid feature-parameter value — IEEE 754 ordered comparisons involving NaN always evaluate to false, so the bounds check n < min and n > max both returned false for any NaN produced by strtod("nan", …). The NaN was stored unmodified and propagated through powf(area * NaN, 1/3) in the ADM noise-floor computation, silently producing null scores in the JSON output. Affects all VMAF_OPT_TYPE_DOUBLE parameters including adm_noise_weight, adm_enhn_gain_limit, adm_norm_view_dist, etc. Surfaced by round-8 angle-5 negative-param test (T-ROUND8-OPT-NAN-BYPASS / CWE-704). | This PR (fix/round8-opt-nan-bypass, 2026-05-10): isnan(n) guard inserted before n < min / n > max in set_option_double; #include <math.h> added. Two regression tests added: test_double_nan_is_rejected (covers "nan", "NaN", "NAN") and test_double_inf_rejected_when_max_finite. | — (bug fix; no ADR per CLAUDE §12 r8 — only one-way fix, no tradeoff) | core/build-test/test/test_opt reports 25/25 passed; 54/54 full meson test pass. Reproducer: vmaf … -m "version=vmaf_v0.6.1" --feature float_adm:adm_noise_weight=nan — previously propagated NaN to all frame scores; now exits with feature-option validation error. |
T-CUDA-FEATURE-EXTRACTOR-DOUBLE-WRITE — 750+ "cannot be overwritten" warnings per scoring run when --feature <name> is combined with an auto-loaded VMAF model on a GPU binary — feature_extractor_vector_append() deduplicated by extractor name ("adm" vs "adm_cuda"), which are distinct strings, so both the CPU and GPU twin were registered and both ran their extract() callback at every frame. The second write to each collector slot tripped the overwrite guard. Surfaced during K150K extraction investigation (PR #739 / Research-0096), fix deferred at that time. | PR #742 (fix/fex-dedup-by-provided-feature, 2026-05-10); new provided_features_overlap() helper in core/src/fex_ctx_vector.c detects CPU/GPU twins by shared provided-feature names (ADR-0214 parity contract). | ADR-0385 | ./build-cpu/tools/vmaf -r python/test/resource/yuv/src01_hrc00_576x324.yuv -d python/test/resource/yuv/src01_hrc01_576x324.yuv -w 576 -h 324 -p 420 -b 8 --feature adm --threads 1 2>&1 | grep "cannot be overwritten" | wc -l → 0 post-fix (was 750+ pre-fix on CUDA binary). VMAF mean 94.323010 unchanged. test_fex_vector_dedup_by_provided_feature_name passes (6/6 total tests). |
Vulkan VIF scale 2/3 numerical saturation (+1.07 VMAF inflation) — Vulkan VMAF reported 95.069 vs CPU 93.996 on the Netflix golden 576x324 pair (src01_hrc01). All 48 frames had float_vif_scale2_score and float_vif_scale3_score (and integer equivalents) saturated at 1.0. Two independent root causes: (1) float_vif.comp was compiled with glslc -O (SPIR-V optimizer enabled), causing FMA contraction of sigma1_sq = xx - mu1*mu1; at scales 2/3 the local variance is very small so contraction-induced catastrophic cancellation pushed all pixels into the unconditional low-sigma branch. (2) Integer VIF rd buffer allocated with floor division (h/2) for odd-height inputs — for h=81 at scale 2 this under-allocated by 72 uint32 slots, corrupting the adjacent per-WG int64 accumulator and producing massively-negative denominators clamped to 1.0. | this PR (fix/vulkan-vif-scale-precision, 2026-05-10); core/src/feature/vulkan/shaders/float_vif.comp + vif_vulkan.c + core/src/vulkan/meson.build | ADR-0381 | Fix 1: float_vif.comp added to psnr_hvs_strict_shaders list (→ -O0) + precise qualifiers on vertical/horizontal accumulator vars and sigma expressions. Fix 2: alloc_buffers() changed from (h/2) to (h+1)/2 ceiling division. Verification: all 8 per-scale VIF scores (4 integer + 4 float) match CPU within 2e-6 (gate: ±1e-3); full VMAF 94.323 vs CPU 93.996 (Δ=0.327, well within ±0.5). 53/53 meson tests pass. |
T-FUZZ-Y4M-NEG-WIDTH-SEGV — fuzz_y4m_input NULL-deref SEGV on negative width/height in Y4M header — y4m_input_open_impl passed pic_w = -8 / pic_h = 4 through y4m_parse_tags without validation; the subsequent size arithmetic wrapped, malloc returned NULL or a stub, and fread(_y4m->dst_buf, …) in y4m_input_fetch_frame NULL-dereffed inside libc. Reproduced every nightly fuzz run 2026-05-05–08. | This PR (fix/t-fuzz-y4m-neg-width-segv, 2026-05-10); fix at core/tools/y4m_input.c after tag-parse: guard if (_y4m->pic_w <= 0 || _y4m->pic_h <= 0) + diagnostic + return -1. Reproducer promoted to core/test/fuzz/y4m_input_known_crashes/y4m_neg_width_null_deref.y4m. | ADR-0382 | CC=clang meson setup build-fuzz -Dfuzz=true -Db_sanitize=address -Denable_cuda=false -Denable_sycl=false && ninja -C build-fuzz core/test/fuzz/fuzz_y4m_input && ./build-fuzz/core/test/fuzz/fuzz_y4m_input core/test/fuzz/y4m_input_known_crashes/y4m_neg_width_null_deref.y4m — exits 0 with "Invalid YUV4MPEG2 dimensions: W=-8 H=4 (must be > 0)." printed; no SEGV. fuzz.yml fuzz_y4m_input leg will return to green on next nightly run. |
picture_compute_geometry off-by-one for odd-height / odd-width YUV 4:2:0 inputs (ASan heap-OOB in ciede::scale_chroma_planes) — picture_compute_geometry in core/src/picture.c used floor division (h >> ss_ver, w >> ss_hor) for chroma plane dimensions. For odd luma dimensions under 4:2:0 subsampling, the standard requires ceiling division (ceil(luma/2) = (luma+1) >> 1); the floor formula under-allocated by one row/column. A 577 × 323 input produced 288 × 161 chroma planes instead of the correct 289 × 162, causing a one-past-end heap OOB in any extractor iterating pic->h[1] rows (CIEDE scale_chroma_planes being the confirmed reproducer). The same floor error was present in vmaf_cuda_picture_alloc_pinned and vmaf_cuda_picture_alloc in picture_cuda.c, and in the local geometry of integer_psnr_cuda.c and integer_psnr_hvs_cuda.c. | this PR (fix/picture-odd-dim-chroma-ceiling, 2026-05-10) | — (bug fix, no ADR per CLAUDE §12 r8) | ASAN_OPTIONS=halt_on_error=1 ./build-chroma-asan/tools/vmaf --reference /tmp/odd.yuv --distorted /tmp/odd.yuv --width 577 --height 323 --pixel_format 420 --bitdepth 8 --feature ciede --threads 4 exits 0 with zero ASan reports post-fix. New test test_picture_odd_dim_chroma_ceiling asserts pic.w[1]==289, pic.h[1]==162 for 577 × 323 YUV420. 53 / 53 meson test pass. |
vf_libvmaf HIP error-code mis-mapping in ffmpeg-patches/0011 — when vmaf_hip_state_init() returned -ENODEV (no AMD GPU) or vmaf_hip_import_state() returned -ENOSYS (T7-10c scaffold stub), the filter mapped both to AVERROR(EINVAL), producing "Error: Invalid argument" rather than the correct errno string. Discovered during the full-stack FFmpeg e2e test (patches 0001–0011 applied against n8.1.1, libvmaf built with enable_hip=true): hip_device=0 on a machine with an AMD GPU showed vmaf_hip_import_state failed: -38 but then reported "Invalid argument" to the caller. The correct mapping is AVERROR(-err) which passes the libvmaf-supplied errno through to FFmpeg's error-string table. | this PR (fix/hip-averror-propagation-0011, 2026-05-10) — ffmpeg-patches/0011-libvmaf-wire-hip-backend-selector.patch | — (bug fix in patch file, no ADR per CLAUDE §12 r8; touches ADR-0380 surface) | AVERROR(EINVAL) → AVERROR(-err) at both error sites in the #if CONFIG_LIBVMAF_HIP block of init(). Applied patch replayed against n8.1.1 with 0001–0010 stack; ffmpeg -i dis.mp4 -i ref.mp4 -filter_complex "[0:v][1:v]libvmaf=hip_device=0" -f null - now exits with "Error: Function not implemented" (errno 38 = ENOSYS from the scaffold stub) rather than "Error: Invalid argument". Series replay still applies cleanly; all 11 patches green. |
Motion 5-tap filter mirror-padding overflow (Research-0094, round-7 stability audit) — The reflect-101 mirror formula height - (i_tap - height + 2) used in edge_8 / edge_16 (CPU) and the equivalent mirror() / dev_mirror_motion() device functions (all GPU backends) requires height >= filter_width/2 + 1 = 3. For height < 3 or width < 3 (e.g. 1×1 or 2×N frames) the formula produces a negative index, resulting in an out-of-bounds read — UB that manifests as ASan SEGV or garbage scores. Previously deferred as "1×1 frames are not a valid production use case"; the correct fix is an init()-time refusal with -EINVAL and a human-readable message, not a tolerance of undefined behaviour in the convolution kernel. All three CPU extractors (motion, motion_v2, float_motion) and all five GPU families (CUDA × 2, SYCL × 2, Vulkan × 2, HIP × 2, no-op NEON/AVX paths inherit the CPU check) now reject frames below 3×3 at init(). | this PR (fix/motion-mirror-padding-min-dim, 2026-05-10) | — no ADR per CLAUDE §12 r8 (input-validation hardening, no architecture decision) | test_motion_min_dim: 13/13 pass — init(1×1) and init(2×2) return -EINVAL for all three CPU extractors; init(3×3) and init(576×324) return 0. Reproducer from Research-0094: ./build/tools/vmaf --reference /tmp/1x1.yuv --distorted /tmp/1x1.yuv --width 1 --height 1 --pixel_format 420 --bitdepth 8 --feature motion --threads 1 now exits with motion: frame 1x1 is below the 5-tap filter minimum 3x3 + non-zero exit code instead of SEGV or garbage scores. 54/54 meson tests pass (576×324 Netflix golden resolution is well above the floor). |
integer_motion_v2 flush() dict leak (CWE-401, round-7 stability audit) — In the threaded dispatch path, flush() on the registered context allocates feature_name_dict but vmaf_feature_extractor_context_close() returns early (!is_initialized guard) without calling close_fex(), leaking 378 bytes per scoring run. | this PR (fix/motion-v2-flush-dict-leak, 2026-05-10) | Research-0094 — no ADR per CLAUDE §12 r8 (bug fix, no architecture decision) | ASAN_OPTIONS='detect_leaks=1' ./build-leak/tools/vmaf --feature motion_v2 --threads 4 --output /dev/null … produces 0 bytes of ASan output; pre-fix logged 378 bytes across 8 allocations. 53/53 meson tests pass. |
T-PYPSNR-AST-EVAL — PyFeatureExtractorMixin._get_feature_scores and the NoReferenceFeatExtractor read their per-frame logs with ast.literal_eval after writing them with str(...). Under numpy 2.x + Python 3.14, str(np.float64(x)) produces the string np.float64(34.79...) — a function-call expression that ast.literal_eval correctly rejects for safety. All eight test_run_pypsnr_* test cases raised ValueError: malformed node or string. | PR fix/pypsnr-ast-eval — replaced import ast + str(...) write + ast.literal_eval read with import json + json.dump write + json.load read at all four sites in python/vmaf/core/feature_extractor.py. No encoder subclass needed: PyPsnrFeatureExtractor._generate_result already calls .tolist() on numpy arrays and uses Python min/max which return plain floats; the only numpy scalars flowing into log_dicts are psnr_y/u/v computed by 10 * np.log10(...) which json.dump serializes as plain JSON numbers. | No ADR needed — mechanical serialization-format fix; no architectural decision | PYTHONPATH=$PWD/python python3 -m pytest python/test/feature_extractor_test.py -k "pypsnr" — 8/8 passed on Python 3.14 + numpy 2.x. assertAlmostEqual values (places=4) unchanged. Full feature_extractor_test.py green. |
T-PY-FEXT-ATOM-SYNC — VmafFeatureExtractor.ATOM_FEATURES in python/vmaf/core/feature_extractor.py was behind Netflix upstream commits 3dee9666 + 7209110e. Upstream moved vif_scale0–3, adm_scale0–3, adm2, adm3, aim, motion3 from derived computation into direct XML-attribute reads; the fork retained the old derivation (vif_scale0 = vif_num_scale0 / vif_den_scale0), causing 105/132 Python tests to fail against the current binary. The correct upstream ADM math also required porting the noise_weight + adm_csf_scale / adm_csf_diag_scale parameters and the AIM derivation (correct cm() src/dst buffer order + noise_weight=0.0). GPU backends required a follow-up parameter-threading pass. | PR #731 / commit 67b0a1c6 (CPU port, merged 2026-05-10) + PR #732 / commit b8b1f99b (fork-local adm_f1f2 assertion recalibration, merged 2026-05-10) + PR #733 / commit 0a5123df (GPU backend param sync, merged 2026-05-10) | docs/research/0095-atom-features-port-blocker-2026-05-10.md (PR #730 / commit 1d9e0d47); no new ADR per CLAUDE §12 r8 (upstream port, no fork-specific architectural decision) | PR #731: 63/65 feature_extractor_test.py pass (2 fork-local adm_f1s/f2s assertions updated by #732). PR #732: recalibrates the single assertAlmostEqual for the fork-local adm_f1s/f2s feature from 0.9539779375 → 0.8872294167 to match the corrected AIM math. PR #733: GPU host-side parameter threading (CUDA × 2, SYCL × 2, Vulkan × 2); meson test -C build-cpu 54/54 OK; python3 -m pytest python/test/vmafexec_test.py 34/34 passed. Netflix golden CPU assertions unchanged throughout. |
PyPsnrFeatureExtractor ImportError — python/test/feature_extractor_test.py failed to import because PyPsnrFeatureExtractor and PyPsnrMaxdb100FeatureExtractor were not defined in vmaf.core.feature_extractor. The fork had the class hierarchy inverted vs upstream: PypsnrFeatureExtractor was the full implementation while PyPsnrFeatureExtractor (the upstream primary) was absent. All 66 tests in the file were silently skipped due to the collection error. | PR (this PR) — fix/pypsnr-feature-extractor-import; python/vmaf/core/feature_extractor.py | No ADR needed — mechanical import fix with no architectural decision | PYTHONPATH=$PWD/python python3 -c "from vmaf.core.feature_extractor import PyPsnrFeatureExtractor, PypsnrFeatureExtractor, PyPsnrMaxdb100FeatureExtractor, PypsnrMaxdb100FeatureExtractor; print(PyPsnrFeatureExtractor.TYPE)" prints PyPsnr_feature. PYTHONPATH=$PWD/python python3 -m pytest python/test/feature_extractor_test.py --collect-only collects 66 items with no error. |
float_ansnr_hip: hipMemcpy2DAsync direction tagged hipMemcpyDeviceToDevice for host→device transfer — submit_fex_hip in core/src/feature/hip/float_ansnr_hip.c:324,330 copies host-side ref_pic->data[0] / dist_pic->data[0] into device-side staging buffers s->ref_in / s->dis_in but tagged the transfer direction as hipMemcpyDeviceToDevice. Modern ROCm tolerates this (auto-detects from pointer attributes) but the wrong tag is undefined per the HIP spec and inconsistent with every other HIP feature kernel in the fork (float_psnr, integer_psnr, float_moment, float_ssim, float_motion all correctly use hipMemcpyHostToDevice). Surfaced during the round-6 ROCm/HIP bench audit (gfx1036, post-PR-#710). | this PR (fix/hip-ansnr-memcpy-direction, 2026-05-10) | — (one-token fix, no ADR per CLAUDE §12 r8) | Two hipMemcpyDeviceToDevice → hipMemcpyHostToDevice corrections at lines 324 and 330. meson test -C build-hip 54/54 green; the float_ansnr_hip kernel now reports a finite, CPU-comparable score on the gfx1036 bench. |
vmaf_cuda_picture_alloc_pinned null-deref when vmaf_picture_priv_init fails (CWE-476, round-6 cross-PR seam audit) — identical to the vmaf_picture_alloc bug fixed by PR #700: the vmaf_picture_priv_init(pic) return code was bitwise-OR-accumulated into err unconditionally, then VmafPicturePrivate *priv = pic->priv; priv->cuda.state = … dereferenced a potentially NULL priv. PR #700 fixed picture.c but missed picture_cuda.c. Also aligns DATA_ALIGN_PINNED - 1 → DATA_ALIGN_PINNED - 1u with the fully-unsigned mask pattern PR #708 applied to picture.c. | this PR (fix/round6-cuda-pinned-alloc-null-deref, 2026-05-10) | — (bug fix, no ADR per CLAUDE §12 r8) | Sequential checks replace the bitwise-OR-assign idiom: err = vmaf_picture_priv_init(pic); if (err) goto free_data; guards all subsequent priv->… dereferences. gcc -fanalyzer CWE-476 warning no longer emitted on picture_cuda.c. meson test -C build --suite=fast green. |
-fsanitize=integer narrowing/overflow defects in picture.c, libvmaf.c, dnn/tensor_io.c (round-5 integer sanitizer sweep) — three integer-type defects caught by compiling with -fsanitize=integer: (1) picture_compute_geometry computed aligned_y/aligned_c using signed mask literal ~(DATA_ALIGN - 1) = int(-64) stored into unsigned stride fields, firing implicit signed→unsigned conversion UB in four tests. (2) vmaf_init passed ~cfg.cpumask (type uint64_t) to vmaf_set_cpu_flags_mask(unsigned) without an explicit truncation cast, firing implicit narrowing conversion. (3) f16_to_f32_one normalised f16 subnormals via a uint32_t exp counter that wrapped through UINT32_MAX twice per subnormal; the algorithm was numerically correct but the unsigned wraps triggered -fsanitize=integer. | this PR (fix/picture-align-unsigned-narrowing, 2026-05-10) | — (bug fix; no ADR per CLAUDE §12 r8) | (1) const int aligned_y/aligned_c → const unsigned with DATA_ALIGN - 1u mask literal. (2) Explicit (unsigned)(~cfg.cpumask) cast with explanatory comment. (3) Local int32_t exp_adj = 1 counter in the subnormal branch, bounded to [-9, 1], eliminates the unsigned wraps. -fsanitize=integer build (CC=clang meson setup -Db_sanitize=integer) exits 0/53 failures for the three fixed tests; test_f16_to_f32_subnormal asserts the bit-exact output is unchanged (2^-24). |
| CUDA picture_cuda.c integer-precision defects (CWE-190 / bugprone-narrowing-conversions, round-5 clang-tidy sweep) — five type-precision bugs in core/src/cuda/picture_cuda.c: (1,2) CUDA_MEMCPY2D.WidthInBytes in vmaf_cuda_picture_download_async and vmaf_cuda_picture_upload_async computed as a unsigned × unsigned product before being assigned to size_t, silently truncating for frames wider than ~2 GiB on 32-bit arithmetic. (3) Same overflow in cuMemAllocPitch width argument inside vmaf_cuda_picture_alloc. (4) aligned_y / aligned_c declared as int while computed from unsigned arithmetic, with a signed mask literal ~(DATA_ALIGN_PINNED - 1) that can produce signed UB near INT_MAX. (5) vmaf_ref_load() returns long (64-bit atomic counter) but the result was stored in int, narrowing silently. | this PR (fix/cuda-picture-widening-narrowing, 2026-05-10) | — (bug fix, no ADR per CLAUDE §12 r8) | (size_t) cast added to three w[i] * (…) operands; int → unsigned for aligned_y/aligned_c with 1u mask literal; int err → long err for vmaf_ref_load result. clang-tidy -checks=-*,bugprone-narrowing-conversions,bugprone-implicit-widening-of-multiplication-result -p core/build-cuda core/src/cuda/picture_cuda.c emits zero warnings for those checks. | | CUDA dispatch_strategy.c getenv() not MT-safe (latent, concurrency-mt-unsafe, round-5 clang-tidy sweep) — vmaf_cuda_select_strategy() called getenv("VMAF_CUDA_DISPATCH") on every invocation; per POSIX.1-2008 §2.2.2 calling getenv while another thread calls setenv/putenv/unsetenv is undefined behaviour. The function has no live callers today (stub per ADR-0181), so there was no practical crash risk, but the pattern would have become a real race when graph-capture callers arrive. | this PR (fix/cuda-dispatch-getenv-mt-safety, 2026-05-10) | — (latent bug fix, no ADR per CLAUDE §12 r8) | pthread_once guard (g_env_once / cache_env_dispatch) caches the env value at first call; strdup copies the string away from the live environment. clang-tidy -checks=-*,concurrency-* -p core/build-cuda core/src/cuda/dispatch_strategy.c emits zero warnings. |
| libvmaf.so.3 exported 207 internal symbols (libsvm C API, pdjson, SIMD kernels, internal helpers) — silent symbol interposition risk (Research-0092, round-4 sanitizer sweep angle 6) — nm -D --defined-only build-cpu/src/libvmaf.so.3.0.0 | grep ' [TW] ' | grep -v ' vmaf_' returned 207. Any downstream binary linking both libvmaf and libsvm would experience silent svm_predict / svm_train interposition. | this PR (fix/libvmaf-symbol-visibility, 2026-05-10) | ADR-0379 | After fix: nm -D --defined-only build-vis/src/libvmaf.so.3.0.0 | grep ' [TW] ' | grep -v ' vmaf_' | wc -l = 0. Public API exports: 44 vmaf_* symbols. meson test -C build-vis = 53/53 OK. |
| CUDA integer_adm_cuda.c macro missing parentheses + switch-missing-default (latent, round-5 clang-tidy bugprone-*) — (1) RES_BUFFER_SIZE macro expanded without surrounding parentheses; in an additive/shift context this can produce wrong values due to operator precedence. (2) Two switch(scale) statements in adm_dwt2_s123_combined_device (ADM DWT scales 1–3) and one in filter1d_16 in integer_vif_cuda.c (VIF scales 0–3) had no default: clause — an unexpected scale value would silently skip the dispatch. All fixed: macro wrapped in (…); default: break; added to all three switches. No scoring changes (added paths are unreachable with valid input). | this PR (fix/cuda-switch-defaults-macro-parens, 2026-05-10) | — (defensive code, no ADR per CLAUDE §12 r8) | clang-tidy -checks=-*,bugprone-macro-parentheses,bugprone-switch-missing-default-case -p core/build-cuda core/src/feature/cuda/integer_adm_cuda.c core/src/feature/cuda/integer_vif_cuda.c emits zero warnings. | | Vulkan GCC 16 build break — static void buffer-invalidate readback functions swallowed error codes with return <int>; — GCC 16 promotes -Wreturn-mismatch to a hard error, blocking the Vulkan build. Four sites in float_ansnr_vulkan.c (reduce_partials) and cambi_vulkan.c (cambi_vk_readback_image, cambi_vk_readback_mask) had return <int-expr>; inside static void functions guarding vmaf_vulkan_buffer_invalidate() calls. The error code was silently discarded under GCC 14–15; on a coherency-flush failure the CPU would read stale GPU-written memory and produce incorrect scores without surfacing any error. Note: The Vulkan backend was subsequently removed entirely in ADR-0726; this bug entry is preserved for historical record. | this PR (fix/vulkan-gcc16-return-mismatch, 2026-05-10) | ADR-0376 | Reproducer no longer applicable — Vulkan backend removed in ADR-0726. Historical: meson setup build-vk -Denable_vulkan=true && ninja -C build-vk exits 0 under GCC 16.1.1. |
| vmaf_cuda_buffer_upload_async / vmaf_cuda_buffer_download_async inverted stream-select ternary (common.c:388,416) — c_stream == 0 ? c_stream : cu_state->str passed NULL through when c_stream was NULL (using the CUDA default stream — the very thing PR #702 fixed) and silently overrode a non-NULL caller-supplied stream with cu_state->str. The correct fallback logic is c_stream != 0 ? c_stream : cu_state->str. No live callers exist today (both functions are uncalled), so the bug was a latent footgun only. Flagged by the cuda-reviewer pass on PR #702; deferred to this PR. | this PR (fix/cuda-upload-async-inverted-ternary, 2026-05-10) | — (one-char fix, no ADR per CLAUDE §12 r8) | grep -n "c_stream" core/src/cuda/common.c shows c_stream != 0 at both sites. meson test -C build-cuda 58/58 green. No callers affected (grep confirms zero call sites). |
| vmaf_framesync_init null-deref on buf_que OOM (CWE-690, gcc -fanalyzer round-4) — VmafFrameSyncBuf *buf_que = ctx->buf_que = malloc(...) result was not checked before the subsequent field writes; on OOM buf_que is NULL and buf_que->frame_data = NULL is undefined behaviour. No caller checked the return value of vmaf_framesync_init for OOM on the second allocation because the first ctx allocation was checked correctly. | this PR (fix/framesync-null-deref-buf-que, 2026-05-10) | — (bug fix, no ADR per CLAUDE §12 r8) | Guard added: on buf_que == NULL, pthreads primitives destroyed, ctx freed, *fs_ctx = NULL, return -ENOMEM. gcc -fanalyzer warning CWE-690 no longer emitted on framesync.c. meson test -C build --suite=fast green. |
| vmaf_picture_alloc null-deref via |= short-circuit bypass in vmaf_picture_set_release_callback (CWE-476, gcc -fanalyzer round-4) — vmaf_picture_priv_init can fail when malloc returns NULL (setting pic->priv = NULL). The caller used err |= vmaf_picture_set_release_callback(...), which evaluates unconditionally regardless of whether vmaf_picture_priv_init failed. Inside vmaf_picture_set_release_callback, priv = pic->priv is then NULL and priv->cookie = cookie is a null dereference. | this PR (fix/picture-alloc-priv-null-deref, 2026-05-10) | — (bug fix, no ADR per CLAUDE §12 r8) | err |= replaced with an early-return: if vmaf_picture_priv_init fails, jump to free_data: directly, skipping the callback setter. gcc -fanalyzer warning CWE-476 no longer emitted on picture.c. meson test -C build --suite=fast green. |
| T-CUDA-MOTION-SUB4K-PERF — CUDA motion feature at 576x324 was 0.55x CPU scalar on RTX 4090 — vmaf_cuda_picture_alloc created the per-picture upload stream with CU_STREAM_DEFAULT (flag = 0), opting the stream into the CUDA implicit null-stream serialisation rule. The per-frame context barrier cost ~50-100 µs vs a ~5-15 µs kernel, making every frame dominated by serialisation overhead. Crossover with CPU at ~1080p; GPU wins at 4K because the kernel runtime exceeds the barrier. | PR #695 (fix/motion-cuda-stream, 2026-05-10) | ADR-0378 / Research-0092 | Single line change: cuStreamCreate(..., CU_STREAM_DEFAULT) → cuStreamCreateWithPriority(..., CU_STREAM_NON_BLOCKING, 0) in core/src/cuda/picture_cuda.c:226. All three runtime stream-creation sites now use CU_STREAM_NON_BLOCKING. Re-bench at 576x324 post-fix to confirm ≥30 fps. |
| HIP float_psnr_hip scaffold stub promoted to first real kernel (T7-10b / ADR-0254) — every HIP consumer extractor returned -ENOSYS from init() because the kernel-template helpers were stubs (closed in the T7-10b runtime PR, 2026-05-08) but the device-side kernel code and the submit/collect launch chains were not yet written. float_psnr_hip was the simplest scalar-reduction metric and the natural first target. | this PR (feat/hip-float-psnr-first-real, 2026-05-09) | ADR-0254 | float_psnr/float_psnr_score.hip added: two HIP kernels (float_psnr_kernel_8bpc, _16bpc) performing the same per-pixel warp reduction as the CUDA twin. submit_fex_hip wired with hipMemcpy2DAsync (HtoD) + hipModuleLaunchKernel + hipEventRecord + hipMemcpyAsync (DtoH); collect_fex_hip wired with partial-sum host reduction + 10 * log10 score emission. enable_hipcc meson option added; hipcc --genco + xxd -i embed the HSACO fat binary. vmaf_hip_kernel_submit_post_record added to kernel_template.{h,c}. Both enable_hip=true (HIP build) and enable_hip=false (CPU build) compile cleanly. meson test -C build test_hip_smoke 21/21 green on a non-ROCm host (registration sub-test passes; kernel sub-tests skip on no-device). |
| T7-32 — motion_v2 AVX2 logical-vs-arithmetic right-shift divergence on negative-accumulator inputs — motion_score_pipeline_16_avx2 (16-bit Phase-1 path) used _mm256_srlv_epi64 (logical shift) to normalise the per-pixel accumulator. When accum + (1 << (bpc - 1)) is negative — which occurs whenever the distorted frame is uniformly brighter than the reference across the 5-row Gaussian support — scalar C >> bpc on int64_t yields a sign-extended negative result, while srlv_epi64 produces a large positive value (the two's-complement bit pattern shifted as unsigned). The int64-to-int32 pack then wrote the wrong sign bit into y_row[j], feeding a corrupted value into the Phase-2 absolute-sum accumulator. A companion bug in the test's scalar reference (mirror_idx using -1 instead of -2) masked the shift bug on all previously tested microarchitectures. The divergence became reproducible when the adversarial fixture (fill_adversarial_neg, bpc=10, seed=0xa5a5a5a5) produced scalar=59072324 avx2=59081027. | this PR (fix/motion-v2-avx2-arithmetic-shift, 2026-05-09) | — (correctness bug fix; no ADR per CLAUDE §12 r8; rebase-notes §0038 updated) | srav_epi64_imm inline helper added to core/src/feature/x86/motion_v2_avx2.c: logical shift OR sign-fill mask via srai_epi32(shuffle(v, 3,3,1,1), 31) + slli_epi64(sign, 64-n). Bit-exact vs scalar >> n on signed int64_t for 1 ≤ n ≤ 32. mirror_idx in core/test/test_motion_v2_simd.c corrected from -1 to -2 to match integer_motion_v2.c::mirror(). Both fixes together: all 4 adversarial fixtures pass (test_neg_diff_bpc10, test_neg_diff_bpc12, test_mixed_diff_bpc10, test_mixed_diff_bpc12). meson test -C build 50/50 OK. Netflix CPU goldens untouched (pure SIMD path, no scoring change for non-negative inputs). |
| OpenVINO NPU EP not exposed to --tiny-device — Research-0031 (2026-04) deferred Intel AI-PC NPU integration pending hardware access; SYCL audit research-0086 action item A.5 (2026-05-08) revisited under the oneAPI 2025.3.1 bump and recommended GO for the dispatch-layer wiring. The post-bump checklist row in docs/development/oneapi-install.md was unticked — --tiny-device=openvino-npu did not exist. | this PR (feat/openvino-npu-ep, 2026-05-08) | ADR-0332 (supersedes Research-0031 DEFER verdict for the wiring portion; end-to-end NPU silicon validation still pending hardware access) | New VmafDnnDevice enum entries OPENVINO_NPU / _CPU / _GPU route through try_append_openvino() in core/src/dnn/ort_backend.c with device_type ∈ {NPU, CPU, GPU}. CLI keywords openvino-npu / openvino-cpu / openvino-gpu accepted by cli_parse.c::ARG_TINY_DEVICE validator and mapped via vmaf.c::resolve_tiny_device(). Smoke-test core/test/dnn/test_ep_fp16.c extended with three new cases (test_explicit_openvino_{npu,cpu,gpu}_*); on a Ryzen 9950X3D + Arc A380 + RTX 4090 host (no Intel NPU silicon) all 10 EP tests pass with the graceful CPU-EP fallback path covering the absent NPU. ./build/tools/vmaf --tiny-device=openvino-cpu and --tiny-device=openvino-npu accepted by validator (post-validation error is the expected "Reference YUV required"). |
| integer_motion_cuda cross-stream race + pinned-memory leak + motion2_score skipping motion_fps_weight × clip + moving-average guard off-by-one — cuda-reviewer pass on 2026-05-09 surfaced four real defects in core/src/feature/cuda/integer_motion_cuda.c: (1) cuMemsetD8Async of the SAD accumulator ran on the drain stream s->str while the kernel that atomic-adds into the same buffer ran on the picture stream; both CU_STREAM_NON_BLOCKING, no event linkage, ordering by happenstance only; (2) s->sad_host (page-locked uint64_t from vmaf_cuda_buffer_host_alloc) was never freed in close_fex_cuda, leaking one pinned page per init/close cycle; (3) motion2_score was emitted as raw min(prev, cur) instead of the CPU reference's MIN(score * motion_fps_weight, motion_max_val) post-process from integer_motion.c:563; (4) s->frame_index is pre-incremented in collect() so the moving-average guard frame_index > 1 in motion3_postprocess_cuda triggered at framework-collect index 1 where the CPU reference (integer_motion.c:523's index > minimum_past_frames_needed = 1) skips. Bugs 3 + 4 only manifested under non-default motion_fps_weight ≠ 1.0 / motion_moving_average = true, which is why the default-config Netflix golden gate stayed green. | this PR (fix/cuda-motion-race-and-leaks, 2026-05-09) | ADR-0358 | (1) memset moved onto pic_stream, mirroring integer_motion_v2_cuda.c:188. (2) vmaf_cuda_buffer_host_free(s->sad_host) added to both close_fex_cuda and the init_fex_cuda free_ref: error unwind. (3) collect path (line 468 pre-fix) and flush path (line 359 pre-fix) now emit MIN(score * motion_fps_weight, motion_max_val). (4) guard relaxed to frame_index > 2 to compensate for pre-increment. Verification: compute-sanitizer --tool memcheck --leak-check full reports LEAK SUMMARY: 0 bytes leaked in 0 allocations post-fix (was 8 bytes leaked in 1 allocations traced to init_fex_cuda → cuMemHostAlloc); racecheck reports 0 hazards; meson test -C build-cuda is 55/55 green; cross-backend diff (CUDA vs CPU, default settings, Netflix src01_hrc00_576x324.yuv ↔ src01_hrc01_576x324.yuv, motion / motion2 / motion3 over 48 frames) is 0 / 144 mismatches at places=4, max_abs = 0.00e+00. Also lands two perf advisories on motion_score.cu + motion_v2_score.cu: pad shared-tile inner stride 20 → 21 (GCD(20, 32) = 4 → 2-way bank conflict; GCD(21, 32) = 1; +64 B/block) and add __launch_bounds__(BLOCK_X * BLOCK_Y, 8) to all four kernels. The cuda-reviewer also flagged common.c:388,416 inverted stream-select condition (no live callers) — fixed in this PR's successor (fix/cuda-upload-async-inverted-ternary, 2026-05-10). Motion3 GPU coverage (T3-15(c)) remains a separate feature track. | | T-NIGHTLY-TSAN-ADM-INIT — nightly.yml ThreadSanitizer job failed on 23 of 23 consecutive scheduled runs (2026-04-16 → 2026-05-08) due to a real data race in div_lookup_generator at core/src/feature/integer_adm.h:32-38. The static div_lookup[65537] table was populated without synchronisation by every worker thread spawned from vmaf_thread_pool_create; N threads raced to write the same 65 536 entries. Failing tests: test_model, test_framesync, test_pic_preallocation (3 of 50, exit status 66 = TSan diagnostic). | PR #548 (fix/sanitizer-real-bugs-2026-05-09, merged 2026-05-09) | — (defect fix; no new ADR per CLAUDE §12 r8) | pthread_once_t guard wraps div_lookup_populate; nightly CI green on 2026-05-09 and 2026-05-10 (2/2 runs post-fix). See SAN-INTEGER-ADM-DIV-LOOKUP-RACE row for implementation detail. | | SAN-INTEGER-ADM-DIV-LOOKUP-RACE — div_lookup_generator() in core/src/feature/integer_adm.h populated the 65 537-entry static div_lookup table from every worker thread spawned by vmaf_thread_pool_create with no synchronisation. TSan reported the overlapping writes during test_model, test_framesync, and test_pic_preallocation. Cross-confirmed by the nightly-triage agent (#537) and the sanitizer-matrix-scope agent (#540). | this PR (fix/sanitizer-real-bugs-2026-05-09, 2026-05-09) | — (defect fix; no new ADR per CLAUDE §12 r8) | pthread_once_t guard now wraps the populator; table contents are loop-invariant (div_Q_factor / i) so once-init preserves the bit-exact output every existing caller depends on. TSan-instrumented meson test -C build-tsan clean across test_framesync (5 s, 0 races), test_pic_preallocation (8/8 incl. multithreaded + stress), and test_model (39/39). Netflix golden CPU goldens unchanged: test_run_vmaf_runner_v061 + test_run_vmaf_runner_checkerboard both pass. | | SAN-FRAMESYNC-MUTEX-DOMAIN — core/src/framesync.c lines 102 / 125 mutated the buf_que linked-list spine (next pointers, buf_cnt, FREE/ACQUIRED/RETRIEVED transitions) under acquire_lock (M0) while submit_filled_data and retrieve_filled_data walked the same spine under retrieve_lock (M1) only. TSan flagged the inconsistent lock domains as a lock-ordering violation. Cross-confirmed by #537 and #540. | this PR (fix/sanitizer-real-bugs-2026-05-09, 2026-05-09) | — (defect fix; no new ADR per CLAUDE §12 r8) | Established a strict M0-before-M1 ordering invariant: every entry point that walks the spine takes M0 first; producer/consumer paths additionally take M1 for the condvar handshake; pthread_cond_wait releases M1 atomically and M0 is dropped before the wait so producers can append. Every pthread_mutex_* / pthread_cond_* return value is now checked or (void)-cast (per feedback_no_test_weakening). TSan-clean on test_framesync (10-frame producer/consumer test, 0 reports). | | SAN-MODEL-MALLOC-OOB — core/src/svm.cpp parse_header() and parse_support_vectors() fed unbounded nr_class / total_sv parsed from the SVM model file straight into Malloc(double *, model->nr_class - 1) (line 2955) and memcpy(support_vectors, sv_buffer.data(), sizeof(svm_node) * sv_buffer.size()) (line 2989), with no validation. On a crafted model file ASan reported alloc-too-big at the first Malloc and null-passed-as-argument at the memcpy. Cross-confirmed by #540. | this PR (fix/sanitizer-real-bugs-2026-05-09, 2026-05-09) | — (defect fix; no new ADR per CLAUDE §12 r8) | New VMAF_SVM_MAX_AXIS_COUNT (1 << 24, comfortably above Netflix vmaf_v0.6.1's ~6000 SVs) bounds-check applied at every parse-time entry that consumes nr_class / total_sv. parse_support_vectors() now throws cleanly when sv_buffer.empty(). All Malloc returns checked via exceptAssert. ASan-instrumented test_model 39/39 pass; well-formed vmaf_v0.6.1 parses unchanged (CLI vmaf --reference … --distorted … --model path=model/vmaf_v0.6.1.json smoke clean). | | SAN-PREDICT-METADATA-LEAK — ASan flagged 162 leaked bytes (16 + 128 + 11 + 7) across dict.c:53/59/121/124 strdup chain through feature_collector_dispatch_metadata / set_meta in test_propagate_metadata. The local VmafDictionary *dict was never freed by the test owning it. Cross-confirmed by #537 and #540. | this PR (fix/sanitizer-real-bugs-2026-05-09, 2026-05-09) | — (defect fix; no new ADR per CLAUDE §12 r8) | Added vmaf_dictionary_free(&dict) at the test's teardown. ASan-instrumented ./build-asan/test/test_predict reports 4 tests run, 4 passed with no leak summary (was: 162 byte(s) leaked in 4 allocation(s)). | | T-VK-VIF-1.4-RESIDUAL — vif residual mismatch at API 1.4 on the NVIDIA RTX 4090 + Vulkan 1.4.341 + driver 595.71.05 lane. Phase-2 (PR #510) localised it to a memory race in the Phase-4 cross-subgroup int64 reduction (vif.comp lines 547-592). The shader's bare barrier() was relying on implementation-defined shared-memory ordering that Vulkan 1.4 NVIDIA's stricter default memory model no longer provides. Note: on this multi-GPU host the same gate also surfaces a non-NVIDIA Arc-A380 residual that survives Phase-3 — tracked separately as T-VK-VIF-1.4-RESIDUAL-ARC (Open). | this PR (fix/vulkan-vif-int64-reduction-race-condition, 2026-05-09) | ADR-0269 Phase-3 status-update appendix (no new ADR per CLAUDE §12 r8 — bug fix, not architectural). | vif.comp now wraps the three cross-stage transitions in explicit memoryBarrierShared(); barrier(); pairs, which expand to SPIR-V OpControlBarrier with gl_StorageSemanticsShared | gl_SemanticsAcquireRelease shared-memory acquire-release semantics. Real hardware run on this session: NVIDIA RTX 4090 + driver 595.71.05 with the local Vulkan-1.4 apiVersion bump applied (core/src/vulkan/common.c 3 sites + vma_impl.cpp VMA_VULKAN_VERSION 1004000) gates 0/48 at places=4 across all four scales (was: 45/48 scale-2 at 1.527e-02 max abs). 5-run vif_vulkan=debug=true gives identical num_scale2 = +2.494358e+04, den_scale2 = +2.522523e+04 matching the CPU reference (was: 5 distinct pairs with 10¹¹× magnitudes + sign flips). RADV (Mesa 26.1.0) was already clean and stays clean. The 3 Netflix CPU goldens are untouched (Vulkan code path is independent). | | T-CoreML-EP-Wiring — Apple-silicon users had no path to the on-die Neural Engine for tiny-AI inference — the fork's --tiny-device grammar accepted only the auto / cpu / cuda / openvino / rocm keywords; Apple users running tiny-AI inference (saliency, fr_regressor, MOS head) silently degraded to the CPU EP regardless of available silicon, missing the substantial perf-per-watt win the Apple Neural Engine offers on M-series chips. | this PR (feat/coreml-ane-ep, 2026-05-09) | ADR-0365 | New --tiny-device=coreml{,-ane,-gpu,-cpu} selectors and matching VmafDnnDevice values 5..8 (append-only). Wiring uses the generic SessionOptionsAppendExecutionProvider("CoreMLExecutionProvider", …) key/value form so the Linux build degrades cleanly when the EP is absent. New smoke tests in core/test/dnn/test_ep_fp16.c (test_explicit_coreml_* x3) and core/test/dnn/test_cli.sh step 3 cover open-and-fallback + validator-accepts on the hardware-less Linux host. End-to-end ANE silicon validation deferred until Apple-silicon hardware access; the Linux CI lane covers the open-and-fallback path on every push. | | vmaf --feature float_adm printed "problem loading feature extractor: float_adm" on default builds — enable_float in core/meson_options.txt defaulted to false, so vmaf_fex_float_adm and all other float_* extractors were omitted from feature_extractor_list[] at build time. vmaf_get_feature_extractor_by_name("float_adm") returned NULL, vmaf_use_feature returned -EINVAL, and the CLI exited with the error. Reproducer verified on master commit 713031cb. | this PR (fix/float-adm-extractor-loading, 2026-05-09) | — (build-default bug fix; no ADR per CLAUDE §12 r8) | enable_float default flipped false → true in core/meson_options.txt. docs/development/build-flags.md updated. Reproducer now exits 0 and emits adm2 scores. meson test -C build 52/52 OK. Netflix CPU goldens untouched. | | motion_v2 public option-surface deferral (PR #453 / PR #460) — both PRs deferred upstream Netflix/vmaf commits a2b59b77 (motion_five_frame_window) and 4e469601 (remaining motion_v2 options + motion3_v2_score) on the architectural concern that v2's option surface would duplicate motion v1's existing surface (ADR-0158). The deferral row blocked any future model file referencing motion_v2=motion_blend_factor=… from loading on the fork. | this PR (feat/motion-v2-public-api-options-adr, 2026-05-09) | ADR-0337 | ADR-0337 picks alternative A1 (duplicate option surfaces). v1 (integer_motion.c) and v2 (integer_motion_v2.c) each register their own VmafOption[] table; the seven option names (motion_force_zero, motion_blend_factor, motion_blend_offset, motion_fps_weight, motion_max_val, motion_five_frame_window, motion_moving_average) match upstream 4e469601 byte-for-byte. The mirror off-by-one fix from 856d3835 is propagated to scalar + AVX2 + AVX-512 + fork-local NEON (CUDA / SYCL / HIP / Vulkan twins refresh tracked in docs/rebase-notes.md §0337). motion_five_frame_window=true is rejected at init() with -ENOTSUP mirroring ADR-0219 §Decision's GPU motion3 precedent; the picture-pool prev_prev_ref plumbing (8 conflict regions on the fork's ADR-0152 read_pictures* decomposition) is deferred to a follow-up PR. CPU build clean (ninja -C build — 584 targets); meson test -C build reports 51/52 OK with the pre-existing T7-32 test_motion_v2_simd adversarial-fixture guard fail (ADR-0038 follow-up; reproduces on master HEAD without any changes from this PR). Smoke: vmaf … --feature 'motion_v2=motion_blend_factor=0.5' --xml -o /tmp/r.xml writes 49 frames carrying VMAF_integer_feature_motion3_v2_score_mbf_0.5. ENOTSUP guard: --feature 'motion_v2=motion_five_frame_window=1' → problem loading feature extractor: motion_v2 + stderr motion_v2: motion_five_frame_window=true is not supported … see ADR-0337. Netflix golden gate (ADR-0024) untouched (motion v1 unchanged). | | vmaf --feature ssim could not resolve — vmaf_fex_ssim was defined in core/src/feature/integer_ssim.c but not listed in feature_extractor.c's feature_extractor_list[], and integer_ssim.c was not in core/src/meson.build — pure dead code masquerading as a shipped feature surfaced by docs/research/0091-partial-integration-audit-2026-05-08.md (PR #454). docs/metrics/features.md row 41 advertised a Vulkan twin via T7-24 but no Vulkan kernel for the fixed-point path actually existed; the only Vulkan SSIM kernel in core/src/feature/vulkan/ssim_vulkan.c defines vmaf_fex_float_ssim_vulkan (the floating-point twin), not vmaf_fex_ssim_vulkan. | this PR (fix/ssim-extractor-registration, 2026-05-08) | — (registry-omission bug fix; no ADR per CLAUDE §12 r8) | integer_ssim.c added to core/src/meson.build; extern VmafFeatureExtractor vmaf_fex_ssim; declared and &vmaf_fex_ssim added to feature_extractor_list[] in feature_extractor.c; config.h include added to integer_ssim.c to align the VmafFeatureExtractor struct layout across TUs (the HAVE_CUDA / HAVE_SYCL / HAVE_VULKAN conditional members were previously seen by only one of the two TUs, tripping -Wlto-type-mismatch on the Vulkan-enabled LTO link). New test_ssim_extractor_registered_and_extracts in core/test/test_feature_extractor.c asserts the extractor resolves by name and emits a ssim score; passes on both CPU-only and Vulkan-enabled builds (50/50 meson-tests green, the new test included). CLI smoke: vmaf --reference testdata/ref_576x324_48f.yuv --distorted testdata/dis_576x324_48f.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --feature ssim --output /tmp/ssim_smoke.json writes a non-empty <metric name="ssim" mean="0.996108" /> row (was: silently dropped before the fix). Cross-extractor sanity: max abs diff between the fixed-point ssim and the floating-point float_ssim over 48 frames of the fork's ref_576x324_48f ↔ dis_576x324_48f pair is 2.46e-4 — well below the documented places=4 cross-backend tolerance, consistent with the expected fixed-point quantisation gap. docs/metrics/features.md row 41 + footnote ² updated to drop the misleading Vulkan claim. Netflix golden gate untouched (3/3 CPU goldens still pass; the fix only adds a missing registration and does not change any score path). | | T6-1 / Tiny-AI C1 baseline fr_regressor_v1.onnx — Deferred (waiting on external dataset access) row triggered 2026-04-29 when the Netflix Public dataset became locally available at .workingdir2/netflix/. Baseline trained, ONNX shipped, sidecar + ADR + doc-page landed; docs/state.md never recorded the closure (CLAUDE.md §12 r13 reviewer-enforced rule, no CI gate). Backfilled by this PR (state.md staleness sweep 2026-05-08). | PR #249 / commit f809ce09 (merged 2026-05-02) | ADR-0249 (status: Accepted) supersedes the C1 deferral in ADR-0168 §"Defer C1". | model/tiny/fr_regressor_v1.onnx (real weights) + model/tiny/fr_regressor_v1.json sidecar + docs/ai/models/fr_regressor_v1.md doc page all on master. Registry row carries smoke: false. PLCC against vmaf_v0.6.1 recorded in ADR-0249 §Decision. | | T6-2a-followup' / saliency replacement of mobilesal_placeholder_v0 — three-path unblock recorded in ADR-0265 §Consequences ("(a) widen op_allowlist; (b) mirror u2netp.pth; (c) train fork-saliency-student"). Path A + path C are closed; path B is in flight as PR #469. The saliency surface is no longer content-independent — saliency_student_v1 is the production weight in model/tiny/registry.json. Backfilled by this PR (state.md staleness sweep 2026-05-08). | Path A (op-allowlist Resize decision): closed by ADR-0258 (Accepted 2026-05-03 — opted against per-attribute enforcement per ADR-0169 wire-scanner-scope rule). Path C (fork-trained student): PR #359 / commit landed 2026-05-05. Path B (u2netp upstream-mirror via fork release artefact): merged as PR #469. | ADR-0265 (Status update 2026-05-08 appendix); ADR-0286 (path C); ADR-0258 (path A). | model/tiny/saliency_student_v1.onnx (~113 K params, ONNX opset 17, Apache-2.0 / BSD-3-Clause-Plus-Patent) trained from scratch on DUTS-TR; reported IoU 0.6558 on DUTS-TR. C-side feature_mobilesal.c extractor unchanged (same input / saliency_map tensor names). bash core/test/dnn/test_registry.sh clean. (verified 2026-05-16: PR #469 MERGED — all three paths A/B/C closed.) | | Embedded MCP server compute_vmaf returned {"status":"deferred_to_v2"} and UDS transport returned -ENOSYS (T5-2c v2) — PR #490 / T5-2b shipped the v1 stdio runtime but punted two pieces: compute_vmaf only validated inputs and returned a placeholder marker (no actual scoring), and vmaf_mcp_start_uds still returned -ENOSYS (Unix-domain-socket bind/listen body not yet written). AI hosts driving libvmaf via the embedded MCP surface had no functional scoring tool, and embedded hosts that wanted a non-stdio surface had nothing to bind to. | this PR (feat/mcp-runtime-v2, 2026-05-09) | ADR-0332 | Added core/src/mcp/compute_vmaf.c (real scoring binding via per-call ephemeral VmafContext + vmaf_model_load + POSIX YUV reader + vmaf_read_pictures + vmaf_score_pooled mean) and core/src/mcp/transport_uds.c (socket(AF_UNIX, SOCK_STREAM, 0) + bind + chmod 0700 + listen + serial accept loop with line-delimited JSON-RPC framing). vmaf_mcp_start_uds flipped from -ENOSYS to a real lifecycle. Smoke test extended to 16 sub-tests (was 15): test_uds_roundtrip connects an AF_UNIX client, round-trips a tools/list request, verifies the socket file is unlinked on close; test_compute_vmaf_real_score round-trips a tools/call against the testdata 576x324 48-frame YUV pair, asserts a numeric score field is present and the v1 placeholder string is gone. Real probe against testdata/{ref,dis}_576x324_48f.yuv with vmaf_v0.6.1: pooled mean VMAF = 94.32 over 48 frames. 16/16 sub-tests green. SSE / loopback HTTP transport remains pinned at -ENOSYS (mongoose vendoring deferred to v3). Repro: meson setup build -Dlibvmaf:enable_mcp=true -Dlibvmaf:enable_mcp_stdio=true -Dlibvmaf:enable_mcp_uds=true && ninja -C build && build/test/test_mcp_smoke. | | Embedded MCP server returned -ENOSYS from every entry point (T5-2b) — the T5-2 audit-first PR (ADR-0209) shipped the public header, build flags, stub TU, and smoke test, but every transport-start entry point returned -ENOSYS, so the server was inert and the smoke test only verified the stub contract. Phase-A audit (HP-4) flagged the gap from "scaffold landed" to "responds to a real request". | this PR (feat/mcp-runtime-stdio-v1, 2026-05-08) | ADR-0209 § "Status update 2026-05-08: MCP runtime stdio + 2 tools landed (T5-2b v1)" | Vendored cJSON v1.7.18 (MIT) at core/src/mcp/3rdparty/cJSON/. Added core/src/mcp/dispatcher.c (JSON-RPC 2.0 routing for initialize / tools/list / tools/call / resources/list) + core/src/mcp/transport_stdio.c (line-delimited JSON-RPC framing on a caller-supplied fd pair, dedicated MCP pthread, 64 KiB per-line cap per Power-of-10 §2). Two tools shipped: list_features (walks the canonical extractor list and returns names actually present in the build) and compute_vmaf (validates reference_path/distorted_path and returns a deferred-to-v2 marker; the scoring binding lands in v2). vmaf_mcp_init/_start_stdio/_stop/_close flipped from -ENOSYS to a real lifecycle; _start_sse/_start_uds still -ENOSYS (mongoose vendoring + UDS body trimmed to v2). Smoke test core/test/test_mcp_smoke.c flipped from pinning the scaffold contract to driving real JSON-RPC round-trips over a pipe(2) pair: 15/15 sub-tests green (meson test -C build test_mcp_smoke). Repro: meson setup build -Denable_mcp=true -Denable_mcp_stdio=true && ninja -C build && meson test -C build test_mcp_smoke. | | HP-2 — vmaf-tune HDR encoder + scorer wiring missing despite shipped hdr.py + CLI flags. PR #434 / ADR-0300 added tools/vmaf-tune/src/vmaftune/hdr.py (detect_hdr / hdr_codec_args / select_hdr_vmaf_model) and the four --auto-hdr / --force-sdr / --force-hdr-pq / --force-hdr-hlg CLI flags but never imported the module from corpus.iter_rows. Result: PQ-tagged sources silently encoded as SDR with their mastering-display + max-CLL SEI metadata stripped; the promised schema-v2 hdr_transfer / hdr_primaries / hdr_forced row columns never materialised. Surfaced by Phase-A audit follow-up 2026-05-08 (grep -nE "from.*\.hdr\|import.*hdr" tools/vmaf-tune/src/vmaftune/*.py returned zero hits). | this PR (feat/vmaf-tune-hdr-iter-rows-integration, 2026-05-08) | ADR-0300 § Status update 2026-05-08 (immutability per ADR-0028 honoured — original body left untouched) | corpus.iter_rows now imports vmaftune.hdr and resolves the effective HDR mode once per source via _resolve_hdr (auto / force-sdr / force-hdr-pq / force-hdr-hlg), injects hdr_codec_args(opts.encoder, info) into EncodeRequest.extra_params, swaps in an HDR VMAF model when select_hdr_vmaf_model() returns one (else logs a one-shot warning and keeps the SDR model), and populates the new hdr_transfer / hdr_primaries / hdr_forced row columns via _row_for. SCHEMA_VERSION bumped 2 → 3 (additive). The two integration tests previously gated by _HDR_ITER_ROWS_DEFERRED (test_corpus_emits_hdr_fields_when_source_is_hdr, test_corpus_force_sdr_skips_hdr_path) are un-skipped; python -m pytest tools/vmaf-tune/tests/test_hdr.py -q reports 21 passed. | | HIP backend public API + kernel-template helpers returned -ENOSYS (T7-10b runtime deferred since ADR-0212 scaffold) — every entry point in core/src/hip/{common,kernel_template}.c returned -ENOSYS; eight kernel-template consumers (psnr, float_psnr, ciede, float_moment, float_ansnr, motion_v2, float_motion, float_ssim) were registered but their init() short-circuited at the kernel-template -ENOSYS line. Builds with -Denable_hip=true linked no ROCm runtime (the dependency('hip-lang') probe was required: false). | this PR (feat/hip-runtime-t7-10b, 2026-05-08) | ADR-0212 §"Status update 2026-05-08" | kernel_template.c now wraps real hipStreamCreateWithFlags / hipEventCreateWithFlags / hipMallocAsync-equivalent / hipMemsetAsync / hipStreamWaitEvent / hipStreamSynchronize calls; common.c wraps hipGetDeviceCount / hipSetDevice / hipStreamCreateWithFlags / hipGetDeviceProperties. meson.build hard-requires libamdhip64 (with cc.find_library fallback rooted at /opt/rocm/lib because ROCm 7.x has no hip-lang.pc). vmaf_hip_import_state stays at -ENOSYS until T7-10c. meson test -C build test_hip_smoke reports 20/20 sub-tests green on a host with gfx1036 visible (AMD Ryzen 9 9950X3D iGPU); kernel-template lifecycle round-trip + pinned-host hipMemcpy round-trip verified. Full meson test -C build reports 51/51 OK. | | fr_regressor_v2_ensemble_v1_seed{0..4} shipped synthetic-corpus scaffold weights with smoke: true — the five seed ONNX files committed in ADR-0303's scaffold PR (#372) were 3025-byte synthetic-corpus checkpoints (1 epoch each) and the registry rows carried smoke: true. PROMOTE.json from the LOSO trainer (ADR-0319) had been green for days (mean PLCC 0.997, spread 0.001) but PR #423's metadata-only flip attempt tripped core/test/dnn/test_registry.sh because no per-seed sidecars existed and was closed for redo. | this PR (feat/fr-regressor-v2-ensemble-full-prod-flip, 2026-05-06) | ADR-0321 | New driver ai/scripts/export_ensemble_v2_seeds.py re-trains each seed on the full Phase A canonical-6 corpus (5,640 rows over 9 sources × h264_nvenc) and exports five real-weight ONNXs (opset 17, two-input). Per-seed sidecars model/tiny/fr_regressor_v2_ensemble_v1_seed{N}.json mirror the canonical fr_regressor_v2.json shape (encoder vocab v2, codec_block_layout, scaler params, training_recipe) plus seed-specific gate-pass evidence from PROMOTE.json. Registry rows updated with new sha256 + smoke: false. bash core/test/dnn/test_registry.sh reports OK: 19 registry entries verified (was: FAIL no sidecars). python -c "import onnx; m=onnx.load('model/tiny/fr_regressor_v2_ensemble_v1_seed0.onnx'); onnx.checker.check_model(m)" passes for all 5 seeds. |
| fr_regressor_v2 ensemble seeds parked at smoke: true pending real-corpus LOSO verdict — PR #399 (ADR-0303) shipped the LOSO trainer + CI gate scaffold with the five fr_regressor_v2_ensemble_v1_seed{0..4} rows registered as smoke: true; PR #405 (ADR-0309) shipped the harness wrapper + validator but the trainer body was still NotImplementedError; PR #422 (ADR-0319) plugged in the real loader + per-fold trainer, leaving the registry flip as the explicit follow-up gated on a passing PROMOTE.json. Without the flip, downstream consumers (vmaf-tune --quality-confidence, ADR-0237 Phase A) could not rely on the ensemble for predictive-distribution queries. | this PR (feat/fr-regressor-v2-ensemble-seeds-prod-flip, 2026-05-06) | ADR-0320 (closes the ADR-0303 flip contract on the ADR-0319 verdict) | Operator ran bash ai/scripts/run_ensemble_v2_real_corpus_loso.sh end-to-end against the locally-generated Phase A canonical-6 corpus (5,640 rows from 9 Netflix sources × h264_nvenc × 4 CQs); python ai/scripts/validate_ensemble_seeds.py runs/ensemble_v2_real/ emitted runs/ensemble_v2_real/PROMOTE.json with verdict: PROMOTE, mean_plcc = 0.9972533887602454 (gate ≥ 0.95 ✓), plcc_spread = 0.0009510602756565012 (gate ≤ 0.005 ✓), per-seed PLCC range [0.9969, 0.9978], no failing seeds. Verdict file committed at model/tiny/fr_regressor_v2_ensemble_v1_seed_flip_PROMOTE.json as the immutable audit trail. The five seed rows in model/tiny/registry.json now carry smoke: false. No aggregate fr_regressor_v2_ensemble_v1_mean row exists today; if added later, ADR-0303's variance-bound clause governs that flip. Trainer / gate / validator unchanged — ADRs 0303 / 0309 / 0319 are the honoured contract. |
| vf_libvmaf_tune full scoring deferred in patch 0008 — ffmpeg-patches/0008-add-libvmaf_tune-filter.patch (PR #410 / ADR-0312) shipped the libvmaf_tune filter as scaffold-only: it accepted both inputs but never invoked libvmaf, accumulating a synthetic pseudo-score and emitting a recommended_crf=… line derived from a linear CRF↔VMAF interpolation. End-to-end recipes in docs/usage/vmaf-tune-ffmpeg.md ran to completion but the recommendation was decoupled from real frame quality. | this PR (feat/ffmpeg-patches-0008-full-scoring, 2026-05-06) | ADR-0312 (sub-decision: vf_libvmaf_tune full-scoring deferral retired; no new ADR per CLAUDE §12 r8) | Patch 0008 now mirrors vf_libvmaf.c's CPU framesync pipeline: init() runs vmaf_init + vmaf_model_load + vmaf_use_features_from_model; do_vmaf_tune() allocates VmafPictures, copies plane data, and calls vmaf_read_pictures(ref, dist, frame_cnt); uninit() flushes via vmaf_read_pictures(NULL, NULL, 0) and extracts the mean via vmaf_score_pooled(VMAF_POOL_METHOD_MEAN). The recommendation curve is still piece-wise linear (slope 0.4 CRF / VMAF point near the 90–96 sweet spot); per-clip TPE search remains in tools/vmaf-tune/src/vmaftune/recommend.py. Smoke: ffmpeg -hide_banner -f lavfi -i "color=red:size=128x128:r=10:d=1" -f lavfi -i "color=red:size=128x128:r=10:d=1" -lavfi "[0:v][1:v]libvmaf_tune=recommend_target_vmaf=95" -f null - against the fork-built libvmaf 3.0.0 reports recommended_crf=35.5 (target_vmaf=95.0, observed_vmaf=97.43, n_frames=10) — observed VMAF is the real pooled score, not the scaffold's static 95.0 placeholder. 9/9 patches still apply against pristine n8.1 via git am --3way; ffmpeg builds clean against the fork's DNN-enabled libvmaf. | | ffmpeg-patches/0008-add-libvmaf_tune-filter.patch referenced removed AVFilterLink::frame_rate — frame_rate moved off AVFilterLink onto a new FilterLink struct in FFmpeg n7+; sibling patches 0005 (vf_libvmaf_sycl.c) and 0006 (vf_libvmaf_vulkan.c) already used the post-n7 ff_filter_link() accessor, but patch 0008 was written against the n6-era API and slipped through CI because the FFmpeg-Vulkan lane only builds vf_libvmaf.o, not vf_libvmaf_tune.c. Discovered by PR #415's investigation while path-filtering the FFmpeg-integration workflow (ADR-0317). | PR #416 / commit c1a2eccc (merged 2026-05-06) | — (bug-fix on existing ADR-0312 surface; no new ADR per CLAUDE §12 r8) | config_output() now does ff_filter_link(outlink)->frame_rate = ff_filter_link(mainlink)->frame_rate;, mirroring patches 0005/0006. Series replay against pristine n8.1 (git -C /path/to/ffmpeg-8 reset --hard n8.1 && for p in ffmpeg-patches/000*-*.patch; do git am --3way "$p" || break; done) applies all 9 patches clean; make -j$(nproc) libavfilter/vf_libvmaf_tune.o ffmpeg builds green against fork-installed libvmaf 3.0.0. The full SYCL lane catches future regressions of this kind (path filter from PR #415 already includes ffmpeg-patches/**). | | SVT-AV1 ROI bridge deferred in patch 0007 — ffmpeg-patches/0007-libvmaf-tune-qpfile-unified.patch (PR #409 / ADR-0312) shipped the libsvtav1 hook as scaffold-only: it parsed the qpfile but did not set enable_roi_map or attach ROI_MAP_EVENT priv-data nodes. Salient-region encodes through the patched libsvtav1 silently fell back to plain encoder defaults. | PR #417 / commit 6f30814b (merged 2026-05-06) | ADR-0312 (sub-decision: SVT-AV1 deferral retired; no new ADR per CLAUDE §12 r8) | Patch 0007 now caches the parsed qpfile in SvtContext, builds one SvtAv1RoiMapEvt per qpfile frame upfront (per-MB qp_offsets averaged into a per-64×64-SB b64_seg_map of up to 8 segment QPs), flips enc_params.enable_roi_map = true before svt_av1_enc_set_parameter, and attaches each event as a ROI_MAP_EVENT priv-data node (with node->size = sizeof(SvtAv1RoiMapEvt*) per resource_coordination_process.c validation) on every eb_send_frame(). Lifetime: the events + maps live for the full encode session because SVT-AV1 reads ROI_MAP_EVENT data via shallow-copied pointers on async pipeline threads. Gated on SVT_AV1_CHECK_VERSION(1, 6, 0); older SVT-AV1 builds fall back to log-and-continue. Smoke: ffmpeg -f lavfi -i testsrc2=size=128x128:r=10:d=0.5 -c:v libsvtav1 -qpfile /tmp/test.qpfile -f null - against SVT-AV1 v4.1.0 logs ROI bridge enabled. and encodes 5 frames clean (was: Svt[error]: invalid private data of type ROI_MAP_EVENT in the scaffold draft). 9/9 patches still apply against pristine n8.1 via git am --3way. | | libaom-av1 ROI bridge deferred in patch 0007 — ffmpeg-patches/0007-libvmaf-tune-qpfile-unified.patch (PR #409 / ADR-0312) shipped the libaom-av1 hook as scaffold-only: it parsed the qpfile but never called aom_codec_control(AOME_SET_ROI_MAP, ...). Salient-region encodes through the patched libaom-av1 silently fell back to plain encoder defaults. | this PR (feat/ffmpeg-patches-0007-libaom-roi, 2026-05-06) | ADR-0312 (sub-decision: libaom-av1 deferral retired; no new ADR per CLAUDE §12 r8) | Patch 0007 now caches the parsed qpfile in AOMContext, allocates a per-mi-cell segment-id map at libaom's mode-info grid (ALIGN_POWER_OF_TWO(dim, 8) >> 2, since av1/common/enums.h::MI_SIZE == 4), and on every encoded frame picks up to 8 segment QPs from the per-frame qp_offset value range (uniform linear binning when the span exceeds AOM_MAX_SEGMENTS == 8), paints the per-mi segment map by expanding each per-16×16-MB qp_offset into a 4×4 block of mi cells, and issues aom_codec_control(&ctx->encoder, AOME_SET_ROI_MAP, &roi_map). libaom deep-copies the segment map and delta_q[] table on every control call (per av1/encoder/encoder.c::av1_set_roi_map memcpy), so a single buffer is reused across frames; the qpfile + map are freed in aom_free(). Smoke: ffmpeg -f lavfi -i testsrc2=size=128x128:r=10:d=0.5 -c:v libaom-av1 -qpfile /tmp/test.qpfile -f null - against libaom v3.13.3 logs ROI bridge enabled. and encodes 5 frames clean. 9/9 patches still apply against pristine n8.1 via git am --3way. Trade-off: the 8-segment cap rounds nearby qp_offsets together when the saliency model emits more than 8 distinct values per frame; finer granularity requires vmaf-tune corpus instead. | | cli_parse.c error() assert on long-only options with non-numeric optarg — handlers for --threads / --subsample / --cpumask passed a synthesised short-option char ('t' / 's' / 'c') into parse_unsigned(); on a bad optarg, error() walked long_opts[] for that char, found nothing (the options are long-only), and tripped assert(long_opts[n].name) -> SIGABRT instead of clean usage error. Surfaced by the libFuzzer harness landed in PR #408 (ADR-0311); reproducer was parked at core/test/fuzz/cli_parse_known_crashes/cli_threads_abbrev_assert.argv. | this PR (fix/cli-parse-long-only-error-assertion, 2026-05-06) | ADR-0316 (follow-up to ADR-0311) | Pass the long-only ARG_* enum value into parse_unsigned() so error() finds the matching long_opts[] row via the existing < 256 branch — brings the three call-sites into parity with the seven sibling handlers (ARG_GPUMASK, ARG_FRAME_*, ARG_*_DEVICE, ARG_TINY_THREADS). New unit test core/test/test_cli_parse_long_only_args.c (POSIX fork()/waitpid(); 4 cases) confirms --threads abc / --subsample xyz / --cpumask qqq / --th=foosoxe exit(1) with Invalid argument on stderr instead of SIGABRT. Reproducer promoted from cli_parse_known_crashes/ to cli_parse_corpus/; known_assert_in_input early-reject filter removed from fuzz_cli_parse.c; 60 s fuzzer smoke clean post-fix. | | ssimulacra2_cuda GPU module leak + per-scale malloc in hot path — init_fex_cuda calls cuModuleLoadData for both ssimulacra2_blur_ptx + ssimulacra2_mul_ptx and stores the handles in Ssimu2StateCuda::module_blur / ::module_mul; close_fex_cuda destroyed the stream and freed buffers but never called cuModuleUnload, leaking ~200-500 KB of GPU-resident module backing store per vmaf_close() cycle. The leak is invisible to compute-sanitizer --tool memcheck because the leak-checker tracks cuMem*Alloc only. Surfaced by the 2026-05-09 cuda-reviewer pass. Bundled with three additional perf fixes in the same PR: per-scale malloc(3 * width * height * sizeof(float)) removed from extract_fex_cuda (replaced with two pre-allocated pinned scratch buffers), full-plane H2D / D2H shrunk to per-plane scale_w * scale_h * sizeof(float) (~15× PCIe traffic reduction at 1080p scale 2), __launch_bounds__(64, 32) added to the blur kernels. | this PR (fix/ssimulacra2-cuda-leaks-perf, 2026-05-09) | ADR-0356 + Research-0067 | Bit-exact at places=4 (0/48 mismatches, max abs diff 0.000000e+00) on the Netflix 576×324 fixture: python3 scripts/ci/cross_backend_vif_diff.py --vmaf-binary core/build/tools/vmaf --reference testdata/ref_576x324_48f.yuv --distorted testdata/dis_576x324_48f.yuv --width 576 --height 324 --feature ssimulacra2 --backend cuda --places 4. The 8-byte cuMemHostAlloc leak still reported by compute-sanitizer is from a separate code path (not ssimulacra2). H-pass non-coalesced reads + V-pass L1 pressure remain known follow-ups (require shared-memory tile-transpose rewrite). | | Metal scaffold batch-1: psnr, float_ssim, and motion consumer kernels — T8-1 scaffold PR (#661) wired the first three consumer extractors (psnr_metal, float_ssim_metal, motion_v2_metal) into the Metal backend runtime stub. Every entry point returns -ENOSYS per the T8-1 scaffold contract until T8-1b flips the runtime; the kernel-template helpers and init() / submit() / collect() stubs are fully scaffolded and compile cleanly under enable_metal=enabled. | PR #661 (feat/metal-scaffold-batch-1, merged 2026-05-10) | ADR-0361 | Three new .metal source stubs (psnr_metal.metal, float_ssim_metal.metal, motion_v2_metal.metal) + C wrappers added under core/src/feature/metal/. Smoke: meson test -C build test_metal_smoke 17/17 green (14 original + 3 new consumer stubs) against the -ENOSYS contract on a non-macOS host. enable_metal defaults remain auto (on on macOS, off elsewhere). | | SYCL CAMBI port closes CUDA → SYCL parity gap (T3-15(g)) — integer_cambi_sycl.cpp was the last major metric missing a SYCL twin; CUDA and CPU had full coverage but SYCL users fell back to the CPU path. | PR #662 (feat/sycl-integer-cambi-port, merged 2026-05-10) | ADR-0371 | New core/src/feature/sycl/integer_cambi_sycl.cpp + core/test/test_integer_cambi_sycl.c (17 sub-tests). meson test -C build-sycl test_integer_cambi_sycl 17/17 green on an Intel Arc A380 host; cross-backend diff (SYCL vs CPU) 0/48 mismatches at places=4. | | ssimulacra2_cuda PTX module unload + per-scale malloc (memory leak fix) — cuModuleUnload was never called for the two PTX-backed modules (module_blur, module_mul) per close_fex_cuda, leaking GPU-resident backing store per vmaf_close() cycle; also removed the per-frame malloc from the hot extract_fex_cuda path. | PR #670 (fix/ssimulacra2-cuda-leaks-perf, merged 2026-05-10) | ADR-0356 | compute-sanitizer --tool memcheck --leak-check full reports 0 bytes leaked post-fix (was ~200–500 KB / vmaf_close cycle). Per-scale scratch buffers pre-allocated at init; H2D/D2H shrunk to per-plane dimensions (~15× PCIe reduction at 1080p scale 2). Bit-exact at places=4: 0/48 mismatches on Netflix 576×324 fixture. | | Vulkan VIF API-1.4 NVIDIA Phase 2 dump (T-VK-VIF-1.4-RESIDUAL research) — Phase-2 research dump collects NV_SHADER_DUMP + SPIR-V disassembly of vif.comp at API 1.3 vs 1.4 for the NVIDIA RTX 4090 lane, producing the binary-diff evidence needed for Phase-3 fence-variant selection. | PR #671 (research/vif-1.4-residual-bisect-2026-05-08, merged 2026-05-10) | — (research-only dump; T-VK-VIF-1.4-RESIDUAL tracked in Open section; no ADR per CLAUDE §12 r8) | docs/research/T-VK-VIF-1.4-RESIDUAL.md committed with SPIR-V diff and NV_SHADER_DUMP at API 1.3 vs 1.4 + Phase-2 hypothesis matrix. Confirms the driver emits an OpControlBarrier at 1.3 that is removed at 1.4 under the stricter memory model. Phase-3 fork candidates identified. | | fix(ai): run_ensemble_v2_real_corpus_loso.sh wrapper-trainer interface mismatch — the shell wrapper passed --corpus-dir to train_ensemble_v2_real_corpus.py but the trainer accepted --input-jsonl; every LOSO fold exited with unrecognized arguments: --corpus-dir, producing empty fold outputs and a vacuous PROMOTE.json. | PR #673 (fix/ensemble-retrain-harness-interface-clean, merged 2026-05-10) | ADR-0318 | run_ensemble_v2_real_corpus_loso.sh argument renamed --corpus-dir → --input-jsonl. Smoke: bash ai/scripts/run_ensemble_v2_real_corpus_loso.sh --input-jsonl runs/phase_a_canonical_6.jsonl --output-dir /tmp/loso_smoke --seeds 0 completes fold 0 without argument errors. python ai/scripts/validate_ensemble_seeds.py /tmp/loso_smoke emits verdict: PROMOTE on a 1-seed run. | | fr_regressor_v2 ensemble seeds smoke → prod flip + fr_regressor_v2 registry SHA mismatch fixed — ADR-0320 records the flip claim; the actual prod ONNX files were not shipped in #674; the registry SHA mismatch introduced when #674 updated sidecar hashes without regenerating the ONNX blobs was fixed by PR #677. | PR #674 (feat/fr-regressor-v2-ensemble-seeds-prod-flip-clean, merged 2026-05-10); fix PR #677 (fix/fr-regressor-v2-ensemble-registry-sha-mismatch, merged 2026-05-10) | ADR-0320 | PR #674 flipped the five seed registry rows from smoke: true → smoke: false and updated sidecar hashes. PR #677 regenerated the five ONNX files to match the sidecar SHA256 values and corrected the model/tiny/registry.json rows. bash core/test/dnn/test_registry.sh reports OK: N registry entries verified post-#677. | | HIP batch-1 real kernels: integer_psnr_hip + float_ansnr_hip — following the T7-10b runtime PR (#657), the first two HIP consumer extractors are promoted from scaffold stubs to real GPU kernels with hipcc-compiled HSACO fat binaries. | PR #675 (feat/hip-port-batch-1, merged 2026-05-10) | ADR-0372 | core/src/feature/hip/integer_psnr/psnr_score.hip + core/src/feature/hip/float_ansnr/float_ansnr_score.hip added. submit_fex_hip / collect_fex_hip wired for both extractors. meson test -C build-hip test_hip_smoke 23/23 green (21 original + 2 new kernel sub-tests) on a gfx1036 host. Cross-backend diff (HIP vs CPU, Netflix 576×324 fixture) 0/48 mismatches at places=4 for both features. | | HIP batch-2 real kernel: float_motion_hip — promotes the temporal float_motion_hip extractor (5×5 Gaussian blur ping-pong, per-block float SAD, motion2 tail flush) from -ENOSYS scaffold to a real HIP module-API consumer. HIP real-kernel count: 4 of 11. | PR open (feat/hip-port-batch-2) | ADR-0373 | float_motion_hip.c rewritten with #ifdef HAVE_HIPCC dual-path; float_motion_score added to hip_kernel_sources in meson.build. CPU-only build (-Denable_hip=true -Denable_hipcc=false) compiles clean. | | OpenVINO NPU EP wiring: --tiny-device=openvino-npu surface — Research-0031 deferred Intel AI-PC NPU integration; the oneAPI 2025.3.1 bump re-opened the path. --tiny-device=openvino-npu / openvino-cpu / openvino-gpu selectors and matching VmafDnnDevice entries were absent from the CLI. | PR #676 (feat/openvino-npu-ep-clean, merged 2026-05-10) | ADR-0332 | New VmafDnnDevice values OPENVINO_NPU, OPENVINO_CPU, OPENVINO_GPU + try_append_openvino() in core/src/dnn/ort_backend.c. CLI keywords accepted by ARG_TINY_DEVICE validator; graceful CPU-EP fallback on hardware-less Linux hosts. Smoke: 10/10 EP tests pass (no NPU silicon required for the fallback path). | | OpenVINO CLI validator + fr_regressor_v2 registry SHA fix — PR #676 shipped the NPU EP wiring but the CLI validator did not accept the new openvino-{npu,cpu,gpu} keywords; separately, the fr_regressor_v2 registry row carried a stale SHA256 after the batch-1 flip in #674. | PR #677 (fix/fr-regressor-v2-ensemble-registry-sha-mismatch, merged 2026-05-10) | — (validator + registry fix; no new ADR per CLAUDE §12 r8) | cli_parse.c ARG_TINY_DEVICE case table extended with openvino-npu, openvino-cpu, openvino-gpu keywords. model/tiny/registry.json fr_regressor_v2 SHA256 updated to match the regenerated ONNX. bash core/test/dnn/test_registry.sh clean. |
MCP transport assertion-density compliance — scripts/ci/assertion-density.sh gate requires ≥ 1 assertion per 10 non-blank lines in all core/src/mcp/*.c TUs; the v2 transport files (compute_vmaf.c, transport_uds.c) shipped below threshold. | PR #678 (fix/mcp-assertion-density, merged 2026-05-10) | — (CI compliance fix; no new ADR per CLAUDE §12 r8) | Added VMAF_RETURN_ERR_IF / assert guards at every socket, bind, and scoring entry point in the two under-density TUs. bash scripts/ci/assertion-density.sh core/src/mcp/ exits 0 post-fix (was: 2 files below threshold). meson test -C build test_mcp_smoke 16/16 green. |
|---|---|---|---|
OSSF Scorecard workflow red on every push to master — github/codeql-action/upload-sarif@b25d0ebf40e5... was an "imposter commit" (a SHA that no longer exists in the action's repository, so Scorecard's webapp returns 400 on the publish-results call). Workflow-level failure was masking a stable 6.2 / 10 aggregate score that should have been the live signal. | this PR (chore/ossf-scorecard-remediation, 2026-05-03) | ADR-0263 + Research-0053 | Repinned to current v4 head e46ed2cbd0...; verified via gh api /repos/github/codeql-action/commits/e46ed2cb... returns 200 (was 422 for the old SHA). Scorecard policy + per-check accepted-blocker list documented in ADR-0263 §Decision; active remediation queue in Research-0053 §Action plan. |
y4m_convert_411_422jpeg 1-byte heap-buffer-overflow on 4:1:1 streams with dst_c_w == 1 — the chroma-upsample routine's first two sub-loops unconditionally wrote _dst[(x << 1) | 1]; the third sub-loop already carried the correct (x << 1 | 1) < dst_c_w guard. Triggered by any 4:1:1 Y4M whose width (after Daala chroma decimation) reduces to a 1-pixel destination chroma row — minimal repro: YUV4MPEG2 W2 H4 F30:1 Ip C411. Surfaced by the libFuzzer harness staged in PR #348. | PR #357 / commit 05ba29a6 (merged 2026-05-04), report PR #348 (merged 2026-05-04) | — (one-line guard fix, no ADR per CLAUDE §12 r8) | core/test/test_y4m_411_oob.c exercises the W=2 H=4 4:1:1 stream end-to-end through video_input_open + video_input_fetch_frame; ASan-clean after the fix, faults at y4m_input.c:507 with WRITE of size 1 before. The 3 canonical Netflix CPU golden tests (src01_hrc00_576x324, both checkerboard_1920_1080 shifts) are unaffected — none use 4:1:1 with dst_c_w == 1. |
#239 — FFmpeg libvmaf_vulkan filter wall-clock serialisation (lawrence profile 2026-04-30) — synchronous fence wait inside vmaf_vulkan_import_image (ADR-0186 v1) blocked the FFmpeg decoder thread on every frame, preventing CPU/GPU overlap | PR #241 / commit e266bf8e (2026-05-02), Issue #239 closed 2026-05-03 | ADR-0251 (renumbered from 0235 in PR #310 dedup sweep) | v2 async pending-fence ring shipped; the v2 ≤ 0.7 × v1 measurement gate flipped ADR-0251 from Proposed to Accepted. Reproducer: ffmpeg -hwaccel vulkan -i ref.mkv -i dis.mkv -filter_complex '[0:v]hwupload[r];[1:v]hwupload[d];[r][d]libvmaf_vulkan' -f null - against the Netflix normal pair shows the wall-clock improvement on lavapipe + hardware. Netflix golden CPU gate unchanged (Vulkan path is host-side; goldens are CPU-only per ADR-0214 / CLAUDE §8) |
vmaf_tiny_v1.onnx external-data filename ref broken on load — ONNXRuntime fails with "External data path validation failed for initializer: 0.weight" because the v1 ONNX referenced mlp_small_final.onnx.data while only vmaf_tiny_v1.onnx.data was committed | PR #296 / commit fa81d5b4 (merged 2026-05-03) | — (artifact-only fix, no ADR) | python3 -c 'import onnxruntime; onnxruntime.InferenceSession("model/tiny/vmaf_tiny_v1.onnx")' loads cleanly; the v2-vs-v1 diff path in validate_vmaf_tiny_v2.py runs end-to-end (was erroring on v1 load before) |
kernel_template.h 8-SSBO binding cap blocked float_adm_vulkan (9 bindings) — vmaf_vulkan_kernel_pipeline_create() returned -EINVAL at init, surfaced as "problem reading pictures" / "problem flushing context" in the cross-backend gate run | PR #288 / commit bb9d772e (merged 2026-05-02) — bundled with the float_adm_vulkan migration (T-GPU-DEDUP-22) | — (template-extension fix; ADR-0246 covers the template proper) | Cap raised 8 → 16, named VMAF_VULKAN_KERNEL_MAX_SSBO_BINDINGS constant introduced in PR #292 / commit 76d6d41e; float_adm_vulkan smoke run reports adm2 mean = 0.934515 (was failing to extract) |
scripts/ci/deliverables-check.sh mis-stripped backslashes from heredoc-quoted PR bodies — gh pr create heredocs add escaped-backtick sequences that survive tr -d (which only strips backticks/asterisks/underscores), breaking the - [x].*ITEM regex (~18 PRs affected this session before the diagnosis landed) | PR #292 / commit 76d6d41e (merged 2026-05-03) | — (CI script hardening, no ADR) | Extended tr -d to also strip backslashes; a test fixture with literal escaped-backticks around AGENTS.md now prints OK (ticked) |
CI workflows ran on draft PRs, burning runner-minutes — none of the 7 pull_request-triggered workflows filtered on the draft flag, silently violating single-active-CI policy whenever a subagent pushed a branch as draft | PR #300 / commit 257f1e28 (merged 2026-05-03) | — (CI-infrastructure fix, no ADR) | 33 jobs across 7 workflows now carry a draft-skip guard (if: clause that allows pull_request events only when pull_request.draft == false). The ready_for_review event re-triggers CI on un-draft; push-to-master and workflow_dispatch are unaffected |
CLAUDE.md §12 r14 ffmpeg-patches reviewer command was wrong — for p in ffmpeg-patches/000*-*.patch; do git apply --check "$p"; done only succeeds for patch 0001 because patches 0002–0006 build on each other; correct gate is git am --3way series replay against pristine n8.1 | PR #297 / commit b161fc39 (merged 2026-05-03) | — (rule wording fix, no ADR) | 2026-05-02 /refresh-ffmpeg-patches skill run: per-patch apply --check failed on 4/6 patches; git am --3way series replay succeeded for all 6 |
docs/state.md + CHANGELOG.md carried 15 stale ADR slug refs (slug renames where NNNN stayed but filename evolved, e.g. 0152-monotonic-index-rejection.md → 0152-vmaf-read-pictures-monotonic-index.md) | PR #304 / commit 3cbb0956 (merged 2026-05-03) | — (doc cleanup, no ADR) | mkdocs --strict build clean; spot-check verifies each rewritten ref points at the actual on-disk filename for that NNNN. 11 wrong-NNNN refs (different concept under same NNNN, e.g. 0246-gpu-kernel-template.md while disk-0221 is now changelog-adr-fragment-pattern.md) split into a separate per-ADR-review PR (#306) |
1.07e-3 CPU vmaf_v0.6.1 score drift between /usr/local/bin/vmaf v3.0.0 and master tip — surfaced by 2026-05-02 /run-netflix-bench subagent run; well within Netflix golden's places=2 tolerance, so the gate did NOT fire, but the drift was stable + reproducible | PR #305 / commit ae1dafad (merged 2026-05-03) — bisect identifies upstream Netflix a44e5e61 (motion edge-mirror bugfix, Kyle Swanson 2026-04-17) inherited at fork root. Per-feature isolation: drift is entirely integer_motion (-1.005e-3) + integer_motion2 (-0.985e-3); ADM and VIF are bit-identical. Snapshot regen via PR #309 aligns testdata/netflix_benchmark_results.json with the fork's actual behavior. | — (bisect triage, no ADR) | /bisect-regression predicate against vmaf_v0.6.1.json brackets fork root 41301496 ↔ master 4cd3a8d8; "first bad" = fork root means drift was inherited, not introduced. Doc at docs/development/cpu-score-drift-bisect-2026-05-02.md |
T7-16 — NVIDIA-Vulkan + SYCL adm_scale2 boundary drift (2.4e-4, 1/48 frames) baseline at PR #120 | this PR (empirical close, sister of T7-15) | — (verification-only close, no ADR) | python3 scripts/ci/cross_backend_vif_diff.py --feature adm --backend vulkan --device 0 (NVIDIA proprietary 595.58.3.0) reports adm_scale2 max_abs_diff = 1e-6 (JSON %f print floor; ULP=0) at places=4, 0/48 mismatches. Same bit-exact result on Vulkan device 1 (Arc Mesa anv 26.0.5) and SYCL device 0 (Arc A380). 2.4e-4 baseline at PR #120 is gone. No adm_vulkan.c / adm_sycl.cpp commits since PR #120 — same NVCC / driver / SYCL-runtime upgrade hypothesis as T7-15 |
T7-15 — motion_cuda + motion_sycl 2.6e-3 SAD drift vs CPU integer_motion on 47/48 frames (surfaced by PR #120's corrected cross-backend gate) | #172 (empirical close — no motion-kernel commits between PR #120 and master; the NVCC 13.x / NVIDIA-driver upgrade since PR #120 is the most likely cause of the bit-exact restoration) | — (no ADR — verification-only close; reopened as a new T-row if the gate ever re-fails) | python3 scripts/ci/cross_backend_vif_diff.py --feature motion --backend cuda reports max_abs_diff=0.0 at places=8 over 48 frames (was 2.6e-3 47/48 mismatches). SYCL on Arc and Vulkan on Mesa anv each show 1e-6 (JSON %f print-rounding floor; ULP=0). All three backends pass the existing places=4 contract; the gate locks the contract going forward |
libvmaf_vulkan.h not installed under prefix → FFmpeg --enable-libvmaf-vulkan silently drops the filter (lawrence repro 2026-04-28 19:27) | #175 (4b43ad2f, 2026-04-28) | — | meson install --destdir /tmp/x produces /tmp/x/usr/local/include/libvmaf/libvmaf_vulkan.h post-fix (was missing); FFmpeg configure --enable-libvmaf-vulkan then passes the check_pkg_config libvmaf_vulkan ... libvmaf/libvmaf_vulkan.h vmaf_vulkan_state_init_external probe and the libvmaf_vulkan filter actually builds |
libvmaf.pc Cflags leak (build-dir -include path) on static builds — broke lawrence's BtbN FFmpeg configure 2026-04-27 22:19 | this PR | ADR-0200 | pkg-config --cflags libvmaf post-fix returns -I${includedir} -I${includedir}/libvmaf -DVK_NO_PROTOTYPES -pthread (no leaked path); rename behaviour byte-for-byte identical (0 GLOBAL vk*, 719 vmaf_priv_vk* in static libvmaf.a); shared libvmaf.so Cflags unchanged |
volk / vk* symbol clash in BtbN-style fully-static FFmpeg builds (lawrence repro 2026-04-27) | #152 (73620ff5, 2026-04-27) | ADR-0198 | Static nm libvmaf.a reports 0 GLOBAL vk* (was ~700); BtbN-style gcc -static main.c libvmaf.a libvulkan-stub.a link succeeds; test_vulkan_smoke 10/10 pass |
Netflix#755 — vmaf_score_pooled interleaves with vmaf_read_pictures | #91 9b983e0a (2026-04-24) | ADR-0154 | API contract test + Netflix golden gate (CPU bit-identical) |
| Netflix#910 — out-of-order flush misses last frame | #88 f478c65d (2026-04-24) | ADR-0152 | Regression test rejects non-monotonic indices with -EINVAL |
Netflix#1414 — float_ms_ssim broken at <176×176 | #90 7905ac78 (2026-04-24) | ADR-0153 | Init-time rejection with -EINVAL + regression test |
Netflix#1420 — CUDA concurrency assert at cuda/common.c:166 | #93 49a64088 (2026-04-24) | ADR-0156 | 178 CHECK_CUDA sites replaced with -errno propagation; OOM reducer hits -ENOMEM (was: assert(0)) |
| Netflix#1300 — CUDA preallocation memory leak | #94 fd1b22c2 (2026-04-24) | ADR-0157 | New vmaf_cuda_state_free() API + ASan reducer confirms 0 framework-side leaked bytes across 10 init/preallocate/fetch/close cycles |
Netflix#1486 — motion edge-mirror + motion_max_val + motion3 output | #95 383190a4 (2026-04-24) | ADR-0158 | Doc-only verify-PR; substance already on master via earlier incremental commits |
| Netflix#1376 — Python FIFO hang on slow IO | #85 e5a52e74 (2026-04-24) | ADR-0149 | Replaces 1-second polling with multiprocessing.Semaphore |
| Netflix#1472 — CUDA feature extraction broken on Windows MSYS2/MinGW | #86 f9d1cae2 (2026-04-24) | ADR-0150 | Linux CPU 32/32 + CUDA 35/35 + Windows MSVC+CUDA CI build-only green |
| Netflix#1430 — locale-unsafe parsing (comma decimal) | #74 e0e78db3 (earlier) | ADR-0137 | New thread_locale.{c,h} subsystem; round-trip parse tests |
Netflix#1382 / #1381 — cuMemFreeAsync use-after-free on concurrent free | #72 (Batch-A) | ADR-0131 | cuMemFree port; assertion-0 crash no longer reproduces |
Netflix#1476 — UB in void* pointer arithmetic + VIF-init memory leak | leak: #47; UB: master b0a4ac3a | — | ASan repro green before/after |
| --- | --- | --- | --- |
T-VK-VIF-1.4-RESIDUAL — vif residual mismatch at API 1.4 on the NVIDIA RTX 4090 + Vulkan 1.4.341 + driver 595.71.05 lane. Phase-2 (research-PR #510) localised it to a memory race in the Phase-4 cross-subgroup int64 reduction (vif.comp lines 547-592). The shader's bare barrier() was relying on implementation-defined shared-memory ordering that Vulkan 1.4 NVIDIA's stricter default memory model no longer provides. Note: on this multi-GPU host the same gate also surfaces a non-NVIDIA Arc-A380 residual that survives Phase-3 — tracked separately as T-VK-VIF-1.4-RESIDUAL-ARC (Open). (verified 2026-05-09: gh pr view 511 → MERGED 2026-05-09T00:51:11Z.) | PR #511 (fix/vulkan-vif-int64-reduction-race-condition, merged 2026-05-09) | ADR-0269 Phase-3 status-update appendix (no new ADR per CLAUDE §12 r8 — bug fix, not architectural). | vif.comp now wraps the three cross-stage transitions in explicit memoryBarrierShared(); barrier(); pairs, which expand to SPIR-V OpControlBarrier with gl_StorageSemanticsShared | gl_SemanticsAcquireRelease shared-memory acquire-release semantics. Real hardware run on this session: NVIDIA RTX 4090 + driver 595.71.05 with the local Vulkan-1.4 apiVersion bump applied (core/src/vulkan/common.c 3 sites + vma_impl.cpp VMA_VULKAN_VERSION 1004000) gates 0/48 at places=4 across all four scales (was: 45/48 scale-2 at 1.527e-02 max abs). 5-run vif_vulkan=debug=true gives identical num_scale2 = +2.494358e+04, den_scale2 = +2.522523e+04 matching the CPU reference (was: 5 distinct pairs with 10¹¹× magnitudes + sign flips). RADV (Mesa 26.1.0) was already clean and stays clean. The 3 Netflix CPU goldens are untouched (Vulkan code path is independent). |
vmaf --feature ssim could not resolve — vmaf_fex_ssim was defined in core/src/feature/integer_ssim.c but not listed in feature_extractor.c's feature_extractor_list[], and integer_ssim.c was not in core/src/meson.build — pure dead code masquerading as a shipped feature surfaced by docs/research/0091-partial-integration-audit-2026-05-08.md (PR #454). docs/metrics/features.md row 41 advertised a Vulkan twin via T7-24 but no Vulkan kernel for the fixed-point path actually existed; the only Vulkan SSIM kernel in core/src/feature/vulkan/ssim_vulkan.c defines vmaf_fex_float_ssim_vulkan (the floating-point twin), not vmaf_fex_ssim_vulkan. (verified 2026-05-09: gh pr view 470 → MERGED 2026-05-08T22:05:26Z; on-disk grep "vmaf_fex_ssim" core/src/feature/feature_extractor.c confirms registry entry.) | PR #470 (fix/ssim-extractor-registration, merged 2026-05-08) | — (registry-omission bug fix; no ADR per CLAUDE §12 r8) | integer_ssim.c added to core/src/meson.build; extern VmafFeatureExtractor vmaf_fex_ssim; declared and &vmaf_fex_ssim added to feature_extractor_list[] in feature_extractor.c; config.h include added to integer_ssim.c to align the VmafFeatureExtractor struct layout across TUs (the HAVE_CUDA / HAVE_SYCL / HAVE_VULKAN conditional members were previously seen by only one of the two TUs, tripping -Wlto-type-mismatch on the Vulkan-enabled LTO link). New test_ssim_extractor_registered_and_extracts in core/test/test_feature_extractor.c asserts the extractor resolves by name and emits a ssim score; passes on both CPU-only and Vulkan-enabled builds (50/50 meson-tests green, the new test included). CLI smoke: vmaf --reference testdata/ref_576x324_48f.yuv --distorted testdata/dis_576x324_48f.yuv --width 576 --height 324 --pixel_format 420 --bitdepth 8 --feature ssim --output /tmp/ssim_smoke.json writes a non-empty <metric name="ssim" mean="0.996108" /> row (was: silently dropped before the fix). Cross-extractor sanity: max abs diff between the fixed-point ssim and the floating-point float_ssim over 48 frames of the fork's ref_576x324_48f ↔ dis_576x324_48f pair is 2.46e-4 — well below the documented places=4 cross-backend tolerance, consistent with the expected fixed-point quantisation gap. docs/metrics/features.md row 41 + footnote ² updated to drop the misleading Vulkan claim. Netflix golden gate untouched (3/3 CPU goldens still pass; the fix only adds a missing registration and does not change any score path). |
T6-1 / Tiny-AI C1 baseline fr_regressor_v1.onnx — Deferred (waiting on external dataset access) row triggered 2026-04-29 when the Netflix Public dataset became locally available at .workingdir2/netflix/. Baseline trained, ONNX shipped, sidecar + ADR + doc-page landed; docs/state.md never recorded the closure (CLAUDE.md §12 r13 reviewer-enforced rule, no CI gate). Backfilled by this PR (state.md staleness sweep 2026-05-08). | PR #249 / commit f809ce09 (merged 2026-05-02) | ADR-0249 (status: Accepted) supersedes the C1 deferral in ADR-0168 §"Defer C1". | model/tiny/fr_regressor_v1.onnx (real weights) + model/tiny/fr_regressor_v1.json sidecar + docs/ai/models/fr_regressor_v1.md doc page all on master. Registry row carries smoke: false. PLCC against vmaf_v0.6.1 recorded in ADR-0249 §Decision. (verified 2026-05-09: PR #249 MERGED 2026-05-02; ONNX file present on master.) |
T6-2a-followup' / saliency replacement of mobilesal_placeholder_v0 — three-path unblock recorded in ADR-0265 §Consequences ("(a) widen op_allowlist; (b) mirror u2netp.pth; (c) train fork-saliency-student"). Path A + path C are closed; path B is in flight as PR #469. The saliency surface is no longer content-independent — saliency_student_v1 is the production weight in model/tiny/registry.json. Backfilled by this PR (state.md staleness sweep 2026-05-08). | Path A (op-allowlist Resize decision): closed by ADR-0258 (Accepted 2026-05-03 — opted against per-attribute enforcement per ADR-0169 wire-scanner-scope rule). Path C (fork-trained student): PR #359 / commit landed 2026-05-05. Path B (u2netp upstream-mirror via fork release artefact): in flight as PR #469. | ADR-0265 (Status update 2026-05-08 appendix); ADR-0286 (path C); ADR-0258 (path A). | model/tiny/saliency_student_v1.onnx (~113 K params, ONNX opset 17, Apache-2.0 / BSD-3-Clause-Plus-Patent) trained from scratch on DUTS-TR; reported IoU 0.6558 on DUTS-TR. C-side feature_mobilesal.c extractor unchanged (same input / saliency_map tensor names). bash core/test/dnn/test_registry.sh clean. (verified 2026-05-16: PR #359 MERGED 2026-05-05; PR #469 MERGED — path B closed; all three paths now closed.) |
fr_regressor_v2_ensemble_v1_seed{0..4} shipped synthetic-corpus scaffold weights with smoke: true — the five seed ONNX files committed in ADR-0303's scaffold PR (#372) were 3025-byte synthetic-corpus checkpoints (1 epoch each) and the registry rows carried smoke: true. PROMOTE.json from the LOSO trainer (ADR-0319) had been green for days (mean PLCC 0.997, spread 0.001) but PR #423's metadata-only flip attempt tripped core/test/dnn/test_registry.sh because no per-seed sidecars existed and was closed for redo. (verified 2026-05-09: gh pr view 424 → MERGED 2026-05-06T12:08:19Z; PR #423 confirmed CLOSED \| null (redo); five model/tiny/fr_regressor_v2_ensemble_v1_seed{0..4}.json sidecars present on master.) | PR #424 (feat/fr-regressor-v2-ensemble-full-prod-flip, merged 2026-05-06) | ADR-0321 | New driver ai/scripts/export_ensemble_v2_seeds.py re-trains each seed on the full Phase A canonical-6 corpus (5,640 rows over 9 sources × h264_nvenc) and exports five real-weight ONNXs (opset 17, two-input). Per-seed sidecars model/tiny/fr_regressor_v2_ensemble_v1_seed{N}.json mirror the canonical fr_regressor_v2.json shape (encoder vocab v2, codec_block_layout, scaler params, training_recipe) plus seed-specific gate-pass evidence from PROMOTE.json. Registry rows updated with new sha256 + smoke: false. bash core/test/dnn/test_registry.sh reports OK: 19 registry entries verified (was: FAIL no sidecars). python -c "import onnx; m=onnx.load('model/tiny/fr_regressor_v2_ensemble_v1_seed0.onnx'); onnx.checker.check_model(m)" passes for all 5 seeds. |
vf_libvmaf_tune full scoring deferred in patch 0008 — ffmpeg-patches/0008-add-libvmaf_tune-filter.patch (PR #410 / ADR-0312) shipped the libvmaf_tune filter as scaffold-only: it accepted both inputs but never invoked libvmaf, accumulating a synthetic pseudo-score and emitting a recommended_crf=… line derived from a linear CRF↔VMAF interpolation. End-to-end recipes in docs/usage/vmaf-tune-ffmpeg.md ran to completion but the recommendation was decoupled from real frame quality. (verified 2026-05-09: gh pr view 420 → MERGED 2026-05-06T09:31:53Z.) | PR #420 (feat/ffmpeg-patches-0008-full-scoring, merged 2026-05-06) | ADR-0312 (sub-decision: vf_libvmaf_tune full-scoring deferral retired; no new ADR per CLAUDE §12 r8) | Patch 0008 now mirrors vf_libvmaf.c's CPU framesync pipeline: init() runs vmaf_init + vmaf_model_load + vmaf_use_features_from_model; do_vmaf_tune() allocates VmafPictures, copies plane data, and calls vmaf_read_pictures(ref, dist, frame_cnt); uninit() flushes via vmaf_read_pictures(NULL, NULL, 0) and extracts the mean via vmaf_score_pooled(VMAF_POOL_METHOD_MEAN). The recommendation curve is still piece-wise linear (slope 0.4 CRF / VMAF point near the 90–96 sweet spot); per-clip TPE search remains in tools/vmaf-tune/src/vmaftune/recommend.py. Smoke: ffmpeg -hide_banner -f lavfi -i "color=red:size=128x128:r=10:d=1" -f lavfi -i "color=red:size=128x128:r=10:d=1" -lavfi "[0:v][1:v]libvmaf_tune=recommend_target_vmaf=95" -f null - against the fork-built libvmaf 3.0.0 reports recommended_crf=35.5 (target_vmaf=95.0, observed_vmaf=97.43, n_frames=10) — observed VMAF is the real pooled score, not the scaffold's static 95.0 placeholder. 9/9 patches still apply against pristine n8.1 via git am --3way; ffmpeg builds clean against the fork's DNN-enabled libvmaf. (verified 2026-05-09: PR #420 MERGED; ADR-0312 sub-decision flag retired.) |
ffmpeg-patches/0008-add-libvmaf_tune-filter.patch referenced removed AVFilterLink::frame_rate — frame_rate moved off AVFilterLink onto a new FilterLink struct in FFmpeg n7+; sibling patches 0005 (vf_libvmaf_sycl.c) and 0006 (vf_libvmaf_vulkan.c) already used the post-n7 ff_filter_link() accessor, but patch 0008 was written against the n6-era API and slipped through CI because the FFmpeg-Vulkan lane only builds vf_libvmaf.o, not vf_libvmaf_tune.c. Discovered by PR #415's investigation while path-filtering the FFmpeg-integration workflow (ADR-0317). | PR #416 / commit c1a2eccc (merged 2026-05-06) | — (bug-fix on existing ADR-0312 surface; no new ADR per CLAUDE §12 r8) | config_output() now does ff_filter_link(outlink)->frame_rate = ff_filter_link(mainlink)->frame_rate;, mirroring patches 0005/0006. Series replay against pristine n8.1 (git -C /path/to/ffmpeg-8 reset --hard n8.1 && for p in ffmpeg-patches/000*-*.patch; do git am --3way "$p" || break; done) applies all 9 patches clean; make -j$(nproc) libavfilter/vf_libvmaf_tune.o ffmpeg builds green against fork-installed libvmaf 3.0.0. The full SYCL lane catches future regressions of this kind (path filter from PR #415 already includes ffmpeg-patches/**). (verified 2026-05-09: gh pr view 416 → MERGED 2026-05-06.) |
SVT-AV1 ROI bridge deferred in patch 0007 — ffmpeg-patches/0007-libvmaf-tune-qpfile-unified.patch (PR #409 / ADR-0312) shipped the libsvtav1 hook as scaffold-only: it parsed the qpfile but did not set enable_roi_map or attach ROI_MAP_EVENT priv-data nodes. Salient-region encodes through the patched libsvtav1 silently fell back to plain encoder defaults. | PR #417 / commit 6f30814b (merged 2026-05-06) | ADR-0312 (sub-decision: SVT-AV1 deferral retired; no new ADR per CLAUDE §12 r8) | Patch 0007 now caches the parsed qpfile in SvtContext, builds one SvtAv1RoiMapEvt per qpfile frame upfront (per-MB qp_offsets averaged into a per-64×64-SB b64_seg_map of up to 8 segment QPs), flips enc_params.enable_roi_map = true before svt_av1_enc_set_parameter, and attaches each event as a ROI_MAP_EVENT priv-data node (with node->size = sizeof(SvtAv1RoiMapEvt*) per resource_coordination_process.c validation) on every eb_send_frame(). Lifetime: the events + maps live for the full encode session because SVT-AV1 reads ROI_MAP_EVENT data via shallow-copied pointers on async pipeline threads. Gated on SVT_AV1_CHECK_VERSION(1, 6, 0); older SVT-AV1 builds fall back to log-and-continue. Smoke: ffmpeg -f lavfi -i testsrc2=size=128x128:r=10:d=0.5 -c:v libsvtav1 -qpfile /tmp/test.qpfile -f null - against SVT-AV1 v4.1.0 logs ROI bridge enabled. and encodes 5 frames clean (was: Svt[error]: invalid private data of type ROI_MAP_EVENT in the scaffold draft). 9/9 patches still apply against pristine n8.1 via git am --3way. (verified 2026-05-09: gh pr view 417 → MERGED 2026-05-06.) |
libaom-av1 ROI bridge deferred in patch 0007 — ffmpeg-patches/0007-libvmaf-tune-qpfile-unified.patch (PR #409 / ADR-0312) shipped the libaom-av1 hook as scaffold-only: it parsed the qpfile but never called aom_codec_control(AOME_SET_ROI_MAP, ...). Salient-region encodes through the patched libaom-av1 silently fell back to plain encoder defaults. (verified 2026-05-09: gh pr view 419 → MERGED 2026-05-06T09:05:51Z.) | PR #419 (feat/ffmpeg-patches-0007-libaom-roi, merged 2026-05-06) | ADR-0312 (sub-decision: libaom-av1 deferral retired; no new ADR per CLAUDE §12 r8) | Patch 0007 now caches the parsed qpfile in AOMContext, allocates a per-mi-cell segment-id map at libaom's mode-info grid (ALIGN_POWER_OF_TWO(dim, 8) >> 2, since av1/common/enums.h::MI_SIZE == 4), and on every encoded frame picks up to 8 segment QPs from the per-frame qp_offset value range (uniform linear binning when the span exceeds AOM_MAX_SEGMENTS == 8), paints the per-mi segment map by expanding each per-16×16-MB qp_offset into a 4×4 block of mi cells, and issues aom_codec_control(&ctx->encoder, AOME_SET_ROI_MAP, &roi_map). libaom deep-copies the segment map and delta_q[] table on every control call (per av1/encoder/encoder.c::av1_set_roi_map memcpy), so a single buffer is reused across frames; the qpfile + map are freed in aom_free(). Smoke: ffmpeg -f lavfi -i testsrc2=size=128x128:r=10:d=0.5 -c:v libaom-av1 -qpfile /tmp/test.qpfile -f null - against libaom v3.13.3 logs ROI bridge enabled. and encodes 5 frames clean. 9/9 patches still apply against pristine n8.1 via git am --3way. Trade-off: the 8-segment cap rounds nearby qp_offsets together when the saliency model emits more than 8 distinct values per frame; finer granularity requires vmaf-tune corpus instead. |
integer_motion_cuda / float_motion_cuda last-frame duplicate-write warning + context could not be synchronized — PR #312 (T-GPU-OPT-1, CUDA fence batching at engine scope) reordered flush_context_cuda so the pending double-buffered collect runs before fex->flush(). For motion the pending collect already wrote motion2_score[s->index] / motion3_score[s->index] on the last frame; flush_fex_cuda then re-appended at the same index, tripping vmaf_feature_collector_append's monotonic-index guard. Surfaced as libvmaf WARNING feature "VMAF_integer_feature_motion2_score" cannot be overwritten at index N followed by libvmaf ERROR context could not be synchronized / problem flushing context on every CUDA last-frame. Backfilled to state.md by Research-0086 (2026-05-08 audit); fix shipped 2026-05-05. | PR #391 / commit ab695acb (merged 2026-05-05) | — (targeted regression fix on PR #312's CUDA fence-batching surface; no new ADR per CLAUDE §12 r8) | New append_if_unwritten helper probes vmaf_feature_collector_get_score and skips the append if the slot is already populated. flush_fex_cuda routes both the motion2 and motion3 last-frame writes through it; pre- and post-#312 callers stay correct. Repro ./core/build-cuda/tools/vmaf --gpumask=0 --no_sycl --no_vulkan -r src01_hrc00_576x324.yuv -d src01_hrc01_576x324.yuv -w 576 -h 324 -p 420 -b 8 --model path=model/vmaf_v0.6.1.json runs clean (was: problem flushing context exit). Pooled VMAF deltas are within ADR-0214 places=4 (CPU 94.323011 vs CUDA 94.324112, |Δ|=1.1e-3). (verified 2026-05-09: gh pr view 391 → MERGED 2026-05-05.) |
vmaf-tune Phase A corpus pipeline emitted vmaf_score=NaN on every encoded clip — tools/vmaf-tune/src/vmaftune/corpus.py::run_score handed the encoder's .mp4 directly to the libvmaf CLI, which only accepts raw YUV / Y4M on --distorted. Phase A corpus rows came back with vmaf_score=NaN and exit_status=234, blocking the ADR-0237 corpus build pipeline. Backfilled to state.md by Research-0086 (2026-05-08 audit); fix shipped 2026-05-05. | PR #389 / commit 429d188e (merged 2026-05-05) | — (Phase A bug-fix; no new ADR per CLAUDE §12 r8) | run_score now decodes via a transparent ffmpeg -f rawvideo -pix_fmt <pix_fmt> step in its scratch workdir whenever the distorted suffix is outside {.yuv, .y4m}; the temp YUV is cleaned up with the workdir, ffmpeg-decode failures propagate as vmaf_score=NaN with a marker version string instead of crashing. Smoke on BigBuckBunny_25fps.yuv (1920×1080, 25fps, 150 frames): crf 23/medium = 96.30 post-fix (was NaN); crf 33/medium = 81.86 post-fix (was NaN). 16/16 unit tests in tools/vmaf-tune/tests/ pass (3 new regression tests). (verified 2026-05-09: gh pr view 389 → MERGED 2026-05-05.) |
CUDA build broken on dev hosts with gcc 16.x — every .cu failed to compile — nvcc's default C++17 host parser chokes on gcc 16's libstdc++ headers when they reach for C++20 features (char8_t, constexpr semantics in bits/utility.h); local meson setup … -Denable_cuda=true && ninja fails on every .cu with type_traits(393): error: identifier "char8_t" is undefined. Hosted CI on Ubuntu 24.04 + gcc 13 doesn't trip the bug; only affects local CUDA builds on bleeding-edge rolling distros. Backfilled to state.md by Research-0086 (2026-05-08 audit); fix shipped 2026-05-05. | PR #390 / commit 1aec4128 (merged 2026-05-05) | — (build-system fix; rebase-note 0232 documents the don't-drop-flag contract; no new ADR per CLAUDE §12 r8) | cuda_flags in core/src/meson.build now passes --std c++20 to nvcc. CUDA 13.x supports c++20 natively; CI runners aren't affected so the flag is effectively a no-op there. Local verification: meson setup core/build-cuda -Denable_cuda=true && ninja -C core/build-cuda → 386/386 green; tools/vmaf --gpumask=0 runs against Netflix golden refs. (verified 2026-05-09: gh pr view 390 → MERGED 2026-05-05.) |
cli_parse.c error() assert on long-only options with non-numeric optarg — handlers for --threads / --subsample / --cpumask passed a synthesised short-option char ('t' / 's' / 'c') into parse_unsigned(); on a bad optarg, error() walked long_opts[] for that char, found nothing (the options are long-only), and tripped assert(long_opts[n].name) -> SIGABRT instead of clean usage error. Surfaced by the libFuzzer harness landed in PR #408 (ADR-0311); reproducer was parked at core/test/fuzz/cli_parse_known_crashes/cli_threads_abbrev_assert.argv. (verified 2026-05-09: gh pr view 414 → MERGED 2026-05-06T06:58:59Z.) | PR #414 (fix/cli-parse-long-only-error-assertion, merged 2026-05-06) | ADR-0316 (follow-up to ADR-0311) | Pass the long-only ARG_* enum value into parse_unsigned() so error() finds the matching long_opts[] row via the existing < 256 branch — brings the three call-sites into parity with the seven sibling handlers (ARG_GPUMASK, ARG_FRAME_*, ARG_*_DEVICE, ARG_TINY_THREADS). New unit test core/test/test_cli_parse_long_only_args.c (POSIX fork()/waitpid(); 4 cases) confirms --threads abc / --subsample xyz / --cpumask qqq / --th=foosoxe exit(1) with Invalid argument on stderr instead of SIGABRT. Reproducer promoted from cli_parse_known_crashes/ to cli_parse_corpus/; known_assert_in_input early-reject filter removed from fuzz_cli_parse.c; 60 s fuzzer smoke clean post-fix. (verified 2026-05-09: PR #414 MERGED 2026-05-06.) |
OSSF Scorecard workflow red on every push to master — github/codeql-action/upload-sarif@b25d0ebf40e5... was an "imposter commit" (a SHA that no longer exists in the action's repository, so Scorecard's webapp returns 400 on the publish-results call). Workflow-level failure was masking a stable 6.2 / 10 aggregate score that should have been the live signal. (verified 2026-05-09: gh pr view 337 → MERGED 2026-05-04T07:29:22Z.) | PR #337 (chore/ossf-scorecard-remediation, merged 2026-05-04) | ADR-0263 + Research-0053 | Repinned to current v4 head e46ed2cbd0...; verified via gh api /repos/github/codeql-action/commits/e46ed2cb... returns 200 (was 422 for the old SHA). Scorecard policy + per-check accepted-blocker list documented in ADR-0263 §Decision; active remediation queue in Research-0053 §Action plan. (verified 2026-05-09: PR #337 MERGED 2026-05-04.) |
y4m_convert_411_422jpeg 1-byte heap-buffer-overflow on 4:1:1 streams with dst_c_w == 1 — the chroma-upsample routine's first two sub-loops unconditionally wrote _dst[(x << 1) | 1]; the third sub-loop already carried the correct (x << 1 | 1) < dst_c_w guard. Triggered by any 4:1:1 Y4M whose width (after Daala chroma decimation) reduces to a 1-pixel destination chroma row — minimal repro: YUV4MPEG2 W2 H4 F30:1 Ip C411. Surfaced by the libFuzzer harness staged in PR #348. | PR #357 / commit 05ba29a6 (merged 2026-05-04), report PR #348 (merged 2026-05-04) | — (one-line guard fix, no ADR per CLAUDE §12 r8) | core/test/test_y4m_411_oob.c exercises the W=2 H=4 4:1:1 stream end-to-end through video_input_open + video_input_fetch_frame; ASan-clean after the fix, faults at y4m_input.c:507 with WRITE of size 1 before. The 3 canonical Netflix CPU golden tests (src01_hrc00_576x324, both checkerboard_1920_1080 shifts) are unaffected — none use 4:1:1 with dst_c_w == 1. (verified 2026-05-09: PR #357 + #348 both MERGED 2026-05-04.) |
#239 — FFmpeg libvmaf_vulkan filter wall-clock serialisation (lawrence profile 2026-04-30) — synchronous fence wait inside vmaf_vulkan_import_image (ADR-0186 v1) blocked the FFmpeg decoder thread on every frame, preventing CPU/GPU overlap | PR #241 / commit e266bf8e (2026-05-02), Issue #239 closed 2026-05-03 | ADR-0251 (renumbered from 0235 in PR #310 dedup sweep) | v2 async pending-fence ring shipped; the v2 ≤ 0.7 × v1 measurement gate flipped ADR-0251 from Proposed to Accepted. Reproducer: ffmpeg -hwaccel vulkan -i ref.mkv -i dis.mkv -filter_complex '[0:v]hwupload[r];[1:v]hwupload[d];[r][d]libvmaf_vulkan' -f null - against the Netflix normal pair shows the wall-clock improvement on lavapipe + hardware. Netflix golden CPU gate unchanged (Vulkan path is host-side; goldens are CPU-only per ADR-0214 / CLAUDE §8) (verified 2026-05-09: PR #241 MERGED 2026-05-02; Issue #239 CLOSED 2026-05-03.) |
vmaf_tiny_v1.onnx external-data filename ref broken on load — ONNXRuntime fails with "External data path validation failed for initializer: 0.weight" because the v1 ONNX referenced mlp_small_final.onnx.data while only vmaf_tiny_v1.onnx.data was committed | PR #296 / commit fa81d5b4 (merged 2026-05-03) | — (artifact-only fix, no ADR) | python3 -c 'import onnxruntime; onnxruntime.InferenceSession("model/tiny/vmaf_tiny_v1.onnx")' loads cleanly; the v2-vs-v1 diff path in validate_vmaf_tiny_v2.py runs end-to-end (was erroring on v1 load before) (verified 2026-05-09: PR #296 MERGED 2026-05-03.) |
kernel_template.h 8-SSBO binding cap blocked float_adm_vulkan (9 bindings) — vmaf_vulkan_kernel_pipeline_create() returned -EINVAL at init, surfaced as "problem reading pictures" / "problem flushing context" in the cross-backend gate run | PR #288 / commit bb9d772e (merged 2026-05-02) — bundled with the float_adm_vulkan migration (T-GPU-DEDUP-22) | — (template-extension fix; ADR-0246 covers the template proper) | Cap raised 8 → 16, named VMAF_VULKAN_KERNEL_MAX_SSBO_BINDINGS constant introduced in PR #292 / commit 76d6d41e; float_adm_vulkan smoke run reports adm2 mean = 0.934515 (was failing to extract) (verified 2026-05-09: PR #288 + PR #292 both MERGED 2026-05-02 / 2026-05-03.) |
scripts/ci/deliverables-check.sh mis-stripped backslashes from heredoc-quoted PR bodies — gh pr create heredocs add escaped-backtick sequences that survive tr -d (which only strips backticks/asterisks/underscores), breaking the - [x].*ITEM regex (~18 PRs affected this session before the diagnosis landed) | PR #292 / commit 76d6d41e (merged 2026-05-03) | — (CI script hardening, no ADR) | Extended tr -d to also strip backslashes; a test fixture with literal escaped-backticks around AGENTS.md now prints OK (ticked) (verified 2026-05-09: PR #292 MERGED 2026-05-03.) |
CI workflows ran on draft PRs, burning runner-minutes — none of the 7 pull_request-triggered workflows filtered on the draft flag, silently violating single-active-CI policy whenever a subagent pushed a branch as draft | PR #300 / commit 257f1e28 (merged 2026-05-03) | — (CI-infrastructure fix, no ADR) | 33 jobs across 7 workflows now carry a draft-skip guard (if: clause that allows pull_request events only when pull_request.draft == false). The ready_for_review event re-triggers CI on un-draft; push-to-master and workflow_dispatch are unaffected (verified 2026-05-09: PR #300 MERGED 2026-05-03.) |
CLAUDE.md §12 r14 ffmpeg-patches reviewer command was wrong — for p in ffmpeg-patches/000*-*.patch; do git apply --check "$p"; done only succeeds for patch 0001 because patches 0002–0006 build on each other; correct gate is git am --3way series replay against pristine n8.1 | PR #297 / commit b161fc39 (merged 2026-05-03) | — (rule wording fix, no ADR) | 2026-05-02 /refresh-ffmpeg-patches skill run: per-patch apply --check failed on 4/6 patches; git am --3way series replay succeeded for all 6 (verified 2026-05-09: PR #297 MERGED 2026-05-03.) |
docs/state.md + CHANGELOG.md carried 15 stale ADR slug refs (slug renames where NNNN stayed but filename evolved, e.g. 0152-monotonic-index-rejection.md → 0152-vmaf-read-pictures-monotonic-index.md) | PR #304 / commit 3cbb0956 (merged 2026-05-03) | — (doc cleanup, no ADR) | mkdocs --strict build clean; spot-check verifies each rewritten ref points at the actual on-disk filename for that NNNN. 11 wrong-NNNN refs (different concept under same NNNN, e.g. 0246-gpu-kernel-template.md while disk-0221 is now changelog-adr-fragment-pattern.md) split into a separate per-ADR-review PR (#306) (verified 2026-05-09: PR #304 MERGED 2026-05-03.) |
1.07e-3 CPU vmaf_v0.6.1 score drift between /usr/local/bin/vmaf v3.0.0 and master tip — surfaced by 2026-05-02 /run-netflix-bench subagent run; well within Netflix golden's places=2 tolerance, so the gate did NOT fire, but the drift was stable + reproducible | PR #305 / commit ae1dafad (merged 2026-05-03) — bisect identifies upstream Netflix a44e5e61 (motion edge-mirror bugfix, Kyle Swanson 2026-04-17) inherited at fork root. Per-feature isolation: drift is entirely integer_motion (-1.005e-3) + integer_motion2 (-0.985e-3); ADM and VIF are bit-identical. Snapshot regen via PR #309 aligns testdata/netflix_benchmark_results.json with the fork's actual behavior. | — (bisect triage, no ADR) | /bisect-regression predicate against vmaf_v0.6.1.json brackets fork root 41301496 ↔ master 4cd3a8d8; "first bad" = fork root means drift was inherited, not introduced. Doc at docs/development/cpu-score-drift-bisect-2026-05-02.md (verified 2026-05-09: PR #305 + PR #309 both MERGED 2026-05-03.) |
T7-16 — NVIDIA-Vulkan + SYCL adm_scale2 boundary drift (2.4e-4, 1/48 frames) baseline at PR #120 (verified 2026-05-09: gh pr view 173 → MERGED 2026-04-28T19:51:45Z.) | PR #173 (empirical close, sister of T7-15, merged 2026-04-28) | — (verification-only close, no ADR) | python3 scripts/ci/cross_backend_vif_diff.py --feature adm --backend vulkan --device 0 (NVIDIA proprietary 595.58.3.0) reports adm_scale2 max_abs_diff = 1e-6 (JSON %f print floor; ULP=0) at places=4, 0/48 mismatches. Same bit-exact result on Vulkan device 1 (Arc Mesa anv 26.0.5) and SYCL device 0 (Arc A380). 2.4e-4 baseline at PR #120 is gone. No adm_vulkan.c / adm_sycl.cpp commits since PR #120 — same NVCC / driver / SYCL-runtime upgrade hypothesis as T7-15 (verified 2026-05-09: PR #173 MERGED 2026-04-28.) |
T7-15 — motion_cuda + motion_sycl 2.6e-3 SAD drift vs CPU integer_motion on 47/48 frames (surfaced by PR #120's corrected cross-backend gate) | #172 (empirical close — no motion-kernel commits between PR #120 and master; the NVCC 13.x / NVIDIA-driver upgrade since PR #120 is the most likely cause of the bit-exact restoration) | — (no ADR — verification-only close; reopened as a new T-row if the gate ever re-fails) | python3 scripts/ci/cross_backend_vif_diff.py --feature motion --backend cuda reports max_abs_diff=0.0 at places=8 over 48 frames (was 2.6e-3 47/48 mismatches). SYCL on Arc and Vulkan on Mesa anv each show 1e-6 (JSON %f print-rounding floor; ULP=0). All three backends pass the existing places=4 contract; the gate locks the contract going forward (verified 2026-05-09: PR #172 MERGED 2026-04-28.) |
FFmpeg vf_libvmaf build break against release/8.1 whenever libvmaf is built with Vulkan — libvmaf.pc exports -DVK_NO_PROTOTYPES via volk_dep's compile_args; FFmpeg's build inherits that Cflag through pkg-config, suppressing the standard Vulkan prototypes in <vulkan/vulkan.h> (including vkGetDeviceQueue), and the patch's direct call fails with implicit-declaration error against every release/8.1 + libvmaf-vulkan build. Backfilled to state.md by Research-0086 (2026-05-08 audit); fix shipped 2026-05-01. | PR #234 / commit 3130ca41 (merged 2026-05-01) | — (ffmpeg-patches build fix; no new ADR per CLAUDE §12 r8) | Patch 0006-add-libvmaf_vulkan-filter.patch now loads vkGetDeviceQueue dynamically through FFmpeg's AVVulkanDeviceContext::get_proc_addr (the same loader FFmpeg's own Vulkan filters use under VK_NO_PROTOTYPES). Vulkan headers still provide the PFN_vkGetDeviceQueue typedef even when prototypes are suppressed, so no extra include is required. The CI lane that catches this regression class is the FFmpeg-Vulkan lavapipe job added in PR #235; series replay against pristine n8.1 applies all patches clean post-fix. (verified 2026-05-09: PR #234 MERGED 2026-05-01.) |
libvmaf_vulkan.h not installed under prefix → FFmpeg --enable-libvmaf-vulkan silently drops the filter (lawrence repro 2026-04-28 19:27) | #175 (4b43ad2f, 2026-04-28) | — | meson install --destdir /tmp/x produces /tmp/x/usr/local/include/libvmaf/libvmaf_vulkan.h post-fix (was missing); FFmpeg configure --enable-libvmaf-vulkan then passes the check_pkg_config libvmaf_vulkan ... libvmaf/libvmaf_vulkan.h vmaf_vulkan_state_init_external probe and the libvmaf_vulkan filter actually builds (verified 2026-05-09: PR #175 MERGED 2026-04-28.) |
libvmaf.pc Cflags leak (build-dir -include path) on static builds — broke lawrence's BtbN FFmpeg configure 2026-04-27 22:19 (verified 2026-05-09: gh pr view 155 → MERGED 2026-04-27T21:25:36Z.) | PR #155 (73620ff5 predecessor; ADR-0200, merged 2026-04-27) | ADR-0200 | pkg-config --cflags libvmaf post-fix returns -I${includedir} -I${includedir}/libvmaf -DVK_NO_PROTOTYPES -pthread (no leaked path); rename behaviour byte-for-byte identical (0 GLOBAL vk*, 719 vmaf_priv_vk* in static libvmaf.a); shared libvmaf.so Cflags unchanged (verified 2026-05-09: PR #155 MERGED 2026-04-27.) |
volk / vk* symbol clash in BtbN-style fully-static FFmpeg builds (lawrence repro 2026-04-27) | #152 (73620ff5, 2026-04-27) | ADR-0198 | Static nm libvmaf.a reports 0 GLOBAL vk* (was ~700); BtbN-style gcc -static main.c libvmaf.a libvulkan-stub.a link succeeds; test_vulkan_smoke 10/10 pass (verified 2026-05-09: PR #152 MERGED 2026-04-27.) |
Netflix#755 — vmaf_score_pooled interleaves with vmaf_read_pictures | #91 9b983e0a (2026-04-24) | ADR-0154 | API contract test + Netflix golden gate (CPU bit-identical) (verified 2026-05-09: PR #91 MERGED 2026-04-24.) |
| Netflix#910 — out-of-order flush misses last frame | #88 f478c65d (2026-04-24) | ADR-0152 | Regression test rejects non-monotonic indices with -EINVAL (verified 2026-05-09: PR #88 MERGED 2026-04-24.) |
Netflix#1414 — float_ms_ssim broken at <176×176 | #90 7905ac78 (2026-04-24) | ADR-0153 | Init-time rejection with -EINVAL + regression test (verified 2026-05-09: PR #90 MERGED 2026-04-24.) |
Netflix#1420 — CUDA concurrency assert at cuda/common.c:166 | #93 49a64088 (2026-04-24) | ADR-0156 | 178 CHECK_CUDA sites replaced with -errno propagation; OOM reducer hits -ENOMEM (was: assert(0)) (verified 2026-05-09: PR #93 MERGED 2026-04-24.) |
| Netflix#1300 — CUDA preallocation memory leak | #94 fd1b22c2 (2026-04-24) | ADR-0157 | New vmaf_cuda_state_free() API + ASan reducer confirms 0 framework-side leaked bytes across 10 init/preallocate/fetch/close cycles (verified 2026-05-09: PR #94 MERGED 2026-04-24.) |
Netflix#1486 — motion edge-mirror + motion_max_val + motion3 output | #95 383190a4 (2026-04-24) | ADR-0158 | Doc-only verify-PR; substance already on master via earlier incremental commits (verified 2026-05-09: PR #95 MERGED 2026-04-24.) |
| Netflix#1376 — Python FIFO hang on slow IO | #85 e5a52e74 (2026-04-24) | ADR-0149 | Replaces 1-second polling with multiprocessing.Semaphore (verified 2026-05-09: PR #85 MERGED 2026-04-24.) |
| Netflix#1472 — CUDA feature extraction broken on Windows MSYS2/MinGW | #86 f9d1cae2 (2026-04-24) | ADR-0150 | Linux CPU 32/32 + CUDA 35/35 + Windows MSVC+CUDA CI build-only green (verified 2026-05-09: PR #86 MERGED 2026-04-24.) |
| Netflix#1430 — locale-unsafe parsing (comma decimal) | #74 e0e78db3 (earlier) | ADR-0137 | New thread_locale.{c,h} subsystem; round-trip parse tests (verified 2026-05-09: PR #74 MERGED 2026-04-20.) |
Netflix#1382 / #1381 — cuMemFreeAsync use-after-free on concurrent free | #72 (Batch-A) | ADR-0131 | cuMemFree port; assertion-0 crash no longer reproduces (verified 2026-05-09: PR #72 MERGED 2026-04-20.) |
Netflix#1476 — UB in void* pointer arithmetic + VIF-init memory leak | leak: #47; UB: master b0a4ac3a | — | ASan repro green before/after (verified 2026-05-09: PR #47 MERGED 2026-04-19.) |
| CUDA framesync segfault on null cubin | #62 661a8ac9; #60 d3b6fad6 | ADR-0123 / ADR-0122 | Null-guard + post-cubin-load hardening; segfault no longer reproduces (verified 2026-05-09: PR #62 + PR #60 both MERGED 2026-04-19.) |
Confirmed not-affected (or already-fixed upstream of the fork's master)¶
Netflix bugs that surfaced during triage but don't apply to the fork's code paths. Recording them here protects future sessions from re-investigating dead ends.
| Netflix issue | Status on this fork | Evidence |
|---|---|---|
T-CUDA-KERNEL-LIFECYCLE-HELPERS-CASCADE — container dev-mcp build reported missing VmafCudaKernelLifecycle / VmafCudaKernelReadback types and helper functions in integer_psnr_cuda.c | Never missing -- confirmed intact. VmafCudaKernelLifecycle, VmafCudaKernelReadback, vmaf_cuda_kernel_lifecycle_init/_close, vmaf_cuda_kernel_readback_alloc/_free, vmaf_cuda_kernel_submit_pre_launch/_post_record, and vmaf_cuda_kernel_collect_wait are all defined as static inlines in core/src/cuda/kernel_template.h (ADR-0246, PR #254/#269). The actual cascade regressions in dev-mcp PRs #1192-#1203 were: missing nv-codec-headers (#1192), wrong meson setup working directory (#1197), SYCL std::powf/std::log10f not in std namespace for icpx C++20 (#1200), SYCL duplicate motion_fps_weight field (#1199), and CUDA vif missing vif_skip_scale0 field (#1203). All resolved. | cd libvmaf && meson setup build -Denable_cuda=true && ninja -C build succeeds; integer_psnr_cuda.c at target 114/790 compiles with zero errors. gcc -fsyntax-only -DHAVE_CUDA -I src -I src/cuda -I src/feature -I ../include -I build/src src/feature/cuda/integer_psnr_cuda.c exits 0. |
Phase-A audit — DNN disabled-build -ENOSYS stubs — five return sites in core/src/dnn/dnn_api.c (lines 319, 334, 350, 362) and one in core/src/dnn/dnn_attach_api.c (line 88) flagged as "Phase-A-ish — needs clarification: intentional or real gap?" when -Denable_dnn=false. | Intentional — not a bug. The stubs are the documented disabled-build contract: public symbols are always present so callers link regardless of build configuration; -ENOSYS signals "DNN not built in" rather than a programming error. The same pattern is used by every other optional backend (CUDA, SYCL, HIP, Vulkan, Metal, MCP). vmaf_dnn_available() is the correct runtime probe. | ADR-0374 (2026-05-10). File-level stub-contract comment added to dnn_attach_api.c; dnn_api.c already carried the comment at lines 14–17. No code change required. |
DEEP_AUDIT_2026_05_18 finding 23 — integer_vif_cuda.c:180 hardcodes n_planes = 1; flagged as "CUDA VIF skips chroma planes, source of cross-backend chroma drift" | False-positive — confirmed. VIF (Sheikh & Bovik, 2006) is luma-only by definition. Every libvmaf backend (CPU AVX2/AVX-512, ARM NEON, CUDA, HIP, SYCL, Vulkan, Metal) and upstream Netflix/vmaf reads data[0] only; the CPU twin core/src/feature/integer_vif.c has no enable_chroma option and its extract() reads ref_pic->data[0] directly (lines 804–815). The CUDA twin's enable_chroma option was a vestigial parameter from an abandoned 2026-05-16 PR (#948 / #949) that never landed on master; it has been clarified as a documented no-op (warn-on-true) rather than removed (to preserve invocation-compat). docs/metrics/vif.md previously advertised integer_vif_cb/_cr features and an enable_chroma=true example that produced nothing — corrected in the same PR. | ADR-0547, upstream Netflix/vmaf@32780bd9b6:core/src/feature/cuda/integer_vif_cuda.c (confirms data[0]-only, no n_planes field), new regression test core/test/test_integer_vif_cpu_cuda_parity.c (suite fast,gpu, asserts CPU vs CUDA vif_scaleN_score agreement within 1e-4 and enable_chroma=true bit-identity with the default invocation). |
R610.43.02 driver changelog audit (PR #64 follow-up) — NVIDIA had not published changelog.html at research time; driver-level UVM and scheduler changes remained unverified | Confirmed not-affecting. Re-fetched 2026-05-28: changelog.html still 404. Content extracted from developer-forum thread ("610 release feedback & discussion"). All confirmed R610.43.02 changes are display/graphics-layer only: new Vulkan extensions (VK_EXT_shader_long_vector, VK_KHR_internally_synchronized_queues, VK_NV_push_constant_bank), DRM color pipeline API (Linux v6.19), FP16 EGL, DMABUF mmap on discrete GPUs, Xinerama removal, and regression fixes vs R580. No UVM, CUDA scheduler, power-management, MPS, or new CUDA env-var changes were found. core/src/cuda/picture_cuda.c, core/src/feature/cuda/, and cmd/vmafx-server/ are unaffected. DMABUF mmap capability is a noted future enabler for CUDA zero-copy dmabuf import but requires no code change now. | Research-0734 — sources: NVIDIA XFree86 README directory index + developer forum thread (2026-05-28). |
| Netflix#1032 — PSNR-HVS NaN on 16-bit | Already-fixed upstream b1e3f3bd is in fork master; CLI rejects bpc>12 with -EINVAL and clear error, no NaN produced | Verified by reading core/tools/cli_parse.c bpc validation + core/src/feature/psnr_hvs.c (verified 2026-05-09: cited paths still on master.) |
| Netflix#1449 — SSIM incorrect when smaller dimension > 384 px | Already-fixed upstream 7e16db0a (scale option). Fork default is auto (Wang-Bovik paper); float_ssim=scale=1 gives full-res SSIM | Verified via /cross-backend-diff on test fixtures (verified 2026-05-09: SSIM scale handling unchanged on master.) |
| Netflix#1481 — i686 (32-bit x86) build regression | Build-only matrix row exists (libvmaf-build-matrix.yml i686 cross-file with -Denable_asm=false); reproduces the regression for any future drift | ADR-0151 (verified 2026-05-09: i686 cross-file row still in libvmaf-build-matrix.yml.) |
Deferred (waiting on external trigger)¶
Bugs known to affect the fork where the fix is gated on an external event — typically Netflix merging an upstream fix that the fork preserves bit-exactness against.
| Bug | Defer rationale | Reopen trigger | Watching |
|---|---|---|---|
Netflix#955 — i4_adm_cm rounding overflow (1u << 31 overflows int32_t add_bef_shift_flt[]) | Bit-exactness against Netflix golden requires preserving the overflow until Netflix merges their own fix and updates the goldens | Netflix merges PR #1494 (feature/adm: fix integer precision issue) to master | Last checked 2026-05-09 — Netflix#1494 still state=OPEN (mergedAt=null, last upstream update 2026-04-24). Scheduled remote agent re-runs weekly until merged. ADR-0155 (verified 2026-05-09: gh pr view 1494 --repo Netflix/vmaf → OPEN | null.) |
T-CUDNN-CONV-MEMLEAK-SERVERMODE — cuDNN 9.22.0 known issue: "For certain convolution-related workloads, memory allocations are made that are not released until process termination." Affects CUDA EP inference sessions that repeatedly create/destroy convolution engines within a long-lived process. Current exposure is zero (CPU-only ORT installed; no onnxruntime-gpu in tree); would become medium-severity if a persistent inference server is added (VMAFX Phase 3 cloud-native plan). | CPU-only ORT 1.26.0 in dev/Containerfile and ai/pyproject.toml; CUDA EP only reached when user manually installs onnxruntime-gpu. No server-mode DNN path exists yet. | VMAFX Phase 3 ships a persistent HTTP/gRPC inference server using the CUDA EP. | Research-0734 |
T-NR-PROXY-CALIBRATION-RUN — NRProxyBackend.calibration_threshold defaults to NR_PROXY_DEFAULT_DELTA_FAST (8.0 VMAF units) when no model/tiny/nr_metric_v1.json sidecar exists. The design default (ADR-0615) covers >95 % of in-domain content correctly but has not been validated against the Netflix corpus. The corpus calibration sweep (ai/scripts/calibrate_nr_threshold.py) must run on the .corpus/ dataset to produce a tuned δ_fast and write the sidecar JSON. | model/tiny/nr_metric_v1.onnx does not yet exist in tree (requires python ai/scripts/export_tiny_models.py from a trained tiny-AI checkpoint); calibration sweep therefore blocked on the tiny-AI training milestone. | Tiny-AI NR metric training completes (T-TINY-AI-NR-TRAINING); calibrate_nr_threshold.py runs on .corpus/ and writes model/tiny/nr_metric_v1.json. | ADR-0624 / ADR-0615 |
HDR-VMAF-MODEL-PORT — fork ships no HDR-trained VMAF model; HDR sources fall back to SDR vmaf_v0.6.1.json weights with a one-shot warning | Path A (source from upstream / HuggingFace / academic) exhausted with negative findings 2026-05-09 — no publicly-released BSD-3-Clause-Plus-Patent-compatible libvmaf-JSON-loadable HDR VMAF model exists. Path B (train fork-owned model) blocked on (a) gated subjective HDR corpora — LIVE-HDR / LIVE-HDRvsSDR / LIVE-TMHDR / ESPL-LIVE HDR all behind manual access forms with unclear derived-weight redistribution terms — and (b) multi-day training compute that exceeds an autonomous task window. Path C (degrade + document) chosen: model/vmaf_hdr_model_card.md warns users; no fabricated weights shipped | Either (1) Netflix merges vmaf_hdr_*.json to upstream model/ (issue #645 — last authoritative reply: "no timeline"), OR (2) the fork acquires a permissively-licensed HDR-MOS-labelled corpus AND a deliberate multi-day training slot | Re-checked 2026-05-09 — gh api repos/Netflix/vmaf/contents/model returns no vmaf_hdr_* entry; CSI Magazine 2023-11-30 statement still latest public Netflix word. research-0089 / ADR-0300 |
T-HIP-FLOAT-MOMENT-PROVIDED-FEATURES-MISMATCH-2026-05-31 — float_moment_hip is the only HIP extractor without a CPU-vs-HIP parity gate after ADR-0958 round 4. Root cause: the CPU twin (vmaf_fex_float_moment, core/src/feature/float_moment.c:148) emits a single float_moment channel; the HIP twin (vmaf_fex_float_moment_hip, core/src/feature/hip/float_moment_hip.c:444) emits 4 per-stat channels (float_moment_ref1st, float_moment_dis1st, float_moment_ref2nd, float_moment_dis2nd). A parity gate against this mismatched surface has no shared LHS/RHS feature key to assert against. | Either (a) split the CPU twin into 4 per-stat extractors mirroring HIP, or (b) collapse the HIP path to a single float_moment key mirroring CPU, or (c) accept it as a permanent deferral and document the asymmetry in docs/backends/hip/overview.md. Each is non-test-shaped API work and out of scope for a test-coverage PR. | User decision on the CPU/HIP surface-shape unification path; an ADR opens the work. | ADR-0958 Alternatives table. |
T-FFMPEG-HIP-FILTER-DEFERRED — dedicated libvmaf_hip FFmpeg filter (patch 0012) not yet shipped | The libvmaf_hip.h API includes vmaf_hip_import_state but FFmpeg has no ROCm/HIP hardware-frame decoder producing AVFrames with HIP-native device pointers. There is no ffhipcodec equivalent of ffnvcodec, and no VAAPI / DMA-BUF → HIP zero-copy path through FFmpeg's hwcontext layer. Shipping a dedicated libvmaf_hip filter without a hardware-frame source would produce an unusable shell. The selector patch (0011, hip_device option, ADR-0380) is shipped and closes the CLAUDE.md §12 r14 C-API gap. | FFmpeg adds a ROCm/HIP hwdec context (analogous to AV_HWDEVICE_TYPE_CUDA + ffnvcodec) that can deliver hipDeviceptr_t-backed AVFrames | Reopen trigger: search for AV_HWDEVICE_TYPE_HIP or av_hwdevice_ctx_create(AV_HWDEVICE_TYPE_ROCM,...) in FFmpeg commits. When landed, ship patch 0012 mirroring 0006-libvmaf-add-libvmaf-vulkan-filter.patch. |
T-Y-FUNQUE-PLUS-FUSED-SVR-2026-06-14 — the y_funque_plus extractor (ADR-1114) ships the three wavelet-domain atoms only; the fused Y-FUNQUE+ MOS-predicting score (the single number a consumer would compare against VMAF) is not shipped. The official funque_plus pipeline fuses the atoms through a ScaledSVR (MinMaxScaler[-1,1] + sklearn RBF SVR). | Upstream funque_plus ships no frozen regressor — it trains a ScaledSVR per-dataset at runtime (default 100 random 80/20 splits) and never commits deployable weights. Any fused score would therefore be fork-originated, with no upstream reference to validate against. Training + freezing a fork SVR needs a licensed subjective dataset (CC-HDDO / LIVE have their own usage terms), a frozen-regressor export (support vectors / dual_coef / gamma / intercept + scaler min/max as constants or a model JSON), and a model card — materially expanding the PR's license + asset surface. Maintainer chose atoms-first for RC. | The fork acquires (or licenses) a subjective VQA dataset with redistributable derived weights AND a deliberate training slot; then export the frozen ScaledSVR + add a model card and a fused y_funque_plus feature. Opens a follow-up ADR. | ADR-1114 Consequences §"Neutral / follow-ups"; design dossier .workingdir2/rc/metrics/y-funque-plus.md §"Model assets". |
T-NIQE-HBD-HDR-MODELPATH-2026-06-14 — the NIQE extractor (niqe, ADR-1112) ships 8-bit-calibrated only. Three follow-ups deferred to a later ADR: (a) an explicit >8-bpc scaling policy — the pristine model was trained on 8-bit luma and the MSCN C=1 stabiliser is not scale-invariant, so raw 10/12/16-bit input is not guaranteed to match; (b) HDR (PQ/HLG) handling — NIQE scores raw luma with no transfer-function awareness, so the natural-scene-statistics assumptions break down; (c) an optional model_path option for a user-supplied pristine model. | Each is a deliberate scope cut from the initial CPU extractor PR: the 8-bit path is exercised by the golden gate (testdata/scores_cpu_niqe.json); >8-bpc/HDR need a calibration decision + an ADR; model_path needs an option-surface design. Current behaviour is documented in docs/metrics/niqe.md (Limitations). | A maintainer decision on the >8-bpc scaling policy (scale-to-8-bit vs raw vs reject) and/or demand for a user-supplied pristine model; opens a follow-up ADR. | ADR-1112 Consequences §"Neutral / follow-ups"; design dossier .workingdir2/rc/metrics/niqe.md Open questions. |
T-PREFILTER-LIVE-ENCODE-UNTESTED-2026-06-14 — the vmaf-tune prefilter live deband → encode → score loop (ADR-1116, workstreams D1+D2) cannot be exercised here: Pelorus and the pelorus_deband_vulkan ffmpeg filter are not installed in this environment. The adapter (-vf emission + range validation), the joint deband+CRF search-space construction, and the subcommand wiring (with a mocked encode/score loop) are unit-tested and green; the live encode path is designed against a real ffmpeg-with-Pelorus build and gated behind pelorus_filter_available(). | The live loop needs the Pelorus Vulkan deband filter compiled into the ffmpeg build; vmafx is intentionally Vulkan-free and only emits the -vf string + scores the output. Building a pelorus-enabled ffmpeg is out of scope for this control-plane-seam PR. | A pelorus-enabled ffmpeg build is available (e.g. in the dev-mcp container or a CI lane); run vmaf-tune prefilter --src ... --target-vmaf ... against it and confirm the deband+CRF recommendation matches the smoke-path behaviour. | ADR-1116 / Research-1116; pelorus ADR-0110 control-plane contract. |
Upstream-port-later batch (Research-0090, 18 commits) — 17 python/test MyTestCase migration commits + 1 cambi docs commit (38e905d1, 005988ea, 4679db83, 3e075107, e3827e4d, 25ff9f18, 3a041a97, ead2d12b, 6c097fc4, 7df50f3a, 322ca041, 74bdce1b, a3776335, 0341f730, 9fa593eb, d93495f5, 7d1ad54b, 721569bc) | All 18 commits are already covered by in-flight fork PRs that touch the exact same files. Re-porting in a separate PR would create a 100% conflict matrix against the existing branches and force a destructive rebase on the larger PRs. Per memory feedback_one_pr_at_a_time, the merge train is serialised. The original Research-0090 recommendation #6 was explicit: "Block all 18 PORT_LATER commits behind the agent-E worktree's first PR." | ALL 18 commits DONE. 17 ported via PR #497 (MERGED 2026-05-09). 25ff9f18 + 0341f730 ported in PR chore/port-upstream-drop-legacy-mytestcase-stubs (2026-06-03). 721569bc (cambi docs — cambi_high_res_speedup param + motion2 score update) verified present in fork master as of 2026-06-12 audit: docs/metrics/cambi.md line 65, docs/metrics/confidence-interval.md line 37, docs/usage/python.md line 155 all carry the upstream values. Research-0090 backlog now fully clear. | Research-0090 §"Per-commit classification — PORT_LATER" + §"Recommended action" + §"Riskiest item found"; merged PR #497 (chore/upstream-port-mytestcase-migration-v2-2026-05-08). |
| cpp23-wave-adversarial-review — adversarial review of C→C++23 conversion wave (PRs #41, #43, #44, #45, #48, #51, #54, #56, #58); 4 critical bugs, 2 high, 10 medium identified. Critical: strtof→double precision bug in dict.cpp (#48), strlen-5U underflow heap-overflow in model.cpp (#54), make_unique/C-free allocator mismatch in ref.cpp (#58), non-NUL-terminated string_view::data() → strtol in opt.cpp (#43). Review PR: chore/cpp23-wave-adversarial-review-20260528. See docs/research/cpp23-wave-adversarial-review-20260528.md. |
Update protocol¶
When a PR closes / opens / rules out a bug:
- Add or move the row in the appropriate section above.
- Cross-link the ADR (if any), the PR number + commit, and the Netflix issue (if applicable).
- For "Recently closed" entries, include enough verification detail that a future session can confirm the fix without re-running the reducer.
- For "Confirmed not-affected" rows, cite the file path + reasoning that proves the fork is not in scope.
Older "Recently closed" rows roll off after ~90 days; the audit trail then lives in git log and the closing ADR.