Problem: the interpolation filters took their taps as an array of T
slices built per block row with `array::from_fn`. The array went through
the stack and was read back with wider loads than it was written with
(store-forwarding stalls): about 10 % of the 8-tap luma interpolation
time, plus the per-tap Option handling.
Fix: the filters take the taps as a function (row shifted by k, or k-th
row from x), inlined into the multiply-add loop, so the tap addresses stay
in registers. Same arithmetic, same samples.
Tests: the interpolation unit tests (16-bit lanes against the 32-bit
filter, in-place against copied windows), conformance suite, identical
YUV on the five bench streams. User instructions -1 to -4 % on all five;
cycles -2 to -5 % except g250_500 (within the run-to-run noise of the
shared machine).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: x265 (by default) and other encoders set weighted_pred_flag in the
PPS and then signal the default weights (2^denom, offset 0) for most
slices. Every block of those slices took the scalar 32-bit explicit
weighting path (8-265 / 8-266) instead of the 16-bit-lane default one, and
lost the full-sample copy shortcut.
Fix: when every list a block predicts from has the default weight and a
zero offset for a component, the block uses the default weighted
prediction (8-262 / 8-263) and, for uni-prediction at full-sample
positions, the reference copy. With log2WD = denom + shift1 the weighted
sums are exactly the default ones multiplied by 2^denom, so the samples
are identical.
Tests: a unit test checks the identity for every block width, bit depth
and denominator, and explicit weighting against 8-265 / 8-266 with extreme
weights and offsets; the weighted-prediction conformance fixtures; identical
YUV on the five bench streams. Cycles: -5 % on 1080p x265 medium, -8 % on
the weighted-prediction stream, -3 % on 10-bit, neutral elsewhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: after each picture the decoder classified every 4x4 block for
DecodeStats with an if / else-if chain: about 130 000 unpredictable
branches per 1080p picture, 1.5-2 % of single-thread decoding time.
Fix: `block_counts` sums flag comparisons in one branch-free pass
(inter = coded - intra - skip), with the same results.
Tests: a unit test compares it with the if / else-if chain over every
flag combination; identical decoded YUV on the five bench streams.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: every prediction block copied its whole reference window (block
plus filter margin, w + 7 by h + 7 luma samples) into a 16-bit scratch
buffer before interpolating, one line at a time with a row lookup per line.
After the move to 8-bit storage this copy was the largest single cost of
motion compensation (about 10 % of decoding time on 1080p streams with
much motion), and windows crossing the left or right picture edge were
copied sample by sample with a clamp each.
Fix: when the window lies inside the picture horizontally (almost always),
`Frame::window_rows` hands out the reference lines themselves (lines above
or below the picture replicated by clamping the line index, 8-228) and
the filters read the 8- or 16-bit samples directly: horizontal taps are the
line shifted by k, vertical taps the k-th line. Only windows crossing the
left or right edge are still copied, now as edge fill + run copy + edge
fill. The arithmetic is unchanged, so the prediction is identical.
Tests: in-place interpolation equals interpolation of the copied window
for random fractional vectors, block sizes, both chroma planes and both
sample types (2000 blocks each, inside and across every edge); edge
replicated lines equal the per-sample clamp for every offset; conformance
suite and identical YUV on the five bench streams at 1 and 3 threads.
User instructions on 1080p: -9 % (g250_500), -2 to -6 % elsewhere.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: every picture was stored with 16-bit samples whatever its bit depth.
For 8-bit streams (almost all Main content) that doubled the memory traffic
of the hottest paths: motion-compensation reference windows and full-sample
copies, deblocking and SAO, the per-picture planes and published rows, and
the output copy, which also had to narrow every sample to u8.
Fix: a `Sample` trait (u8, u16) and `Frame` rows stored in the picture's
type (`Rows::U8` when luma and chroma are 8-bit, `Rows::U16` otherwise).
`PicState`, `SliceDecoder`, the interpolation / weighting stores, intra
prediction and the in-loop filters are generic over the sample type and
monomorphised once per type; the decoder picks the type per picture from
its SPS. All arithmetic stays in the same integer types as before, so the
output is unchanged sample for sample. 8-bit output rows are now copied as
they are. Reading a frame with the other sample type (never done: the DPB
is flushed when the bit depth changes) yields nothing instead of a panic,
and the non-threaded build no longer has an `expect` on unpublished rows.
Tests: new unit tests for the storage type, exact crops of both types and
harmless wrong-type access; the full-sample copy test now runs on u8 and
u16 frames; the ffmpeg conformance suite (with and without the threads
feature); identical decoded YUV on five 1080p/720p streams (8-bit, 10-bit,
weighted prediction) at 1 and 3 threads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem: the software HEVC decoder was 2.7-4x slower per core than ffmpeg
on 1080p Main. Motion compensation was about half of the profile: the
weighted-prediction stores clipped in emulated 32-bit SSE2 lanes, every
block line of a reference window was a libc memmove call, and full-sample
uni-prediction went through window copy, shift and store. Deblocking
divided per edge to find the slice of each block.
Fix (output unchanged, sample for sample):
- put_uni / put_bi in 16-bit lanes (prediction samples are 14-bit; bi uses
the exact halved-sum identity), dispatched on constant block widths.
- 8-bit first interpolation passes accumulate in 16-bit lanes (the
filtered value stays within -24*255..88*255).
- Full-sample uni-prediction without weighting copies the reference
samples straight into the picture (Frame::copy_block), and block lines
are copied as fixed-size arrays instead of memmove calls.
- Deblocking takes the slices from edge_params instead of dividing per
edge, returns bS 0 for identical motion in one slice, and filters luma
edges on a gathered 4x8 block.
- 8-bit output planes are copied to u8 directly.
Tests: equivalence against the 32-bit formulas and the previous filter
(new unit tests), the ffmpeg conformance suite, and identical decoded YUV
on five 1080p/720p streams (8-bit, 10-bit, weighted prediction).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HEVC through VA-API the same way as H.264: the software HEVC decoder's
parsers and DPB (now generic over the picture type in filmcraft-hevc:
POC, reference picture sets, reference lists, output order, RASL
handling) drive the stateless hardware. Main, Main 10 and Main Still
Picture, 4:2:0 8/10-bit (P010 read-back); scaling lists, tiles, WPP,
dependent slices, weighted prediction. Missing references and range
extensions go to the software decoder.
Tests: hardware parity on HEVC Main / Main 10 incl. open GOP, scaling
lists, slices+WPP, weighted prediction, tskip+AMP, 1080p/2160p (streamed
comparison to stay within memory); HEVC front end against a recording
stand-in on every OS; ABI checks extended to va_dec_hevc.h. Benchmark on
Iris Xe: 4K HEVC 203 -> 10 ms CPU per frame, 4K Main 10 212 -> 18 ms.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- SAO runs and 8-tap filter rows: fixed-size lanes through `get()` + `first_chunk(_mut)`
instead of slice + `try_into().expect()`.
- Decoder: a slice segment with no started picture is an error; the output queue pop no longer
expects.
- Weighted prediction without a parsed table falls back to the default (unweighted) prediction
instead of `expect`.
- Tile boundaries accumulate without `last().unwrap()`.
- `#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented,
clippy::todo, clippy::unreachable)]` outside tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Interpolation keeps its reference windows in reusable scratch (no 2 x 10 KB zeroing per block)
and filters with taps outer / outputs inner over fixed block widths; SAO edge and band offsets
select the offset per lane instead of a table lookup; uni / bi output clips with min / max; runs of
bypass bins decode with one division; context-coded bins are branch-free. Draft mode skips
deblocking and SAO of sub-layer non-reference pictures of the top sub-layer. Bit-exact with 1, 3
and all threads; draft output differs only on flagged pictures.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Frames are published one superblock row at a time once loop filtered; a post stage per frame
(own thread) assembles bands from the tile strips, filters them in raster order and builds the
output picture, while the next frame's tile columns decode and wait per band for the reference
rows they read. Bit-exact with 1, 3 and all threads. Draft mode skips the loop filter of frames
that refresh no reference slot. av1dec / hevcdec bench knobs and VP9 / AV1 4K playback scenarios
in the bench.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- HEVC inverse transform: sum basis rows of non-zero coefficients with a
const-generic size (bit-exact, tested against the direct product);
SAO edge offset gets a check-free interior loop. HEVC CPU/frame -60%.
- H.264/HEVC/VP9 planes are moved into VideoFrames, not copied.
- Catch-up decoding: frame jobs tell sources which frames are late
(filmcraft_media::cancel::with_catch_up); the GOP cache skips
non-reference samples of late frames (H.264 nal_ref_idc 0, HEVC
sub-layer non-reference pictures). Scrubs keep the last 2 s decoded.
- perf.stats: engine query + UI/control method (decode counters,
playback shown/dropped, frame-worker decode/render ms, cache use).
- Timeline waveform source peak cached; bench seek section and --only.
- docs/performance.md with before/after results.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Runs each crate's ignored generate_fixtures test (h264, hevc, isobmff,
matroska, prores) - the same generators the oracle tests use - and
prints made/cached/skipped per fixture plus a summary. Optional crate
filter: cargo xtask fixtures hevc prores.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New dev-dependency-only crate crates/testkit (L0, no FilmCraft deps):
FILMCRAFT_FFMPEG/FILMCRAFT_FFPROBE, then PATH (incl. .exe), then known
dirs; SKIPPED lines and FILMCRAFT_REQUIRE_ORACLES=1 to make skips fail;
require_ffmpeg!/require_ffprobe!/require_oracles!; fixtures_dir() under
<workspace>/target/fixtures with per-thread temp names and atomic
generation. Oracle helpers in aac, opus, h264, hevc, h264enc, isobmff,
matroska, prores and export migrated (codecs left on its local lookup).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A small CABAC encoder in src/synth_tests.rs writes streams with coding tools
libx265 never emits; outputs are checked exactly (unfiltered PCM) and against
ffmpeg. README documents status, fixture matrix, performance and gaps.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>