M4.10: before / after results in docs/performance.md, crate READMEs and ROADMAP
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a0735c98f6
commit
32f4471e7b
|
|
@ -81,6 +81,7 @@ Status: ✅ done · 🟡 in progress · ⬜ not started. Estimates are remaining
|
|||
|
||||
- **2026-10-03 (later):** graphics & captions (M10.7): `.fcgt` graphics templates (export/install/apply/edit, 8 original built-ins; Adobe .mogrt refused), Essential Graphics Browse/Edit, rolls/crawls, responsive design pins and time, per-character styles, Upgrade Caption to Graphic / to Source Graphic, MCC/EBU STL/TTML/DFXP, Replace Fonts; schema v12. 1479 tests.
|
||||
- **2026-10-03:** 4K H.264 playback (M4.9): vectorised deblocking, branch-free CABAC bins and column-parallel inverse transforms cut H.264 decoding by 23 % (cycles per frame, bit-exact; 4K cold seeks −27 % CPU); opt-in draft decoding (Settings ▸ Playback) skips deblocking of non-reference pictures and uploads decimated planes while playing at 1/2–1/4, never for paused frames or exports; conformance runs 1, 3 and all threads. 4K plays 192/0 at load 100–140 (before: only at load ~90).
|
||||
- **2026-10-03:** 4K VP9 / HEVC / AV1 playback (M4.10): VP9 frame threading (references published per superblock row, loop filter overlapping the next frames) plus a vectorised loop filter, column-parallel transforms and 16-bit MC cut VP9 work by 25 % single-threaded and 44 % with all cores (4K decode 8 → 27–34 fps at load ~200); HEVC −28 % (vectorised SAO, MC without per-block zeroing, batched bypass bins), AV1 −20 % (fixed-width MC, recycled planes); opt-in draft decoding for all three; bit-exact at 1, 3 and all threads. 4K VP9 and HEVC now play 192/0 at load ~200 (before: 0–58 of 192 shown).
|
||||
|
||||
- **2026-10-03:** keyboard parity (M3.12: 143 new commands on Premiere's default keys, table validated against the registry), audio (M7.8: 5.1 tracks/submixes/Mix, 5.1 panner, BS.775 downmix, 5.1 WAV/MOV/AAC export, voice-over record with punch-in, Remix, Clip Mixer Latch/Touch/Write). 1432 tests.
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,8 @@ and `b10-23-film_grain-50` are **bit-exact** in the default run. The ignored
|
|||
with scaled inter-layer prediction), `24-monochrome` (8 and 10-bit) and
|
||||
`16-intra_only-intrabc-extreme-dv` (1080p intra block copy), `23-film_grain-50` (8 and 10-bit).
|
||||
|
||||
`tests/oracle_inter.rs` encodes SVT-AV1 GOPs (hierarchical references, compound prediction,
|
||||
`tests/oracle_inter.rs` also checks draft mode: every unflagged frame stays bit-exact with
|
||||
libdav1d and only flagged frames differ, with 1 and 4 threads. `tests/oracle_inter.rs` encodes SVT-AV1 GOPs (hierarchical references, compound prediction,
|
||||
OBMC / warped motion, motion-field projection, all loop filters; presets 3–8, 8- and 10-bit, odd
|
||||
sizes), plus super-resolution (denominator 12) and film-grain streams, and compares every frame
|
||||
with libdav1d: **bit-exact** on all 6 fixtures.
|
||||
|
|
@ -91,6 +92,36 @@ noisy; single-threaded figures use thread CPU time and are comparable.
|
|||
| `perf_1080p_gop_hq` (crf 18) | 19.5 fps | 19.4 fps | 2.4 fps | 3.6 fps |
|
||||
| `perf_1080p_gop_tiles` (4x2 tiles) | 44.4 fps | 44.7 fps | 2.7 fps | 16.3 fps |
|
||||
|
||||
### M4.10 (2026-10-03)
|
||||
|
||||
CPU cycles per frame counted by the kernel (`/usr/bin/time -l`, all threads summed; mean of two
|
||||
interleaved base / after rounds at load average 190-320) with
|
||||
`AV1_THREADS=n cargo run --release -p filmcraft-av1 --example av1dec -- in.ivf` on the
|
||||
`cargo xtask bench` decode clips (SVT-AV1 preset 10, CRF 35, keyint 48, testsrc2 + grain),
|
||||
bit-exact with libdav1d:
|
||||
|
||||
| stream | threads | Mcycles / frame before | after | after, draft mode |
|
||||
|---|---|---|---|---|
|
||||
| 1080p (120 frames) | 1 | 60.5 | **48.3** (−20 %) | 46.2 |
|
||||
| 1080p | 14 | 63.6 | **51.9** (−18 %) | 50.3 |
|
||||
| 2160p (72 frames) | 1 | 212.5 | **172.4** (−19 %) | 167.9 |
|
||||
| 2160p | 14 | 220.0 | **177.1** (−20 %) | 171.1 |
|
||||
|
||||
What changed: the sub-pixel filter passes run over fixed block widths (taps outer, columns
|
||||
inner); frame and tile-region planes come from a bounded pool of recycled buffers (zeroed on
|
||||
reuse) instead of a fresh `calloc` and `munmap` per frame (allocation, page faults and zeroing
|
||||
were ~10 % of the time); the palette mode-info arrays (40 bytes per 4x4 unit) are only
|
||||
allocated when screen content tools are allowed; CDF adaptation is branch-free (a branch-free
|
||||
symbol search was tried and was slower: most symbols have 2-4 values). Single-threaded 4K now
|
||||
splits into coefficient parsing ~20 %, inter prediction ~18 %, other block-level parsing
|
||||
~12 %, deblocking ~11 % (still one edge at a time), inverse transforms ~12 %, CDEF ~5 %.
|
||||
|
||||
Draft mode (`Decoder::set_draft(true)`, `AV1_DRAFT=1`; reduced-resolution playback only): shown
|
||||
frames whose `refresh_frame_flags` is 0 — no reference slot keeps them, so neither their samples,
|
||||
motion vectors nor CDFs reach another frame — skip deblocking, CDEF and loop restoration
|
||||
(super-resolution still runs, it sets the output size) and are flagged `Picture::draft`. In
|
||||
SVT-AV1's hierarchical GOPs that is the top layer, half of the frames.
|
||||
|
||||
## API
|
||||
|
||||
```rust
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ and fixture generators.
|
|||
| Scaling (flat and scaling lists), DCT 4..32, DST 4x4, transform skip, sign data hiding | done |
|
||||
| Deblocking (bS, slice / tile boundary rules, PCM / bypass exemption), SAO (band / edge, boundary rules) | done |
|
||||
| Frame-level multithreading (CTB-row granularity dependencies) | done |
|
||||
| Draft mode (non-reference pictures skip deblocking / SAO; reduced-resolution playback only) | done |
|
||||
| Tiles / WPP | parsed and decoded sequentially within a picture (parallelism is across pictures) |
|
||||
| 4:2:2 / 4:4:4 / monochrome, range-extension tools (RExt), 12-bit+ with extended precision | not yet (`Error::Unsupported`) |
|
||||
| SCC, multilayer (SHVC / MV-HEVC), 3D extensions | not supported (layers > 0 are ignored) |
|
||||
|
|
@ -48,6 +49,7 @@ for (pts, access_unit) in access_units {
|
|||
}
|
||||
for pic in dec.flush() { /* ... */ }
|
||||
if let Some(err) = dec.take_error() { /* error reported by a decoding thread */ }
|
||||
// dec.set_draft(true): draft mode (playback at reduced resolution only)
|
||||
```
|
||||
|
||||
`decode` expects whole access units (a buffer with several complete access units — e.g. an entire
|
||||
|
|
@ -65,6 +67,13 @@ into the shared `Frame` (`OnceLock` per CTB row). Motion compensation of later p
|
|||
row on exactly the reference rows it reads. Samples are stored as `u16` internally for all bit
|
||||
depths.
|
||||
|
||||
Draft mode (`Decoder::set_draft(true)`; FilmCraft enables it with Settings ▸ Playback ▸ Draft
|
||||
decoding while the Program monitor plays at 1/2 or 1/4): sub-layer non-reference pictures
|
||||
(TRAIL_N, RASL_N, ...) of the highest temporal sub-layer — no later picture predicts from them,
|
||||
and TMVP reads only reference pictures — skip deblocking and SAO and are flagged
|
||||
`Picture::draft`; every other picture is unchanged. libx265's non-reference B pictures are such
|
||||
pictures.
|
||||
|
||||
Modules: `params` (VPS/SPS/PPS/tile layout), `slice` (NAL + slice header), `dpb` (POC, RPS, lists,
|
||||
bumping), `cabac`, `slicedec` (CTU syntax + reconstruction), `mvpred` (merge / AMVP / TMVP),
|
||||
`intra`, `inter`, `transform`, `filter` (deblocking, SAO, row publication).
|
||||
|
|
@ -85,10 +94,13 @@ with ffmpeg; tests print a message and skip when ffmpeg is absent).
|
|||
ffmpeg.
|
||||
- Robustness: hvcC input with pts round trip; randomly corrupted, truncated and garbage input never
|
||||
panics or deadlocks (single- and multi-threaded).
|
||||
- Conformance (`tests/conformance.rs`): every fixture is decoded single-threaded and frame-threaded
|
||||
and compared sample-exactly with `ffmpeg -f rawvideo -pix_fmt yuv420p / yuv420p10le`; on mismatch
|
||||
- Conformance (`tests/conformance.rs`): every fixture is decoded with 1 thread, 3 threads and every
|
||||
core and compared sample-exactly with `ffmpeg -f rawvideo -pix_fmt yuv420p / yuv420p10le`; on mismatch
|
||||
the first differing frame, plane, sample, CTB and 8x8 block are reported. Output pts/POC order is
|
||||
checked too. `compare_detects_mismatch` guards the harness itself.
|
||||
checked too, and that no picture is flagged draft without draft mode. `compare_detects_mismatch`
|
||||
guards the harness itself. Draft mode (`draft_mode_changes_only_flagged_non_reference_pictures`):
|
||||
on B-pyramid / weighted fixtures, single- and frame-threaded, every unflagged picture is
|
||||
bit-exact and only flagged pictures differ.
|
||||
|
||||
### Fixture matrix (all bit-exact, single- and multi-threaded)
|
||||
|
||||
|
|
@ -135,7 +147,7 @@ average 50-100), so these are lower bounds:
|
|||
| 1080p Main, x265 medium CRF 24, 60 frames | ~75 fps | ~225 fps |
|
||||
| 2160p Main 10, x265 fast CRF 26, 20 frames | ~20 fps | ~95 fps |
|
||||
|
||||
No SIMD-specific code yet; interpolation, transforms and loop filters are plain loops. M4.8
|
||||
No intrinsics: the hot loops are written to auto-vectorise. M4.8
|
||||
profiling (`cargo xtask bench`, macOS `sample`) found the inverse transform at over half of the
|
||||
decoder's CPU time: it evaluated the full matrix product with strided table reads. It now sums
|
||||
contiguous basis rows scaled by the non-zero coefficients only, with the block size a compile-time
|
||||
|
|
@ -144,7 +156,30 @@ against the direct product on random sparse and dense blocks). SAO edge offset h
|
|||
loop for samples whose neighbours are inside the CTB. Together: ~60 % less CPU per frame
|
||||
(1080p 78 → 33 ms, 2160p 320 → 129 ms of CPU per frame through the media stack, alternating A/B), see
|
||||
[docs/performance.md](../../docs/performance.md).
|
||||
`cargo run --release -p filmcraft-hevc --example hevcdec -- in.hevc out.yuv` decodes a file.
|
||||
`cargo run --release -p filmcraft-hevc --example hevcdec -- in.hevc out.yuv` decodes a file
|
||||
(`HEVC_THREADS=n`, `HEVC_LOOPS=k`, `HEVC_DRAFT=1`).
|
||||
|
||||
M4.10 (2026-10-03), CPU cycles per frame counted by the kernel (`/usr/bin/time -l`, all threads
|
||||
summed; mean of two interleaved base / after rounds at load average 190-320) on the
|
||||
`cargo xtask bench` decode clips (x265 preset fast, CRF 22, testsrc2 + grain), bit-exact:
|
||||
|
||||
| stream | threads | Mcycles / frame before | after | after, draft mode |
|
||||
|---|---|---|---|---|
|
||||
| 1080p (120 frames) | 1 | 90.9 | **67.3** (−26 %) | 61.2 |
|
||||
| 1080p | 14 | 100.2 | **75.1** (−25 %) | 67.3 |
|
||||
| 2160p (72 frames) | 1 | 362.6 | **262.3** (−28 %) | 232.5 |
|
||||
| 2160p | 14 | 398.3 | **284.8** (−28 %) | 260.1 |
|
||||
|
||||
What changed: motion compensation zeroed two 10 KB windows per prediction block per component
|
||||
and list (11 % of the time in `memset`) and filtered with dynamic-width scalar sums; it now keeps
|
||||
reusable scratch windows and runs taps outer / outputs inner over fixed block widths. SAO
|
||||
looked its offset up per sample in a table (20 % of the time); the edge / band offset is now
|
||||
selected per lane from the neighbour-sign sum / band index, so the runs vectorise. Uni / bi
|
||||
output clips with min / max instead of `clamp` (whose bound assert kept it scalar). Runs of
|
||||
bypass bins (signs, Rice suffixes, SAO / intra fields) decode with one division, and
|
||||
context-coded bins are branch-free. Single-threaded 4K now splits into CABAC residual parsing
|
||||
~28 %, reference window copies ~11 %, interpolation ~11 %, deblocking + SAO + row publication
|
||||
~15 %, inverse transform ~8 %.
|
||||
|
||||
## Known gaps
|
||||
|
||||
|
|
|
|||
|
|
@ -27,8 +27,8 @@ is used only as an external fixture generator and test oracle.
|
|||
| DCT / ADST 4..32, WHT (lossless) | done |
|
||||
| Tiles (columns and rows); tile columns decode in parallel | done |
|
||||
| Hidden frames, show_existing_frame, intra-only frames, resolution changes, error resilient / frame parallel modes | done |
|
||||
| Loop filter in parallel (superblock wavefront) | done |
|
||||
| Frame-level threading (overlapping consecutive frames) | not yet |
|
||||
| Frame threading: references published per superblock row, the loop filter / output of a frame overlaps the next frames | done |
|
||||
| Draft mode (non-reference frames skip the loop filter; reduced-resolution playback only) | done |
|
||||
|
||||
## API
|
||||
|
||||
|
|
@ -45,7 +45,8 @@ for (pts, chunk) in chunks { // one IVF / WebM block / MP4 sample each (frame o
|
|||
// pic.color { color_space (0..7 as coded), full_range }
|
||||
}
|
||||
}
|
||||
let _ = dec.flush(); // VP9 has no reordering: always empty, kept for API symmetry
|
||||
let rest = dec.flush(); // pictures still in flight (frame threads)
|
||||
// dec.set_draft(true): draft mode (playback at reduced resolution only), dec.threads()
|
||||
dec.reset(); // forget references / contexts (seek), keeps the thread pool
|
||||
let stats = dec.stats(); // what the stream exercised (frame kinds, tiles, compound, scaling...)
|
||||
|
||||
|
|
@ -60,33 +61,60 @@ let info: Option<filmcraft_vp9::KeyframeInfo> = filmcraft_vp9::keyframe_info(chu
|
|||
(e.g. an MP4 without `stss`) still seek correctly.
|
||||
|
||||
A superframe produces every shown frame it contains (normally one); hidden frames produce none.
|
||||
With frame threads (`Decoder::new()`, or `with_threads(n)` with n > 1) a picture can come out of
|
||||
a later `decode` call or `flush` (each carries the `pts` of the chunk that showed it);
|
||||
`with_threads(1)` returns every picture from the call that completes it.
|
||||
|
||||
### Architecture
|
||||
|
||||
The calling thread parses the uncompressed and compressed headers and manages the eight reference
|
||||
slots, the four probability contexts, segmentation map and previous-frame motion vectors. Each tile
|
||||
column is decoded by one `TileDecoder` (rayon pool) into a private strip of samples and mode info
|
||||
(tile rows of a column are decoded in order — they share above contexts); strips are then
|
||||
assembled, the loop filter runs as a superblock wavefront (superblock (r, c) waits for (r-1, c+1);
|
||||
each superblock filters a private copy of its area plus 8 samples above / left and writes it
|
||||
back), counts are merged for backward adaptation and the frame is stored. Samples are `u16`
|
||||
internally for every bit depth.
|
||||
(tile rows of a column are decoded in order: they share above contexts). Counts are then merged
|
||||
for backward adaptation, the mode info is assembled (the next frame reads it for motion vector
|
||||
prediction and segment ids) and the frame goes into the reference slots at once, while its
|
||||
**post stage** runs on a thread of its own: it assembles each superblock row ("band") from the
|
||||
strips, loop filters it superblock by superblock in raster order (so the result is the
|
||||
specification's), publishes the band above it once that can no longer change (`OnceLock` per
|
||||
band; the 16-wide filter of row r reads 8 and changes 7 rows of row r - 1, which travel in 8
|
||||
padding rows on top of each band) and copies it into the output picture. Motion compensation of
|
||||
later frames reads reference rows through `Frame::row` / `Frame::span`, which wait for exactly
|
||||
the bands a block reaches, so the next frames' tile columns decode while earlier frames are still
|
||||
being filtered. Probability adaptation needs only the counts, so even backward-adaptive streams
|
||||
start the next frame before the loop filter is done; frame-parallel / error-resilient streams
|
||||
(no backward adaptation) never wait at all. Results never depend on scheduling: the conformance
|
||||
suite decodes every fixture with 1, 3 and all threads. Samples are `u16` internally for every bit
|
||||
depth.
|
||||
|
||||
Draft mode (`Decoder::set_draft(true)`; FilmCraft enables it with Settings ▸ Playback ▸ Draft
|
||||
decoding while the Program monitor plays at 1/2 or 1/4): frames with `refresh_frame_flags` 0 —
|
||||
no reference slot receives them, so nothing predicts from their samples — skip the loop filter
|
||||
and are flagged `Picture::draft`. The loop filter changes neither mode info, segmentation map nor
|
||||
probabilities, so every other frame is unchanged. Such frames occur in temporally layered streams
|
||||
(e.g. libvpx `-ts-parameters`); single-layer realtime streams refresh LAST on every frame.
|
||||
|
||||
Modules: `header` (6.2 / 6.3, superframes), `boolcoder` (9.2), `probs` (frame contexts, counts,
|
||||
8.4), `tile` (partition / mode info / MV prediction), `tile::recon` (residual, tokens, intra /
|
||||
inter prediction calls, reconstruction), `intra`, `inter`, `transform`, `loopfilter`, `decoder`.
|
||||
inter prediction calls, reconstruction), `intra`, `inter`, `transform`, `loopfilter`, `frame`
|
||||
(banded frames, pools), `decoder` (scheduling, post stage).
|
||||
|
||||
## Tests
|
||||
|
||||
`cargo test --release -p filmcraft-vp9`. Fixtures are generated on first use into
|
||||
`target/fixtures/vp9/` with ffmpeg + libvpx-vp9 (tests print a message and skip when ffmpeg or
|
||||
libvpx is unavailable). Every frame is compared sample-exactly with `ffmpeg -c:v vp9 -f rawvideo`
|
||||
output, single-threaded and with the default thread pool; the first mismatch reports frame, plane,
|
||||
position and superblock.
|
||||
output with 1 thread, 3 threads (frame threading with a small pool) and every core; the first
|
||||
mismatch reports frame, plane, position and superblock. No picture may be flagged draft without
|
||||
draft mode.
|
||||
|
||||
- Unit tests: boolean decoder against a literal model of 9.2, tables / trees, inverse DCT / ADST
|
||||
against floating-point bases, DC-only shortcut vs. full transform, scaled MC path vs. unscaled,
|
||||
superframe index, `inv_remap_prob`, probability merging.
|
||||
against floating-point bases, DC-only shortcut vs. full transform, the column-parallel 2D
|
||||
transform against the per-column formulation on random blocks of every size and type, scaled
|
||||
MC path vs. unscaled (through a banded reference frame), superframe index, `inv_remap_prob`,
|
||||
probability merging.
|
||||
- Draft mode (`draft_mode_changes_only_flagged_non_reference_frames`): on a two-layer temporal
|
||||
scalability stream, single- and frame-threaded, every unflagged frame is bit-exact and only
|
||||
flagged frames differ.
|
||||
- Conformance (`tests/conformance.rs`): the fixture matrix below plus a stream concatenating two
|
||||
sizes. `compare_detects_mismatch` guards the harness. `coverage_report` (ignored) prints the
|
||||
decoder statistics for each fixture.
|
||||
|
|
@ -126,47 +154,55 @@ Backward adaptation is on (`-frame-parallel 0`) unless noted.
|
|||
| fade_intra_heavy | 352x288 | fade-in, key frame every 8 |
|
||||
| hd_1080p | 1920x1080 | realtime speed 5 |
|
||||
| resize_concatenated | 352x288 then 347x251 | size change at a key frame |
|
||||
| temporal_layers | 352x288 | realtime, 2 temporal layers (`-ts-parameters`): non-reference frames |
|
||||
|
||||
## Performance
|
||||
|
||||
`cargo test --release -p filmcraft-vp9 --test perf -- --ignored --nocapture` (bit-exactness is
|
||||
verified first), or `VP9_THREADS=n VP9_LOOPS=k cargo run --release -p filmcraft-vp9 --example
|
||||
vp9dec -- in.ivf` (decodes the file k times and prints the statistics).
|
||||
vp9dec -- in.ivf` (decodes the file k times and prints the statistics; `VP9_DRAFT=1` for draft
|
||||
mode).
|
||||
|
||||
Apple M4 Pro (14 cores), 1080p 8-bit `bench_1080p` fixture (libvpx speed 4, 4 MB/s, 4 tile
|
||||
columns), 120 frames per run, before / after binaries alternated. The machine was shared with
|
||||
other builds (load average 80-110 during the multi-thread runs, up to 200 earlier), so
|
||||
wall-clock numbers are the best of 3 runs and still well below what an idle machine gives;
|
||||
single-thread CPU time is the stable figure.
|
||||
M4.10 (2026-10-03), Apple M4 Pro (14 cores) shared with other builds (load average 190-320), so
|
||||
the figures are CPU cycles per frame counted by the kernel (`/usr/bin/time -l`, all threads
|
||||
summed; mean of two interleaved base / after rounds) — wall clock only within a round. Fixtures:
|
||||
the `cargo xtask bench` decode clips (testsrc2 + grain, libvpx realtime speed 8, CRF 32; 1080p
|
||||
4 tile columns, 2160p 8 tile columns, frame-parallel mode), bit-exact with ffmpeg.
|
||||
|
||||
| | before (5e9d7d9) | now |
|
||||
|---|---|---|
|
||||
| 1 thread, CPU time | 52-55 fps | 98-102 fps |
|
||||
| 14 threads, wall clock (best of 3, load ~90) | 82-89 fps | 143-145 fps |
|
||||
| stream | threads | Mcycles / frame before | after | wall ms / frame before → after (same load) |
|
||||
|---|---|---|---|---|
|
||||
| 1080p (120 frames) | 1 | 68.0 | **52.0** (−24 %) | |
|
||||
| 1080p | 14 | 100.7 | **57.9** (−43 %) | 95, 82 → 18, 17 |
|
||||
| 2160p (72 frames) | 1 | 273.6 | **205.8** (−25 %) | |
|
||||
| 2160p | 14 | 391.7 | **219.2** (−44 %) | 146, 118 → 38, 36 |
|
||||
|
||||
What made the difference:
|
||||
Before M4.10 the frame-level work was serial (tile columns of one frame, then a loop-filter
|
||||
wavefront, then assembly and output conversion), so 14 threads spent 40-50 % more cycles than one
|
||||
thread (spinning between short parallel phases) and a 4K frame still took ~120 ms of wall time
|
||||
at this load; now the post stage overlaps the next frames and 4K decodes at ~27 fps on the
|
||||
loaded machine. What changed:
|
||||
|
||||
- Loop filter: each group of 8 samples along an edge is filtered as 8 lanes at once (16-bit lanes
|
||||
for 8 / 10-bit, 32-bit for 12-bit); the mask / flat / flat2 decisions are lane-wise and every
|
||||
filter result is selected per lane, so the arithmetic vectorizes. Per-group decisions use masks
|
||||
instead of divisions and skip groups with no edge before looking up levels. The parallel loop
|
||||
filter (superblock wavefront) locks per superblock-row band only while copying, instead of
|
||||
holding one frame-wide lock.
|
||||
- Motion compensation: the 8-tap filters run tap-outer over fixed-width rows (4..64) and no longer
|
||||
zero a 10 KB edge window per block; the 2D inverse transform buffers are sized per transform.
|
||||
- Memory: strip / frame planes and mode-info grids are recycled through a pool (no per-frame
|
||||
multi-megabyte allocations and page faults); assembling tile strips into the frame and the
|
||||
cropping / 8-bit narrowing of output pictures run in parallel chunks of rows.
|
||||
- **Frame threading** (see Architecture).
|
||||
- **Loop filter** (was ~50 % of single-threaded time): the lane masks were `bool`s with
|
||||
`clamp` and branchy selects, which kept the "vectorised" group filter scalar; masks are now
|
||||
0 / -1 integers with bitwise selects and min / max clipping, so it compiles to NEON.
|
||||
- **Inverse transforms**: the column pass runs the generated butterflies
|
||||
(`transform_gen::cols4` / `cols8`) on 4 / 8 columns at once.
|
||||
- **Motion compensation**: 8-bit sub-pixel filters accumulate in 16-bit lanes (8 per vector
|
||||
instead of 4): every sum of the VP9 filters plus rounding lies in [-13706, 46474], so the sum
|
||||
modulo 2^16 identifies it (negative sums clip to 0).
|
||||
- Bool decoder: branch-free update.
|
||||
|
||||
The loop filter is still the largest single cost (~30% single-threaded), followed by inter
|
||||
prediction and coefficient decoding.
|
||||
Single-threaded 4K time now splits into coefficient tokens (bool decoding) ~27 %, loop filter
|
||||
~25 % (two thirds of it the vertical-edge transposes and per-group decisions), motion
|
||||
compensation ~15 %, inverse transforms ~8 %, intra prediction ~6 %.
|
||||
|
||||
## Known gaps
|
||||
|
||||
- No frame-level threading: parallelism is tile columns + loop filter wavefront + row-parallel
|
||||
copies, so streams with a single tile column (narrower than 512 pixels or encoded without tile
|
||||
columns) decode mostly on one thread, and 1080p (at most 4 tile columns) uses 4 threads for
|
||||
coefficient decoding and prediction.
|
||||
- A frame's tile decoding is still a barrier: the next frame starts once every tile column of
|
||||
the current one is done (its post stage then runs alongside), so streams with a single tile
|
||||
column decode their tiles on one thread at a time; the post stage (loop filter) of one frame
|
||||
runs on one thread.
|
||||
- Scaled references combined with compound prediction are not covered by a fixture (both are
|
||||
verified separately; libvpx through ffmpeg cannot change size mid-stream and the synthetic
|
||||
splice cannot keep the compound streams' probability contexts intact).
|
||||
|
|
|
|||
|
|
@ -13,6 +13,94 @@ the before/after comparison relies on. Each milestone below alternated base / af
|
|||
each); the most load-independent figure is the decoder's own cycle count (`proc_pid_rusage`
|
||||
instructions and cycles of a process, all threads summed).
|
||||
|
||||
## Results (M4.10: 4K VP9, AV1 and HEVC, before → after)
|
||||
|
||||
Baseline = commit `a814ff6` (decoders unchanged since M4.8); after = M4.10. Interleaved runs on
|
||||
2026-10-03 at load average 170-320 (two base and two after rounds; the second bench round of
|
||||
each overlapped another bench run, so its wall-clock columns are extra pessimistic).
|
||||
|
||||
### Decoder work per frame (load-independent)
|
||||
|
||||
Codec examples (`vp9dec`, `av1dec`, `hevcdec`) on the `cargo xtask bench` decode clips, CPU
|
||||
cycles counted by the kernel (`/usr/bin/time -l`, all threads summed), mean of the two rounds;
|
||||
output bit-exact with ffmpeg / libdav1d at every thread count:
|
||||
|
||||
| stream | 1 thread before → after | 14 threads before → after | wall ms / frame, 14 threads, before → after |
|
||||
|---|---|---|---|
|
||||
| VP9 1080p | 68.0 → **52.0** (−24 %) | 100.7 → **57.9** (−43 %) | 95, 82 → 18, 17 |
|
||||
| VP9 2160p | 273.6 → **205.8** (−25 %) | 391.7 → **219.2** (−44 %) | 146, 118 → 38, 36 |
|
||||
| HEVC 1080p | 90.9 → **67.3** (−26 %) | 100.2 → **75.1** (−25 %) | 17, 10 → 5, 13 |
|
||||
| HEVC 2160p | 362.6 → **262.3** (−28 %) | 398.3 → **284.8** (−28 %) | 24, 55 → 26, 25 |
|
||||
| AV1 1080p | 60.5 → **48.3** (−20 %) | 63.6 → **51.9** (−18 %) | 26, 8 → 8, 7 |
|
||||
| AV1 2160p | 212.5 → **172.4** (−19 %) | 220.0 → **177.1** (−20 %) | 158, 39 → 20, 32 |
|
||||
|
||||
Mcycles per frame. Draft mode (1 thread): HEVC 2160p 232.5 (−11 % on top), AV1 2160p 167.9
|
||||
(−3 %: SVT-AV1's top-layer frames filter little); the VP9 clips have no non-reference frames.
|
||||
|
||||
### Decode (every frame through the media stack)
|
||||
|
||||
| codec | size | CPU ms/frame before (2 rounds) | after (2 rounds) | fps before → after |
|
||||
|---|---|---|---|---|
|
||||
| VP9 | 1080p | 33.6, 30.8 | **23.5, 21.7** | 14, 17 → 67, 56 |
|
||||
| VP9 | 2160p | 120.2, 117.7 | **68.4, 67.9** | 9.1, 7.8 → **34, 27** |
|
||||
| HEVC | 1080p | 31.2, 29.3 | **23.9, 22.4** | 50, 58 → 138, 145 |
|
||||
| HEVC | 2160p | 117.9, 111.8 | **86.9, 82.1** | 13, 14 → **30, 41** |
|
||||
| AV1 | 1080p | 18.5, 18.9 | **15.1, 15.7** | 53, 44 → 122, 69 |
|
||||
| AV1 | 2160p | 64.8, 66.7 | **48.4, 51.3** | 16, 8.9 → **25, 25** |
|
||||
|
||||
### Program-monitor playback, 2160p (8 s, GPU path; new `vp9-2160` / `av1-2160` scenarios)
|
||||
|
||||
| case | before: shown/dropped (4 runs) | after (4 runs) | CPU ms/frame before → after |
|
||||
|---|---|---|---|
|
||||
| VP9 Full | 6/186, 53/139, 0/192, 0/192 | **192/0, 192/0, 189/3, 192/0** | 23-49 → 75-83 |
|
||||
| VP9 1/2 | 6/186, 58/134, 2/190, 52/140 | **192/0, 191/1, 192/0, 192/0** | 34-70 → 78-80 |
|
||||
| HEVC Full | 6/186, 1/191, 0/192, 9/183 | **192/0, 192/0, 192/0, 192/0** | 74-124 → 91-97 |
|
||||
| HEVC 1/2 | 0/192, 9/183, 0/192, 19/173 | **192/0, 192/0, 192/0, 185/7** | 64-118 → 94-97 |
|
||||
| HEVC 1/2 draft | 5/187, 88/104, 97/95, 138/54 | **188/4, 192/0, 187/5, 189/3** | 135-148 → 96-104 |
|
||||
| AV1 Full | 4/188, 19/173, 0/192, 36/156 | 52/140, 0/192, **178/14**, 0/192 | 31-47 → 38-56 |
|
||||
| AV1 1/2 | 32/160, 44/148, 1/191, 38/154 | 2/190, **150/42, 184/8**, 50/142 | 30-50 → 41-72 |
|
||||
| AV1 1/2 draft | 1/191, 24/168, 1/191, 27/165 | **192/0, 187/5, 184/8, 192/0** | 25-51 → 68-70 |
|
||||
|
||||
CPU per *displayed* frame rises where the decoder now keeps up: before, most frames were never
|
||||
decoded (dropped jobs cost nothing). At load ~180-220, 4K VP9 and HEVC now play in real time at
|
||||
Full and 1/2; AV1 does at 1/2 with draft decoding and sometimes at 1/2 / Full: its frames
|
||||
decode concurrently only once their references are complete (whole-frame dependencies) and the
|
||||
clip has a single tile, so a 4K frame waits for its references' deblocking, CDEF and output
|
||||
copy.
|
||||
|
||||
## What M4.10 changed
|
||||
|
||||
1. **VP9 frame threading** (the decoder had none: tile columns, then a loop-filter wavefront,
|
||||
then assembly and output, each a barrier — 14 threads used 40-50 % more cycles than one and
|
||||
barely ran faster). Frames are now published one superblock row at a time once loop
|
||||
filtered; each frame's post stage (band assembly from the tile strips, loop filter in raster
|
||||
order, publication, output picture) runs on its own thread while the next frames' tile
|
||||
columns decode, their motion compensation waiting per band for exactly the reference rows it
|
||||
reads. Backward-adaptive streams only wait for the previous frame's symbol counts.
|
||||
2. **VP9 kernels**: the loop filter's lane masks were `bool`s with `clamp` and branchy selects,
|
||||
which kept it scalar; 0 / -1 integer masks, bitwise selects and min / max make it NEON. The
|
||||
inverse transform's column pass runs the generated butterflies on 4 / 8 columns at once;
|
||||
8-bit sub-pixel filters accumulate in 16-bit lanes (every VP9 filter sum fits modulo 2^16).
|
||||
3. **HEVC**: motion compensation zeroed two 10 KB windows per block (11 % of the time in
|
||||
`memset`) and filtered with dynamic-width scalar sums; it now reuses scratch windows and
|
||||
filters taps-outer over fixed widths. SAO (20 % of the time) selects edge / band offsets per
|
||||
lane instead of a table lookup per sample. Runs of bypass bins decode with one division;
|
||||
output clipping uses min / max. Frame threading with per-CTB-row waits was already in place.
|
||||
4. **AV1**: fixed-width sub-pixel filter passes; frame and tile-region planes recycled through a
|
||||
bounded pool instead of a fresh `calloc` / `munmap` per frame (~10 % of the time); palette
|
||||
mode-info arrays (40 bytes per 4x4) only with screen content tools; branch-free CDF update.
|
||||
5. **Draft mode** for all three through `VideoDecoder::set_draft` (Settings ▸ Playback ▸ Draft
|
||||
decoding, reduced-resolution playback only): VP9 frames that refresh no reference slot skip
|
||||
the loop filter; HEVC sub-layer non-reference pictures of the top sub-layer skip deblocking
|
||||
and SAO; AV1 shown frames that refresh no slot skip deblocking, CDEF and loop restoration.
|
||||
Tested per codec (unflagged pictures bit-exact, single- and multi-threaded) and through the
|
||||
media stack (`crates/codecs/tests/draft_decoders.rs`: draft frames reach draft requests only;
|
||||
an export after draft playback is bit-exact with ffmpeg).
|
||||
|
||||
Not done: AV1 row-level frame threading (references are waited for as whole frames) and its
|
||||
per-edge deblocking; VP9 and HEVC coefficient / CABAC parsing are now the largest single costs
|
||||
(~27 %); HEVC reference windows are still copied per block (~11 %).
|
||||
|
||||
## Results (M4.9: 4K H.264, before → after)
|
||||
|
||||
Baseline = commit `3d14099` (M3.12, unchanged decoder); after = M4.9. Interleaved runs on
|
||||
|
|
|
|||
|
|
@ -239,8 +239,9 @@ Options: `--sections decode,scrub`, `--only <substring>` (fixture or scenario),
|
|||
`--section NAME --json FILE` (one section in-process, e.g. under a profiler). Fixtures are made
|
||||
with ffmpeg in `target/fixtures/playback/` (or `$FILMCRAFT_FIXTURES/playback`). The suite's
|
||||
`playback` section plays `h264-1080` and `stack3` at Full, `h264-2160` at Full, 1/2 and 1/4 and with
|
||||
draft decoding (Settings ▸ Playback ▸ Draft decoding) at 1/2 and 1/4, and `hevc-2160` at Full and
|
||||
1/2; its `draft` column counts the frames decoded in draft mode.
|
||||
draft decoding (Settings ▸ Playback ▸ Draft decoding) at 1/2 and 1/4, and `hevc-2160`, `vp9-2160`
|
||||
and `av1-2160` at Full, 1/2 and 1/2 draft; its `draft` column counts the frames decoded in draft
|
||||
mode.
|
||||
|
||||
The perf tests check bit-exactness before they time anything. Results go in the crate README's
|
||||
performance table, with machine and thread count. Headline numbers go in
|
||||
|
|
|
|||
Loading…
Reference in New Issue