From 32f4471e7b8084baccc35b99f8ca04a6c325d86c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 3 Oct 2026 10:51:57 -0400 Subject: [PATCH] M4.10: before / after results in docs/performance.md, crate READMEs and ROADMAP Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/av1/README.md | 33 +++++++++++- crates/hevc/README.md | 45 ++++++++++++++-- crates/vp9/README.md | 118 +++++++++++++++++++++++++++--------------- docs/performance.md | 88 +++++++++++++++++++++++++++++++ docs/testing.md | 5 +- 6 files changed, 241 insertions(+), 49 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 96556c07..7321b3d5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -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. diff --git a/crates/av1/README.md b/crates/av1/README.md index 8843e955..5d4d5c50 100644 --- a/crates/av1/README.md +++ b/crates/av1/README.md @@ -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 diff --git a/crates/hevc/README.md b/crates/hevc/README.md index 37eba9e0..26256ff6 100644 --- a/crates/hevc/README.md +++ b/crates/hevc/README.md @@ -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 diff --git a/crates/vp9/README.md b/crates/vp9/README.md index 665b42bb..aea3a778 100644 --- a/crates/vp9/README.md +++ b/crates/vp9/README.md @@ -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::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). diff --git a/docs/performance.md b/docs/performance.md index 6a4fc465..b9f0cf28 100644 --- a/docs/performance.md +++ b/docs/performance.md @@ -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 diff --git a/docs/testing.md b/docs/testing.md index ee2f8e58..32dbb1eb 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -239,8 +239,9 @@ Options: `--sections decode,scrub`, `--only ` (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