`read_ue()? + K` wrapped (or panicked with overflow checks) for values near
u32::MAX, so tile counts, picture size, bit depth, num_ref_idx and similar
fields could slip past their range checks. Every occurrence in the h264/hevc
parameter sets and slice headers now uses `BitReader::read_ue_plus`, the
remaining `a + read_ue()?` / `as i32 + 1` sums saturate or are range-checked
first, and the crop arithmetic (SPS validation and `crop_rect`) is checked or
saturating.
Tested with SPS/PPS builders that write u32::MAX per field and assert Err
(they panicked with "attempt to add with overflow" before), plus a
huge-conformance-window `crop_rect` test.
Fixes#2780
Move the header byte/bit flips, the big-field writes and the random rounds of the
dnx, h264 and hevc hostile-stream sweeps into helper functions. The RNG seeds and
draw order, the labels and the depth gating are unchanged.
* chore: resolve SonarCloud maintainability issues
* fix: repair CI on the Sonar maintainability refactor
- h264: the coefficient scratch is i32, so the method path is <[i32]>::first_chunk_mut
- hevc: Px::w32 takes self, so keep the closure over the &P from slice::get
- tts: upsamples is a method, not an importable item
- vp8: drop the unused B_DC_PRED/DC_PRED imports; cargo fmt the import lists
- ui-egui: editing no longer needs to be mutable
- packaging: keep literal printf formats (shellcheck SC2059)
* fix: restore the mutable binding in hot_number
Prediction is dispatched to a const-size kernel. Angular modes compute
whole rows with the fact == 0 and horizontal checks hoisted out of the
sample loop: horizontal modes predict into a u16 tile that is then
transposed, and non-negative angles read the reference row directly
instead of copying it into a zero-initialised i32 array.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The second stage summed a full basis row per column for every output
row (O(N^3) for dense blocks). It now uses the even/odd partial
butterfly on 16-bit intermediates and skips all-zero columns; the
sparse first stage is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The in-place reference window is now an array of exactly its h + T - 1
rows (dispatch on the fixed prediction block heights), and the copied
window and the first-pass rows are read with their stride instead of
through slice arrays. interpolate takes a row accessor, so it still has
one instance per sample type and filter length.
Fixes#3169
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Work out once per CTB which of the 8 neighbouring CTBs SAO may use and run
the first/last lines and edge columns through sao_edge_run instead of the
per-sample path with per-sample neighbour checks. Samples of transquant
bypass / PCM-without-loop-filter CUs get their deblocked values back after
the offset, and that scan only runs when the PPS or SPS allows such CUs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Replace the per-call morton bit loop with a 256-entry z-order table and
an early same-CTB path in zavail. intra_predict now computes the left and
top reference availability as unit masks (one CTB check or a z-order
prefix per CTB segment) and copies the available samples run by run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep unique frame identities while lazily caching one gray backing for
the active geometry and plane depths. Invalidate it on format changes
and release it on flush without invalidating readers.
Fixes#2797.
Co-authored-by: Codex <noreply@openai.com>
* Tests: one integration-test binary per crate
Each crate with several tests/*.rs files keeps them as modules of tests/it/,
so cargo links one test executable per crate instead of one per file. Files
that change process-wide state or time a workload stay binaries of their own.
* Profiles: line tables for workspace crates, no debug info for dependencies
* Windows ARM64: nightly and on release pushes instead of every push to main
* xtask ci: clippy and tests in the dev profile
The release profile's thin LTO and single codegen unit made linking the test
binaries take most of the gate. The codec and audio crates stay at opt-level 3
in the dev profile, so their tests run at release speed.
* xtask ci: nextest, doctests, no examples, one wasm check
Tests run with cargo nextest when it is installed (cargo test otherwise) and
build with --lib --bins --tests, so examples aren't linked; doctests run
separately. The wasm check keeps going past the first failing crate. VP8
fixture generation locks across processes, since nextest runs each test in
its own.
* VP9 synthetic fixtures: write aside and rename
A test in another process could read a fixture ffmpeg was still writing.
* Fuzz tests: a third of each sweep by default, all of it with FILMCRAFT_FUZZ_ITERS
Every seed, mutation kind and thread count still runs. Header bytes each get
one overwrite value and two bit flips in rotation, so every value and bit
index still occurs across the region. FILMCRAFT_FUZZ_ITERS=N runs the whole
sweep plus N random rounds per seed, as before.
* Platform fallback tests: one failure point per hybrid path
The failure-anywhere test injected a failure at four points around every
restart point; the hybrid's paths differ only in the first GOP, around the
second and last restart points, at the end and never. The replay test needs
the first two GOPs.
* Export tests: shorter ranges with the same checks
The bitrate test measures 0.5–1.5 s, where each preset reaches its own
target (on 0–3 s the simple demo left two presets at the same rate). The
every-preset test exports three frames per preset.
* Audio tests: effects in parallel, a table-driven FFT for the Opus quality metric
The extreme-settings test keeps both settings and the 40 s tail for every
effect, one thread each. The Opus quality metric's FFT builds its twiddles
once and allocates nothing per transform (same algorithm and summation order).
* CI: dev-profile gate with nextest
xtask ci no longer builds with --release, so the release-profile overrides
go; nextest is installed for the test step.
* CI: apt packages through the cached install-apt-packages action
* Review fixes: fuzz tests with a panic hook in their own binaries, loud fixture lock
The fuzz tests of av1, h264, hevc, mpeg2v, vp8 and vp9 count panics through
a process-wide hook, so a panic in another test of the same process would
fail them; they are binaries of their own again. The VP8 fixture lock fails
the test instead of skipping it silently, and the full-sweep doc comments say
how many truncation points the full run samples.
* Fuzz tests: rotate the default subset per run (FILMCRAFT_FUZZ_SEED replays one)
* platform tests: decode-switch tests in their own binaries, one NVENC lock
* packaging: use [[ for bash conditional tests (SonarCloud S7688)
SonarCloud flags every bare [ conditional in bash scripts under packaging/ as a reliability smell (rule S7688). Convert the flagged [ ... ] tests to [[ ... ]]: safer word-splitting and no glob surprises, same semantics for these string and file tests.
* tools: quit the super-linear regex scans in the spec table extractors (SonarCloud S8786)
SonarCloud flags five regular expressions in the AV1/HEVC extract_tables.py tools as super-linear: a lazy (.*) under re.S that rescans the whole remaining spec text at every position, a \s*([^|]+?)\s* sandwich, and \s+(.*) tails that can absorb the same whitespace repeatedly.
- the fence block reader now walks the ~~~~ c / tilde-run pairs with two linear searches instead of the lazy .*? findall; block-pair and 7+ tilde-run semantics are unchanged
- the symbol-row capture restricts its first and last characters away from [|] so the lazy group's backtracking jumps are gone (identical for every non-blank cell; a cell of only spaces is no longer captured, which the table rows never contain)
- the empty-comma cell splitter no longer lets \s* swallow the newline it anchors on; consecutive line breaks inside a cell no longer split (spec cells are single-line rows)
- initValue / ScalingList value captures start on a non-space so \\s+ and .* no longer share a split point (captured text is unchanged for all real values)
Verified old vs new run on synthetic spec markdown: byte-identical spé tables output and stderr.
* security: validate CLI paths/args in dev tools (SonarCloud S2083/S8707/S8705/S7044)
Resolve and validate command-line path arguments before use, enforce HTTPS at
download sinks, and confirm path containment. Hardens local codegen/fetch tools
against path-traversal and command-argument-injection findings.
* ci: exclude dev tool scripts from SonarCloud coverage gate
The extract_tables.py codegen tools aren't part of the unit-tested surface;
exclude tools/, docs/, examples/ from coverage so the new-code coverage gate
doesn't block hardening-only changes.
- decoder.rs: MaxHSize 16384, MaxVSize 8704 and MaxPicSize 35651584 per AV1 Annex A (the earlier
16384 x 8704 product is four times the area limit); boundary unit tests on frame_size_allowed;
README states the three limits.
- testkit: with_deadline/set_progress (whole-sweep hang guard naming the last input;
FILMCRAFT_HOSTILE_TIMEOUT_SECS overrides) and ffmpeg_has_encoder.
- The nine fuzz_hostile tests drop their per-run elapsed assertions (which could not catch a
hang and could fail a slow runner) and run under the guard.
- A seed whose encoder is absent from ffmpeg still skips (and fails under
FILMCRAFT_REQUIRE_ORACLES=1); an encoder that is present but fails now fails the test.
- mpeg2v failure labels carry the seed name.
The cap was 1 << 27, below the 16384 x 8704 it quotes, so a conforming maximum-size
frame was refused. Extract frame_size_allowed() and test the exact boundary, plus a
stream at 16384 x 8704 that must pass the size check. Note the AV1 and MPEG-2 limits
in the crate READMEs. A fuzz seed that ffmpeg cannot encode (encoder missing) now
reports SKIPPED, and fails under FILMCRAFT_REQUIRE_ORACLES=1, instead of passing silently.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Arch Linux ARM's x265 4.3 is built for 8 bits only ("build info ...
8bit"). Asked for -pix_fmt yuv420p10le, ffmpeg encodes an 8-bit stream
without an error; the reference decode then scales it to 10 bits, so every
Main10 conformance test failed with the decoder's output at exactly a
quarter of the reference ("got 74 want 296"). The decoder was right: given
real Main10 streams made by an x86 x265 (8bit+10bit+12bit), the same nine
tests (six fixtures, the mismatch check, draft mode and hvcC input) pass
bit-exact on aarch64.
A 10-bit fixture whose stream ffprobe does not report as yuv420p10le is
now deleted (so a stale cache cannot linger) and skipped with the reason,
through filmcraft_testkit::skip, so FILMCRAFT_REQUIRE_ORACLES=1 still
fails loudly instead of passing by skipping.
Checked on aarch64 Linux: with the native 8-bit x265 the hevc crate is
green (the six libx265 Main10 tests now report ok by skipping and print
the reason; nothing fails); with FILMCRAFT_REQUIRE_ORACLES=1 intra_main10
fails naming the reason; with the x86-made Main10 fixtures all Main10
tests pass.
- av1: a 4:2:2 stream (profile bit flipped) could reach blocks with no chroma size (BLOCK_INVALID) and
index MAX_TX_SIZE_RECT out of bounds; reject them with an error in decode_block.
- av1: frame size accepted up to 65536x65536 (gigabytes of buffers from a few header bytes); cap at 16384 per
side and 2^27 samples.
- mpeg2v: 16383x16383 4:4:4 sequences (gigabytes per picture) are refused; area capped at 2^25.
- fuzz_hostile tests (truncation, header byte/bit sweeps, random mutation) for h264, hevc, vp8, vp9, av1,
mpeg2v, apv, prores and dnx; ffmpeg-based ones skip without ffmpeg.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>