feat(core): percentile pooling and Windows UTF-8 path contract (ADR-1181/1182) - #1311
feat(core): percentile pooling and Windows UTF-8 path contract (ADR-1181/1182)#1311lusoris wants to merge 7 commits into
Conversation
4152a77 to
4fe3e05
Compare
Duplicate resolution: the percentile-pooling half of this PR is superseded by #1340This PR is not being closed — its Part B (Windows UTF-8 path contract, ADR-1182, But Part A (percentile pooling, ADR-1181) duplicates #1340 and loses on the evidence. 1. Heap buffer overflow on the fork's own documented idiom. const unsigned capacity = (index_high - index_low + 1);
double *scores = (double *)malloc(capacity * sizeof(double));
2. Silent output-schema widening. This PR raises 3. Tests. This PR ships 2 tests: a 5-value math unit test and one end-to-end test 4. Smaller points. #1340 namespaces the shared helpers ( 5. Duplicate ledger row. Both PRs move Requested action on this PRDe-scope it to Part B only: drop commit I did not perform that de-scope myself: it requires removing files under One thing this PR has that #1340 does notThe Go binding surface in |
4fe3e05 to
f4215ae
Compare
An earlier keep-both rebase resolution left T-UPSTREAM-818-POOLING-ENUM-NO-PERCENTILES-2026-09-03 duplicated, and the follow-up dedup commit dropped the wrong copy: it kept master's Open row and deleted this branch's Recently-closed row. Rebasing onto current master then removed the Open row as well (master still carried it, this branch closes it), leaving the id with no row at all — an ADR-0165 violation that scripts/ci/check-state-md-rows.sh cannot see, because it only rejects duplicates. Restores the Recently-closed row, corrects its algorithm description (the implementation sorts with qsort and interpolates linearly between neighbouring ranks, matching NumPy's default percentile; it is not quickselect), and moves both closed rows from the branch name to `PR #1311` in the provenance column, matching the convention the other 2026-09-06 rows use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Queue audit (read-only verification pass, 2026-09-06): Part A of this PR duplicates PR #1340. Both add the same four enumerators to
Differences worth weighing: #1340 is percentile-only and ships Rust-binding and FFmpeg-patch coverage ( Both PRs are otherwise clean: no conflict against current master, six-deliverable gate passes locally, and all new C files measure 0 clang-tidy findings. |
An earlier keep-both rebase resolution left T-UPSTREAM-818-POOLING-ENUM-NO-PERCENTILES-2026-09-03 duplicated, and the follow-up dedup commit dropped the wrong copy: it kept master's Open row and deleted this branch's Recently-closed row. Rebasing onto current master then removed the Open row as well (master still carried it, this branch closes it), leaving the id with no row at all — an ADR-0165 violation that scripts/ci/check-state-md-rows.sh cannot see, because it only rejects duplicates. Restores the Recently-closed row, corrects its algorithm description (the implementation sorts with qsort and interpolates linearly between neighbouring ranks, matching NumPy's default percentile; it is not quickselect), and moves both closed rows from the branch name to `PR #1311` in the provenance column, matching the convention the other 2026-09-06 rows use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
9ba5beb to
dbec3f8
Compare
An earlier keep-both rebase resolution left T-UPSTREAM-818-POOLING-ENUM-NO-PERCENTILES-2026-09-03 duplicated, and the follow-up dedup commit dropped the wrong copy: it kept master's Open row and deleted this branch's Recently-closed row. Rebasing onto current master then removed the Open row as well (master still carried it, this branch closes it), leaving the id with no row at all — an ADR-0165 violation that scripts/ci/check-state-md-rows.sh cannot see, because it only rejects duplicates. Restores the Recently-closed row, corrects its algorithm description (the implementation sorts with qsort and interpolates linearly between neighbouring ranks, matching NumPy's default percentile; it is not quickselect), and moves both closed rows from the branch name to `PR #1311` in the provenance column, matching the convention the other 2026-09-06 rows use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
dbec3f8 to
a46c49b
Compare
An earlier keep-both rebase resolution left T-UPSTREAM-818-POOLING-ENUM-NO-PERCENTILES-2026-09-03 duplicated, and the follow-up dedup commit dropped the wrong copy: it kept master's Open row and deleted this branch's Recently-closed row. Rebasing onto current master then removed the Open row as well (master still carried it, this branch closes it), leaving the id with no row at all — an ADR-0165 violation that scripts/ci/check-state-md-rows.sh cannot see, because it only rejects duplicates. Restores the Recently-closed row, corrects its algorithm description (the implementation sorts with qsort and interpolates linearly between neighbouring ranks, matching NumPy's default percentile; it is not quickselect), and moves both closed rows from the branch name to `PR #1311` in the provenance column, matching the convention the other 2026-09-06 rows use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2fb5e36 to
7f716ae
Compare
…rk introduced ADR-0141 / ADR-1142 require every file a PR touches to stay at or below its committed clang-tidy baseline. The percentile-pooling and UTF-8-path commits added five findings to `core/src/libvmaf.c` (which is at zero on master) and introduced three new translation units carrying findings of their own. - `libvmaf.c`: split the ADR-1181 percentile path out of `vmaf_feature_score_pooled()` into `pool_percentile()` plus two small helpers, which clears `readability-function-size` (92 lines / 21 branches) and the three `readability-braces-around-statements` findings from the `perc` if/else chain. Dropped the four percentile `case` labels in `pool_reduce()` that returned the same `-EINVAL` as `default:` (`bugprone-branch-clone`); the explanation moved onto `default:`. - `compat/path_utf8.c`, `test/test_path_utf8.c`, `test/test_pooling_percentile.c`: ADR-1138 `NOLINTBEGIN(modernize-use-nullptr)` block with the standing citation, matching every other C translation unit in the tree. - `test_path_utf8.c`: split the three oversized test bodies into write / read-back / error-class helpers so none exceeds the 15-branch budget, moved the Windows wide-API cross-check behind `assert_wide_path_exists()`, closed the write and read handles before asserting on the transfer result, and cited ADR-1143 on the single-threaded `getenv` test setup. - `test_pooling_percentile.c`: split the end-to-end body into `feed_reference_pair()` / `check_percentile_oracles()` / `check_central_oracles()` / `check_pooled_error_paths()`, which also closes the `clang-analyzer-unix.Stream` leak (both YUV handles were left open when an assertion between `fopen` and `fclose` returned) and the `readability-isolate-declaration` finding. No oracle value changed. Also documents the output-schema half of ADR-1181 in `docs/usage/cli.md`: every `<metric>` row and JSON object now carries `median`, `perc5`, `perc10` and `perc20`, with the measured values for the documented 576x324 pair. Verified: `meson test -C build-prep --suite=fast` 116/116, and `clang-tidy -p build-prep` reports zero findings on `path_utf8.c`, `libvmaf.c`, `test_path_utf8.c` and `test_pooling_percentile.c`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An earlier keep-both rebase resolution left T-UPSTREAM-818-POOLING-ENUM-NO-PERCENTILES-2026-09-03 duplicated, and the follow-up dedup commit dropped the wrong copy: it kept master's Open row and deleted this branch's Recently-closed row. Rebasing onto current master then removed the Open row as well (master still carried it, this branch closes it), leaving the id with no row at all — an ADR-0165 violation that scripts/ci/check-state-md-rows.sh cannot see, because it only rejects duplicates. Restores the Recently-closed row, corrects its algorithm description (the implementation sorts with qsort and interpolates linearly between neighbouring ranks, matching NumPy's default percentile; it is not quickselect), and moves both closed rows from the branch name to `PR #1311` in the provenance column, matching the convention the other 2026-09-06 rows use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
My rebase dedup kept the wrong side. Its rule -- when a bug id appears twice after a keep-both rebase, keep the row matching origin/master -- is right when the branch merely restates a row master already carries, and WRONG when the branch is the PR that closes the bug. Then the branch's past-tense Recently-closed row is the correct survivor and master's present-tense Open row is the stale one. Restored the closure row and left a tombstone where the Open row was, matching the convention used elsewhere in this file. Exactly one row per bug id; scripts/ci/check-state-md-rows.sh reports OK. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
7f716ae to
969ee45
Compare
… absent
test_pooling_percentile is registered in the `fast` suite and opens the
Netflix 576x324 golden pair through a hard mu_assert. Those YUVs are
deliberately untracked -- .gitignore keeps them out of the tree because of
their size and scripts/test/fetch-test-yuvs.sh fetches them -- and only the
golden-harness job restores them from cache. Every other job that runs the C
unit tests therefore had no fixture to open: Sanitizers (address, thread,
undefined), Sanitizers ASan+UBSan, Coverage Gate and Ubuntu ARM clang all
failed on this one test.
Probe for the pair first and set mu_skipped when it is missing, so the
harness exits 77 and meson reports "skipped". "The fixture is not here" and
"the pooling maths is wrong" are different facts and must not share an exit
code.
Second defect, which is what turned the failure into a confusing one: every
early return in test_pooling_percentile_yuv leaked the model and the context.
Under LeakSanitizer that converted a single failed assertion into 96912 bytes
of LSan output in 33 allocations, whose stacks pointed at vmaf_model_load and
feature_collector_mount_model rather than at the missing file. The checks now
funnel through one exit that always calls vmaf_close and
vmaf_model_destroy.
Verified under -Db_sanitize=address,undefined, both paths:
fixtures absent -> "[skip: Netflix golden YUVs not fetched]", exit 77,
no leaks
fixtures present -> 2 tests run, 2 passed, no leaks
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pull request was converted to draft
|
Drafted — two blockers, neither of them the fixture issue I just fixed in 1. Functional regression (blocking). 2. Pre-existing lint debt in touched files (scope decision).
CLAUDE.md rule 12 (ADR-0141 / ADR-1142) requires a touched file to be left lint-clean, so the gate is behaving correctly — but discharging it means ~61 mechanical Fixed and pushed meanwhile: |
Pull request was converted to draft
| const size_t payload_len = strlen(payload); | ||
| assert(payload_len > 0); | ||
|
|
||
| msg = fopen_utf8_write(filepath, payload, payload_len); |
| const size_t payload_len = strlen(payload); | ||
| assert(payload_len > 0); | ||
|
|
||
| msg = open_utf8_write(filepath, payload, payload_len); |
An earlier keep-both rebase resolution left T-UPSTREAM-818-POOLING-ENUM-NO-PERCENTILES-2026-09-03 duplicated, and the follow-up dedup commit dropped the wrong copy: it kept master's Open row and deleted this branch's Recently-closed row. Rebasing onto current master then removed the Open row as well (master still carried it, this branch closes it), leaving the id with no row at all — an ADR-0165 violation that scripts/ci/check-state-md-rows.sh cannot see, because it only rejects duplicates. Restores the Recently-closed row, corrects its algorithm description (the implementation sorts with qsort and interpolates linearly between neighbouring ranks, matching NumPy's default percentile; it is not quickselect), and moves both closed rows from the branch name to `PR #1311` in the provenance column, matching the convention the other 2026-09-06 rows use. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ADR-1182's UTF-8 path contract and ADR-1181's percentile pooling arrived as one PR (#1311). They are independent, and bundling them dragged model_loader.c, vmaf_roi.c and vmaf_per_shot.c into the PR's touched-file set purely to route their fopen calls through the shim. Those three files carry ~71 pre-existing clang-tidy findings between them, which rule 12 then makes this PR's problem. Keep the pooling half here; the shim goes out separately. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Superseded by the split you approved — closing so the ADR-1181/1182 numbers stop colliding with their successors. Why this PR is closed rather than rebased: it bundled two independent changes, ADR-1181 (percentile pooling) and ADR-1182 (Windows UTF-8 path contract). Bundling them dragged Successors:
Both carry the work verbatim — #1392 by file split, the UTF-8 branch by cherry-picking What the split changed on the way through:
Reopen this if the split turns out wrong — but the two halves are independent and the ADR numbers can only live in one open PR each. |
Summary
Two additive public-API / compatibility features that close long-standing upstream issues Netflix#818 and Netflix#1568.
Part A — percentile pooling (ADR-1181). Adds
VMAF_POOL_METHOD_MEDIAN,VMAF_POOL_METHOD_PERC5,VMAF_POOL_METHOD_PERC10andVMAF_POOL_METHOD_PERC20toenum VmafPoolingMethod. The streaming accumulators inpool_reduce()cannot express a percentile, sovmaf_feature_score_pooled()routes the percentile family topool_percentile(), which materialises the (subsample-filtered) per-frame scores, sorts them and interpolates linearly between neighbouring ranks — the same convention NumPy'spercentileuses by default, which is what the Python harness compares against.core/src/pooling_percentile.hholds the interpolation itself. XML and JSON reports emit every registered pooling method, so every<metric>row and JSON object gains four attributes; FFmpeg filter patches 0005 / 0006 / 0013 and the Go bindings inpkg/libvmaf/follow the enum.Part B — Windows UTF-8 path contract (ADR-1182).
core/src/compat/path_utf8.{h,c}addsvmaf_fopen_utf8andvmaf_open_utf8. On Windows they convert the UTF-8 path (and mode) to UTF-16 and call_wfopen/_wopen, mappingERROR_NO_UNICODE_TRANSLATIONtoEILSEQandERROR_INSUFFICIENT_BUFFERtoENAMETOOLONG; on POSIX they delegate straight tofopen/open, so the byte-sequence behaviour is unchanged. 17 narrowfopen/_opencall sites acrosslibvmaf.c, the model loaders and the fork-added tools move onto the shims, which makes UTF-8 the documented contract for every path the library and CLI accept.Follow-up already tracked:
T-WINDOWS-CLI-WMAIN-ARGV-UTF8-2026-09-05— the Windows CLI still receives narrow ACPargvfrom the OS runtime, which is a separatewmainchange.Type
feat— percentile pooling methods and Windows UTF-8 path contract shimChecklist
pre-commit run --filesgreen on every touched file;clang-tidy -p buildreports zero findings oncore/src/compat/path_utf8.c,core/src/libvmaf.c,core/test/test_path_utf8.candcore/test/test_pooling_percentile.c(the ADR-0141 / ADR-1142 requirement for the files this PR adds or moves off their baseline).core/test/test_pooling_percentile.candcore/test/test_path_utf8.c.docs/api/index.md(theVmafPoolingMethodtable and the UTF-8 path contract section) anddocs/usage/cli.md(the pooled-metrics output-schema change).core/src/compat/path_utf8.{h,c}andcore/src/pooling_percentile.h; no SIMD/GPU twin changes; additive, non-breaking API; ADR-1181 and ADR-1182.Bug-status hygiene (ADR-0165)
docs/state.md— movedT-UPSTREAM-818-POOLING-ENUM-NO-PERCENTILES-2026-09-03andT-UPSTREAM-1568-WINDOWS-NARROW-PATH-API-2026-09-03to Recently closed; added follow-upT-WINDOWS-CLI-WMAIN-ARGV-UTF8-2026-09-05to Open.Netflix golden-data gate (ADR-0024)
assertAlmostEqual(...)score in the Netflix golden Python tests.Deep-dive deliverables (ADR-0108)
docs/adr/1181-percentile-pooling-methods.md§ Alternatives considered anddocs/adr/1182-windows-utf8-path-contract.md§ Alternatives consideredAGENTS.mdinvariant note —core/src/AGENTS.mdchangelog.d/added/pooling-percentile-methods.md,changelog.d/fixed/windows-utf8-paths.mddocs/rebase-notes.mdentryReproducer
🤖 Generated with Claude Code