diff --git a/CHANGELOG.md b/CHANGELOG.md index f749146..39c11f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,16 @@ All notable changes to Pelorus are documented here. The format is - **`vf_pelorus_dehalo_vulkan`** gains an opt-in `tile` option (default 0) — shared-memory tiling of the box-blur window ([ADR-0139](docs/adr/0139-dehalo-shared-mem-tile.md)). `box_blur` re-reads an overlapping 17×17 window ~5× per pixel (fetch-bound; an ALU-strip cut Arc rtime −70%), so `tile=1` cooperatively loads the window into shared memory once per plane. **Bit-identical** (SSIM 1.000000); validated **−38% (1.6×) on an Arc A380**, ~neutral on the cache-rich 4090 → default off, opt-in for weak/integrated/mobile GPUs + `tune=anime`. The ADR-0134 denoise-tiling idiom applied to its fetch-bound twin (the `aa` sobel kernel was ALU-bound and refuted). Docs: `docs/metrics/dehalo.md`. - **`vf_pelorus_aa_vulkan`** gains an opt-in `fast` option (default 0) — hoists the redundant sobel-mag into shared memory ([ADR-0140](docs/adr/0140-aa-sobel-mag-hoist.md)). aa is ALU-bound (a premise-check found stripping the sobel collapsed rtime −86%, so tiling was refuted), and `sobel_mag` is recomputed ~1156×/px across the overlapping `emask` windows; `fast=1` computes each cell's sobel **once** per workgroup into shared memory and `emask` reduces from the cache. **Bit-identical** (SSIM 1.000000, `cmp` 0 bytes both GPUs); validated **12.6× (−92%) on Arc A380, 2.6× (−62%) on the RTX 4090** — being an ALU win it helps every GPU (unlike fetch-tiling). Docs: `docs/metrics/aa.md`. - **`vf_pelorus_grain_estimate_vulkan`** now emits `lavfi.pelorus.grain_sigma` and `lavfi.pelorus.grain_flat` as per-frame metadata (the [ADR-0136](docs/adr/0136-analyze-frame-metadata.md) `av_dict_set` pattern — no interop ABI or shader change, the value was already computed). `grain_sigma` is the peak per-band RMS residual measured over edge-gated locally-flat pixels (structure excluded → what survives is grain stddev); `grain_flat` is the flat fraction it was measured over (confidence). This is the first detection enabler for the `tune=auto` content-adaptive router ([ADR-0142](docs/adr/0142-tune-auto-content-router.md)) — it unblocks routing grainy content (the proven −34% denoise lever) without parsing the `PEL_SEC_FILMGRAIN` side-data blob. Verified discriminating: heavy-grain 0.019 vs clean-ish Bluray 0.012. Docs: `docs/metrics/grain_estimate.md`. +- **Benchmark corpus gains `netflix-bar`** — Netflix's Chimera *BarScene*, donated to the + Xiph.Org derf collection. Real camera content with measurably more of what Pelorus + targets than the clean-animation `bbb` pin: **2.4x the texture, 3.1x the variance and + 1.7x the banding** at the identical 640x360/48-frame workload. Also a materially more + durable host than the single third party `bbb` now depends on. It is deliberately + documented as a *complexity* entry, not a grain one: the pinned `.webm` is a VP9 + distribution copy whose encode removed the grain (`grain_sigma` 0.0124 vs `bbb`'s + 0.0131), and the ungraded `.y4m` is 29.6 GB. `docs/metrics/grain_estimate.md` now also + documents that `grain_sigma` must be read together with `grain_flat` — a starved + estimate reads as "clean", which would invert the `tune=auto` routing decision (ADR-0142). ### Changed diff --git a/changelog.d/added/netflix-bar-corpus.md b/changelog.d/added/netflix-bar-corpus.md new file mode 100644 index 0000000..197bac8 --- /dev/null +++ b/changelog.d/added/netflix-bar-corpus.md @@ -0,0 +1,10 @@ +- **Benchmark corpus gains `netflix-bar`** — Netflix's Chimera *BarScene*, donated to the + Xiph.Org derf collection. Real camera content with measurably more of what Pelorus + targets than the clean-animation `bbb` pin: **2.4x the texture, 3.1x the variance and + 1.7x the banding** at the identical 640x360/48-frame workload. Also a materially more + durable host than the single third party `bbb` now depends on. It is deliberately + documented as a *complexity* entry, not a grain one: the pinned `.webm` is a VP9 + distribution copy whose encode removed the grain (`grain_sigma` 0.0124 vs `bbb`'s + 0.0131), and the ungraded `.y4m` is 29.6 GB. `docs/metrics/grain_estimate.md` now also + documents that `grain_sigma` must be read together with `grain_flat` — a starved + estimate reads as "clean", which would invert the `tune=auto` routing decision (ADR-0142). diff --git a/docs/development/benchmarking.md b/docs/development/benchmarking.md index e921b84..f98dcfc 100644 --- a/docs/development/benchmarking.md +++ b/docs/development/benchmarking.md @@ -1,6 +1,20 @@ # Benchmarking — proving the BD-rate win +### What each corpus entry is for + +| entry | source | what it exercises | +|---|---|---| +| `bbb` | Big Buck Bunny (360p) | clean animation — the low-impairment end. Reductive filters are expected to show ~0 here (ADR-0142) | +| `netflix-bar` | Netflix Chimera *BarScene* via Xiph.Org | real camera content: **2.4x the texture, 3.1x the variance and 1.7x the banding** of `bbb` at the same workload | +| `synth-banding` | lavfi gradient | deband torture — a smooth dark gradient with nothing else in it | + +`netflix-bar` is **not** a grain source, despite being real camera footage: the pinned +`.webm` is a VP9 distribution copy and the encode removed most of the grain (measured +`grain_sigma` 0.0124 vs `bbb`'s 0.0131 — marginally *lower*). The ungraded `.y4m` retains +grain but is 29.6 GB. For a grain axis use `run-bench.py --synth noise`, or inject seeded +noise into a real clip; both give a clean monotonic grain response. + > **The pinned corpus URL is dead (verified 2026-08-30).** `download.blender.org` > now 404s for `BigBuckBunny_640x360.m4v`, so `fetch-corpus.sh` cannot materialise > the clip on a cold machine and the harness is only runnable with a warm diff --git a/docs/metrics/grain_estimate.md b/docs/metrics/grain_estimate.md index a8ed146..e1a6be5 100644 --- a/docs/metrics/grain_estimate.md +++ b/docs/metrics/grain_estimate.md @@ -156,3 +156,38 @@ ffmpeg -init_hw_device vulkan=vk:0 -i in.mkv \ -vf "hwupload,pelorus_grain_estimate_vulkan,hwdownload,format=yuv420p,metadata=print:key=lavfi.pelorus.grain_sigma" \ -f null - ``` + +## Reading `grain_sigma` — always pair it with `grain_flat` + +The estimator accumulates only over **flat** neighbourhoods: a pixel whose 3x3 range +exceeds `edge_thr` is skipped entirely, so the sigma reflects grain rather than edge +energy. `grain_flat` is the fraction of pixels that qualified — i.e. the **coverage**, and +therefore the confidence, of the sigma estimate. + +That makes the two values only meaningful together: + +| `grain_sigma` | `grain_flat` | meaning | +|---|---|---| +| ~0 | ~1 | genuinely clean — lots of flat area, no grain found in it | +| moderate | 0.1–0.9 | normal case; sigma is well-supported | +| ~0 or erratic | ~0 | **starved, not clean** — almost nothing qualified as flat | + +A consumer must not read `grain_sigma` alone. Measured on real content (BBB 640x360) with +seeded noise injected, the response is correctly monotonic in both: + +| injected noise | `grain_sigma` | `grain_flat` | +|---|---|---| +| 0 | 0.0132 | 0.331 | +| 4 | 0.0140 | 0.294 | +| 8 | 0.0158 | 0.166 | +| 12 | 0.0172 | 0.055 | +| 20 | 0.0214 | 0.005 | + +But on a *pathologically flat* source — a uniform colour or a smooth gradient, uniformly +noised — essentially no neighbourhood stays under `edge_thr`, `grain_flat` collapses to +~0, and the sigma becomes 0 or an unstable estimate drawn from a handful of pixels. + +**This matters for the `tune=auto` router (ADR-0142)**, which uses `grain_sigma` as its +grain-detection input: keyed on sigma alone, a starved estimate reads as "clean" and would +route heavy-grain content *away* from the denoise leg — the exact case where the largest +measured BD-rate win lives. Gate on `grain_flat` before trusting a low sigma. diff --git a/scripts/bench/corpus.lock b/scripts/bench/corpus.lock index 36748cf..9c39cba 100644 --- a/scripts/bench/corpus.lock +++ b/scripts/bench/corpus.lock @@ -24,4 +24,22 @@ bbb | https://test-videos.co.uk/vids/bigbuckbunny/mp4/h264/360/Big_Buck_Bunny_36 # Deterministic synthetic torture clip for deband (max banding; no download). # Generated by fetch-corpus.sh via lavfi gradients — fully reproducible. +# netflix-bar — Netflix "Chimera" BarScene, donated to Xiph.Org's derf collection. +# Real camera content, and the point of it is CONTENT COMPLEXITY, not grain. Measured +# against the bbb pin at the same 640x360/48-frame workload (pelorus_analyze + +# pelorus_grain_estimate): +# texture 0.225 vs 0.094 (2.4x) +# variance 0.0196 vs 0.0063 (3.1x) +# banding 0.242 vs 0.142 (1.7x) +# grain_sigma 0.0124 vs 0.0131 -- NOT grainier; see below +# It is deliberately NOT pinned as a grain source: this .webm is a VP9 distribution +# copy (~6.9 Mbit/s for 4K) and the encode removed most of the grain. The ungraded +# .y4m original does retain it but is 29.6 GB, which is not pinnable. For a grain +# axis use run-bench.py's `--synth noise`, or inject seeded noise into real content -- +# both give a monotonic grain response (verified: grain_sigma 0.0132 -> 0.0214 as +# injected noise goes 0 -> 20, with grain_flat 0.331 -> 0.005). +# Host durability was the other reason to add it: media.xiph.org is a foundation-run +# archive, unlike the single third party the bbb pin now depends on. +netflix-bar | https://media.xiph.org/video/derf/webm/Netflix_BarScene_4096x2160_60fps_10bit_420.webm | 15b3a0cafbf68f48bc2e7db9785f539cb6fe3e1db5c90608df4dbcc96c69d85c | -ss 6 -t 2 | 640x360 | yuv420p | 48 | 60 + synth-banding | lavfi:gradients | - | - | 640x360 | yuv420p | 24 | 25 diff --git a/scripts/bench/fetch-corpus.sh b/scripts/bench/fetch-corpus.sh index 5373c0a..b3e2af2 100755 --- a/scripts/bench/fetch-corpus.sh +++ b/scripts/bench/fetch-corpus.sh @@ -23,7 +23,11 @@ want=("$@") want_clip() { [ ${#want[@]} -eq 0 ] && return 0; for w in "${want[@]}"; do [ "$w" = "$1" ] && return 0; done; return 1; } while IFS='|' read -r name url sha seek scale pixfmt frames fps; do - name="$(echo "$name" | xargs)"; case "$name" in ''|\#*) continue;; esac + # Skip blanks/comments BEFORE xargs: xargs treats quotes as syntax and dies on + # an apostrophe in a comment ("unmatched single quote"), which made a prose + # comment in corpus.lock able to break the whole fetch. + case "$(printf '%s' "$name" | tr -d '[:space:]')" in ''|\#*) continue;; esac + name="$(echo "$name" | xargs)" want_clip "$name" || continue url="$(echo "$url" | xargs)"; sha="$(echo "$sha" | xargs)"; seek="$(echo "$seek" | xargs)" scale="$(echo "$scale" | xargs)"; pixfmt="$(echo "$pixfmt" | xargs)"