Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
10 changes: 10 additions & 0 deletions changelog.d/added/netflix-bar-corpus.md
Original file line number Diff line number Diff line change
@@ -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).
14 changes: 14 additions & 0 deletions docs/development/benchmarking.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
<!-- markdownlint-disable MD013 -->
# 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
Expand Down
35 changes: 35 additions & 0 deletions docs/metrics/grain_estimate.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
18 changes: 18 additions & 0 deletions scripts/bench/corpus.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 5 additions & 1 deletion scripts/bench/fetch-corpus.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down
Loading