M2 perf/docs: faster CABAC engine and residual parsing, README

- CABAC engine keeps codIOffset scaled in a 64-bit register with 32-bit
  refills instead of re-reading the bitstream on every renormalisation
- significance map collected as a position list; context increments from
  per-category tables
- fixed-size row copies for motion-compensation windows and block copies
- explicit weighted prediction (P_Skip and B_Skip bi-prediction) synthetic
  test, cross-checked with ffmpeg
- crates/h264/README.md: features, API, threading model, fixture matrix,
  performance, known gaps and the plan for High 10 / 4:2:2

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-09-30 06:53:55 -04:00
parent 20f0d08311
commit aa8ee4fcf6
7 changed files with 425 additions and 96 deletions

166
crates/h264/README.md Normal file
View File

@ -0,0 +1,166 @@
# filmcraft-h264
Clean-room, pure-Rust (no `unsafe`) H.264 / AVC decoder, implemented from the public ITU-T
Rec. H.264 (ISO/IEC 14496-10) specification. It is an L0 crate: it depends only on
`filmcraft-bitstream`, `thiserror`, and optionally `rayon` (default feature `threads`), and builds
for `wasm32-unknown-unknown`.
All syntax tables (CAVLC code tables, the 1024 CABAC `(m, n)` context initialisation pairs,
`rangeTabLPS`, scaling defaults, deblocking thresholds, the 8x8 significance-map tables) were
extracted from the specification text (the extraction scripts only read the ITU-T PDF). No code was
taken or ported from FFmpeg, x264, JM or openh264. ffmpeg/libx264 are only used as external test
oracles and fixture generators.
## Supported
- NAL units: Annex-B byte streams and length-prefixed (`avcC`) samples; SPS (all fields incl.
VUI/HRD, scaling lists with fall-back rules A/B, cropping, high-profile fields), PPS (incl.
`transform_8x8_mode`, scaling lists, slice groups parsed), SEI / AUD / filler skipped.
- Slice header: all fields incl. reference list modification, prediction weight table and
decoded reference picture marking. POC types 0, 1, 2. `frame_num` gap handling ("non-existing"
frames).
- Entropy coding: CAVLC and CABAC (all syntax elements needed for frame coding, 4:2:0).
- Macroblocks: I (Intra 4x4 / 8x8 / 16x16, I_PCM), P (all partitions, P_8x8ref0, P_Skip),
B (all partitions and sub-partitions, B_Skip, B_Direct_16x16, B_Direct_8x8), spatial and temporal
direct prediction with/without `direct_8x8_inference`, constrained intra prediction.
- Transforms / quantisation: 4x4, 8x8, Intra16x16 DC Hadamard, 2x2 chroma DC; flat and custom
scaling matrices (SPS and PPS level).
- Inter prediction: quarter-sample 6-tap luma, eighth-sample chroma, explicit and implicit weighted
prediction, multiple reference frames, long-term references.
- DPB: sliding window, all MMCOs (1-6), IDR / `no_output_of_prior_pics`, bumping with DPB size from
the level (or VUI `max_dec_frame_buffering`) and `max_num_reorder_frames`.
- Deblocking filter: full bS derivation, `disable_deblocking_filter_idc` 0/1/2, slice offsets,
8x8-transform edges.
- Output: cropped 8-bit planar 4:2:0 pictures in output order with the caller's `pts`, POC, key
flag, VUI colour description (range, primaries, transfer, matrix) and sample aspect ratio.
- Frame-level multithreading (see below).
## API
```rust
use filmcraft_h264::Decoder;
let mut dec = Decoder::new(); // or Decoder::with_threads(n), Decoder::from_avcc(&avcc)?
for (pts, access_unit) in access_units {
for pic in dec.decode(access_unit, pts)? {
// pic.width / pic.height (cropped), pic.y / pic.u / pic.v, pic.y_stride / pic.uv_stride,
// pic.pts, pic.poc, pic.key, pic.color, pic.sar
}
}
for pic in dec.flush() { /* ... */ }
if let Some(err) = dec.take_error() { /* error reported by a decoding thread */ }
```
`decode` expects whole access units (a buffer with several complete access units — e.g. an
entire Annex-B file — also works). `Decoder::stats()` returns counters of the coding tools seen
(macroblock types, slice types, MMCOs, long-term marks, frame_num gaps, ...).
### Threading model
The calling thread parses headers and performs all POC / DPB / reference-list bookkeeping (it only
needs header data). The macroblock layer of each picture is decoded as a job on a rayon pool. A job
reconstructs into private buffers, deblocks each macroblock row once the row below it is
reconstructed, and publishes finished rows (samples + motion data for co-located lookups) into the
picture's shared `Frame` (`OnceLock` per macroblock row). Jobs of later pictures block per row on
exactly the reference rows they read, so many pictures decode concurrently while the output stays
bit-exact. Without the `threads` feature (or on wasm, or with `with_threads(1)`) jobs run inline.
With threads, `decode` returns pictures once their job has finished, so output lags input by up to
roughly the number of threads.
## Tests
`cargo test -p filmcraft-h264` (fixtures are generated on first use into `target/fixtures/h264/`
with ffmpeg + libx264; tests print a message and skip when `/opt/homebrew/bin/ffmpeg` is absent).
- Unit tests: CAVLC tables (prefix-freeness / Kraft sums, spec-style residual example), CABAC engine
(context init formula, round trip against an encoder model), intra predictors with hand-computed
values, transforms, luma interpolation against a straightforward reference implementation,
weighting, macroblock type tables, scaling-list parsing.
- Synthetic streams (`src/synth_tests.rs`): hand-built CAVLC and CABAC bitstreams covering what
libx264 never produces — I_PCM, long-term references, MMCO 1-6, `frame_num` gaps, reference list
modification (short- and long-term), explicit weighted prediction in P and B slices, B_Skip with
explicit bi-prediction. The expected output is known exactly and is also checked against ffmpeg.
- Robustness: avcC 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 byte-for-byte with `ffmpeg -f rawvideo -pix_fmt yuv420p`; on mismatch the first
differing frame, plane, sample position and macroblock are reported. Output pts/POC order is
checked too.
### Fixture matrix (all bit-exact, single- and multi-threaded)
| fixture | source / size | configuration |
|---|---|---|
| intra_cavlc | testsrc2 176x144 | Baseline, keyint=1, no deblock |
| intra_cavlc_noise | mandelbrot+noise 352x288 | Baseline, keyint=1, no deblock |
| intra_cavlc_8x8 | testsrc2+noise 352x288 | High CAVLC, 8x8 transform, intra only |
| intra_cavlc_cqm | testsrc2+noise 352x288 | High CAVLC, cqm=jvt, intra only |
| p_cavlc_nodeblock | testsrc2+noise 352x288 | Baseline, ref=3, no deblock |
| baseline_qcif | testsrc2 176x144 | Baseline, keyint=15 |
| baseline_cif_noise | mandelbrot+noise 352x288 | Baseline |
| cavlc_b | testsrc2+noise 352x288 | High, cabac=0, bframes=3 |
| cavlc_b_temporal | testsrc2+noise 352x288 | Main, cabac=0, direct=temporal, weightb |
| cavlc_weightp | testsrc2 fade 352x288 | Main, cabac=0, weightp=2, weightb |
| slices4_cavlc | testsrc2+noise 352x288 | Baseline, slices=4 |
| qp1_cavlc | mandelbrot+noise 176x144 | Main, CAVLC, QP 1 |
| main_cabac_b | testsrc2+noise 352x288 | Main (CABAC, B-frames) |
| high_720p | smptehdbars+noise 1280x720 | High |
| high_1080p | testsrc2 1920x1080 | High |
| crop_1918x1078 | testsrc2+noise 1918x1078 | High, frame cropping |
| bpyramid | testsrc2+noise 352x288 | bframes=3, b-pyramid=normal (MMCO) |
| weightp2 | testsrc2 fade 352x288 | weightp=2 |
| weightb | testsrc2 fade 352x288 | weightb=1, bframes=3 (implicit) |
| direct_temporal | testsrc2+noise 352x288 | direct=temporal |
| direct_spatial | mandelbrot+noise 352x288 | direct=spatial |
| ref4 | testsrc2+noise 352x288 | ref=4 |
| no_deblock | testsrc2+noise 352x288 | no-deblock |
| deblock_m2_2 | testsrc2+noise 352x288 | deblock=-2,2 |
| cqm_jvt | testsrc2+noise 352x288 | cqm=jvt |
| slices4 | testsrc2+noise 352x288 | slices=4 (CABAC) |
| keyint10 | testsrc2+noise 352x288 | keyint=10 |
| open_gop | testsrc2+noise 352x288 | open-gop, keyint=12, bframes=3 |
| constrained_intra | testsrc2+noise 352x288 | constrained-intra |
| no8x8dct | testsrc2+noise 352x288 | 8x8dct=0 |
| qp50 | testsrc2+noise 352x288 | QP 50 |
| qp1 | mandelbrot+noise 352x288 | QP 1 |
| bench_1080p | testsrc2+noise 1920x1080, 120 frames | preset medium, CRF 20 (8.8 Mbit/s) |
`cargo test --release -p filmcraft-h264 --test conformance coverage_report -- --ignored --nocapture`
prints which coding tools each fixture exercises.
## Performance
`cargo test --release -p filmcraft-h264 --test perf -- --ignored --nocapture` (1080p High profile,
CABAC, B-pyramid, 120 frames, 8.8 Mbit/s; bit-exactness is verified first). Measured on an Apple M4
Pro (14 cores) while the machine was heavily shared (load average 40-180), so these are lower bounds:
| threads | fps |
|---|---|
| 1 | ~100-115 (≈ 36 Mcycles per frame) |
| 14 | ~570-600 |
The example `cargo run --release -p filmcraft-h264 --example h264dec -- in.h264 out.yuv` decodes a
file; `H264_BENCH_ITERS=n` (and `H264_THREADS=t`) turns it into a benchmark, `H264_STATS=1` prints
the coverage counters.
## Known gaps
- Interlaced coding (field pictures, PAFF, MBAFF): streams with `frame_mbs_only_flag = 0` return
`Error::Unsupported`.
- Only 8-bit 4:2:0. High 10 / 4:2:2 / 4:4:4 / monochrome and lossless
(`qpprime_y_zero_transform_bypass`) return `Error::Unsupported`.
- FMO (slice groups), SP/SI slices, data partitioning (Extended profile), MVC/SVC NAL units.
- Error concealment is minimal: undecodable slices leave the prediction/previous content; missing
references are replaced by other references.
- Single-threaded speed has no SIMD-specific code yet (the hot loops are written to auto-vectorise).
### Extending to High 10 and 4:2:2
The chroma format and bit depth are isolated behind `Sps::check_supported`. The parts that are
format-specific today: `Planes`/`FrameRow` sample type (`u8`; a `u16` variant or a generic sample
type is needed for bit depths > 8, together with `Clip1` ranges, `QpBdOffset` in the QP
derivations and the scaled tC0/alpha/beta in deblocking), chroma block geometry (`MbWidthC` /
`MbHeightC` = 8x8 in `slicedec`, `intra::pred_chroma`, the chroma DC transform and the CABAC/CAVLC
chroma DC parsing — the 2x4 total_zeros tables and ctxBlockCat 3 `NumC8x8` handling are already in
place), chroma motion-vector vertical scaling and the chroma deblocking edges (4:2:2 has four
horizontal chroma edges).

View File

@ -65,10 +65,13 @@ pub const NUM_CTX: usize = 1024;
pub struct Cabac<'a> {
data: &'a [u8],
/// Bit position of the next unread bit.
pos: usize,
/// Byte position of the next byte to load into `value`.
next_byte: usize,
range: u32,
offset: u32,
/// codIOffset scaled by 2^bits: value = (codIOffset << bits) | (bits already loaded but not yet
/// consumed by the arithmetic decoder).
value: u64,
bits: u32,
pub ctx: [u8; NUM_CTX],
}
@ -76,7 +79,7 @@ impl<'a> Cabac<'a> {
/// Create an engine over `data` (slice RBSP) starting at bit `pos` (must be byte aligned),
/// with contexts initialised for `slice_qp` and `init` (0..=2 cabac_init_idc, 3 = I slice).
pub fn new(data: &'a [u8], pos: usize, slice_qp: i32, init: usize) -> Result<Self> {
let mut c = Cabac { data, pos, range: 0, offset: 0, ctx: [0; NUM_CTX] };
let mut c = Cabac { data, next_byte: pos / 8, range: 0, value: 0, bits: 0, ctx: [0; NUM_CTX] };
c.init_contexts(slice_qp, init);
c.init_engine()?;
Ok(c)
@ -93,76 +96,95 @@ impl<'a> Cabac<'a> {
// ctxIdx 276 (end_of_slice / I_PCM) is handled by decode_terminate.
}
/// 9.3.1.2: codIRange = 510, codIOffset = read_bits(9).
/// 9.3.1.2: codIRange = 510, codIOffset = read_bits(9) (from the current byte-aligned position).
pub fn init_engine(&mut self) -> Result<()> {
self.range = 510;
self.offset = self.read_bits(9);
ensure!(self.offset < 510, "invalid CABAC offset at init");
self.value = 0;
self.bits = 0;
self.refill();
// the top 9 loaded bits form codIOffset; the remaining ones stay buffered below it
self.bits -= 9;
ensure!((self.value >> self.bits) < 510, "invalid CABAC offset at init");
Ok(())
}
/// Load 32 more bits (zeros past the end of the data).
#[inline(always)]
fn read_bits(&mut self, n: u32) -> u32 {
// big-endian 64-bit window starting at the byte containing `pos`
let byte = self.pos >> 3;
let mut w = [0u8; 8];
if let Some(src) = self.data.get(byte..byte + 8) {
fn refill(&mut self) {
let b = self.next_byte;
let mut w = [0u8; 4];
if let Some(src) = self.data.get(b..b + 4) {
w.copy_from_slice(src);
} else if byte < self.data.len() {
let n = self.data.len() - byte;
w[..n].copy_from_slice(&self.data[byte..]);
} else if b < self.data.len() {
let n = self.data.len() - b;
w[..n].copy_from_slice(&self.data[b..]);
}
let v = u64::from_be_bytes(w) << (self.pos & 7);
self.pos += n as usize;
(v >> (64 - n)) as u32
self.next_byte += 4;
self.value = (self.value << 32) | u32::from_be_bytes(w) as u64;
self.bits += 32;
}
/// Bit position (for I_PCM alignment after decode_terminate returned 1).
/// Bit position of the next bit not consumed by the arithmetic decoder (for I_PCM alignment
/// after decode_terminate returned 1).
pub fn bit_pos(&self) -> usize {
self.pos
self.next_byte * 8 - self.bits as usize
}
/// Continue reading at byte-aligned bit position `pos` (call `init_engine` afterwards).
pub fn set_bit_pos(&mut self, pos: usize) {
self.pos = pos;
self.next_byte = pos / 8;
self.value = 0;
self.bits = 0;
}
pub fn data(&self) -> &'a [u8] {
self.data
}
/// True when the reader has run past the end of the data (corrupt stream).
pub fn overrun(&self) -> bool {
self.pos > self.data.len() * 8 + 64
self.next_byte > self.data.len() + 16
}
#[inline(always)]
fn renorm(&mut self) {
if self.range < 256 {
let shift = self.range.leading_zeros() - 23;
self.range <<= shift;
if self.bits < shift {
self.refill();
}
self.bits -= shift;
}
}
#[inline(always)]
pub fn decode_decision(&mut self, ctx_idx: usize) -> u32 {
let s = self.ctx[ctx_idx] as usize;
let p = s >> 1;
let mps = (s & 1) as u32;
let q = ((self.range >> 6) & 3) as usize;
let lps = RANGE_TAB_LPS[p][q] as u32;
let lps = RANGE_TAB_LPS[s >> 1][q] as u32;
self.range -= lps;
let scaled = (self.range as u64) << self.bits;
let bin;
if self.offset >= self.range {
bin = 1 - mps;
self.offset -= self.range;
if self.value >= scaled {
bin = 1 - (s & 1) as u32;
self.value -= scaled;
self.range = lps;
self.ctx[ctx_idx] = NEXT_STATE[s][1];
} else {
bin = mps;
bin = (s & 1) as u32;
self.ctx[ctx_idx] = NEXT_STATE[s][0];
}
if self.range < 256 {
let shift = self.range.leading_zeros() - 23;
self.range <<= shift;
self.offset = (self.offset << shift) | self.read_bits(shift);
}
self.renorm();
bin
}
#[inline(always)]
pub fn decode_bypass(&mut self) -> u32 {
self.offset = (self.offset << 1) | self.read_bits(1);
if self.offset >= self.range {
self.offset -= self.range;
if self.bits == 0 {
self.refill();
}
self.bits -= 1;
let scaled = (self.range as u64) << self.bits;
if self.value >= scaled {
self.value -= scaled;
1
} else {
0
@ -171,14 +193,11 @@ impl<'a> Cabac<'a> {
pub fn decode_terminate(&mut self) -> u32 {
self.range -= 2;
if self.offset >= self.range {
let scaled = (self.range as u64) << self.bits;
if self.value >= scaled {
1
} else {
if self.range < 256 {
let shift = self.range.leading_zeros() - 23;
self.range <<= shift;
self.offset = (self.offset << shift) | self.read_bits(shift);
}
self.renorm();
0
}
}

View File

@ -21,6 +21,19 @@ const LAST8: [u8; 63] = [
3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8,
];
/// ctxIdxInc = levelListIdx (ctxBlockCat 0, 1, 2, 4).
const IDENT_CTX: [u8; 64] = {
let mut t = [0u8; 64];
let mut i = 0;
while i < 64 {
t[i] = i as u8;
i += 1;
}
t
};
/// ctxIdxInc = Min(levelListIdx / NumC8x8, 2) for 4:2:0 chroma DC (ctxBlockCat 3).
const CHROMA_DC_CTX: [u8; 4] = [0, 1, 2, 2];
/// Table 9-40 ctxIdxBlockCatOffset for ctxBlockCat 0..=5: (coded_block_flag, significant/last, abs level).
const CAT_OFFSET: [(usize, usize, usize); 6] = [(0, 0, 0), (4, 15, 10), (8, 29, 20), (12, 44, 30), (16, 47, 39), (0, 0, 0)];
@ -371,33 +384,34 @@ impl SliceDecoder<'_> {
}
let (sig_base, last_base, abs_base) =
if cat == 5 { (402, 417, 426) } else { (105 + CAT_OFFSET[cat].1, 166 + CAT_OFFSET[cat].1, 227 + CAT_OFFSET[cat].2) };
let mut sig = [false; 64];
let mut num = max_num;
let mut i = 0;
while i + 1 < num {
let (si, li) = match cat {
3 => (i.min(2), i.min(2)),
5 => (SIG8_FRAME[i] as usize, LAST8[i] as usize),
_ => (i, i),
};
if c.decode_decision(sig_base + si) == 1 {
sig[i] = true;
if c.decode_decision(last_base + li) == 1 {
num = i + 1;
let (sig_tab, last_tab): (&[u8], &[u8]) = match cat {
3 => (&CHROMA_DC_CTX, &CHROMA_DC_CTX),
5 => (&SIG8_FRAME, &LAST8),
_ => (&IDENT_CTX, &IDENT_CTX),
};
// significance map: positions of the significant coefficients in scan order
let mut pos = [0u8; 64];
let mut n = 0usize;
let last_i = max_num - 1;
let mut found_last = false;
for i in 0..last_i {
if c.decode_decision(sig_base + sig_tab[i] as usize) == 1 {
pos[n] = i as u8;
n += 1;
if c.decode_decision(last_base + last_tab[i] as usize) == 1 {
found_last = true;
break;
}
}
i += 1;
}
sig[num - 1] = true;
if !found_last {
pos[n] = last_i as u8;
n += 1;
}
let mut eq1 = 0usize;
let mut gt1 = 0usize;
let mut count = 0u8;
let gt1_cap = if cat == 3 { 3 } else { 4 };
for i in (0..num).rev() {
if !sig[i] {
continue;
}
for &p in pos[..n].iter().rev() {
let inc0 = if gt1 != 0 { 0 } else { (1 + eq1).min(4) };
let mut abs = 1i32;
if c.decode_decision(abs_base + inc0) == 1 {
@ -423,16 +437,13 @@ impl SliceDecoder<'_> {
}
abs += suf;
}
}
if abs == 1 {
eq1 += 1;
} else {
gt1 += 1;
} else {
eq1 += 1;
}
coeff[i] = if c.decode_bypass() == 1 { -abs } else { abs };
count += 1;
coeff[p as usize] = if c.decode_bypass() == 1 { -abs } else { abs };
}
count
n as u8
}
fn residual_cabac(&mut self, c: &mut Cabac, cbp: u8) -> Result<()> {

View File

@ -50,6 +50,23 @@ fn clip1(v: i32) -> u8 {
v.clamp(0, 255) as u8
}
/// Copy `n` bytes with a fixed-size move for the common block widths.
#[inline(always)]
fn copy_n(dst: &mut [u8], src: &[u8], n: usize) {
fn fixed<const N: usize>(dst: &mut [u8], src: &[u8]) {
let s: &[u8; N] = src[..N].try_into().unwrap();
let d: &mut [u8; N] = (&mut dst[..N]).try_into().unwrap();
*d = *s;
}
match n {
16 => fixed::<16>(dst, src),
8 => fixed::<8>(dst, src),
4 => fixed::<4>(dst, src),
2 => fixed::<2>(dst, src),
_ => dst[..n].copy_from_slice(&src[..n]),
}
}
#[inline(always)]
fn avg(a: u8, b: u8) -> u8 {
((a as u16 + b as u16 + 1) >> 1) as u8
@ -100,7 +117,7 @@ pub fn mc_luma_win(src: &[u8], ss: usize, fx: u32, fy: u32, bw: usize, bh: usize
match (fx, fy) {
(0, 0) => {
for r in 0..bh {
out[r * os..r * os + bw].copy_from_slice(&wrow(r + 2)[2..2 + bw]);
copy_n(&mut out[r * os..], &wrow(r + 2)[2..], bw);
}
}
(_, 0) => {
@ -230,7 +247,7 @@ pub fn mc_chroma_win(src: &[u8], ss: usize, fx: u32, fy: u32, bw: usize, bh: usi
let (w00, w10, w01, w11) = ((8 - fx) * (8 - fy), fx * (8 - fy), (8 - fx) * fy, fx * fy);
if fx == 0 && fy == 0 {
for r in 0..bh {
out[r * os..r * os + bw].copy_from_slice(&src[r * ss..r * ss + bw]);
copy_n(&mut out[r * os..], &src[r * ss..], bw);
}
return;
}
@ -285,7 +302,7 @@ pub fn weighted_store(dst: &mut [u8], ds: usize, p0: Option<&[u8]>, p1: Option<&
let (wt, o, log_wd) = match (w, p0.is_some()) {
(Weight::Default, _) => {
for r in 0..bh {
dst[r * ds..r * ds + bw].copy_from_slice(&a[r * ps..r * ps + bw]);
copy_n(&mut dst[r * ds..], &a[r * ps..], bw);
}
return;
}

View File

@ -235,7 +235,7 @@ impl Frame {
let yy = (y0 + r as i32).clamp(0, height as i32 - 1) as usize;
let row = self.row(yy >> 4);
let line = &row.y[(yy & 15) * width..(yy & 15) * width + width];
copy_clamped(line, x0, &mut out[r * os..r * os + w]);
copy_line::<24>(line, x0, &mut out[r * os..], w);
}
}
@ -248,7 +248,7 @@ impl Frame {
let row = self.row(yy >> 3);
let plane = if c == 0 { &row.cb } else { &row.cr };
let line = &plane[(yy & 7) * cw..(yy & 7) * cw + cw];
copy_clamped(line, x0, &mut out[r * os..r * os + w]);
copy_line::<16>(line, x0, &mut out[r * os..], w);
}
}
@ -273,6 +273,19 @@ impl Frame {
}
}
/// Copy `w` samples of `line` starting at x0 into `out`; when possible a fixed-size block of `N`
/// bytes is copied instead (cheaper than a variable-length copy; `out` must then have room for `N`).
#[inline(always)]
fn copy_line<const N: usize>(line: &[u8], x0: i32, out: &mut [u8], w: usize) {
if w <= N && x0 >= 0 && x0 as usize + N <= line.len() && out.len() >= N {
let src: &[u8; N] = line[x0 as usize..x0 as usize + N].try_into().unwrap();
let dst: &mut [u8; N] = (&mut out[..N]).try_into().unwrap();
*dst = *src;
} else {
copy_clamped(line, x0, &mut out[..w]);
}
}
/// Copy `out.len()` samples of `line` starting at x0, replicating edge samples outside the line.
#[inline(always)]
fn copy_clamped(line: &[u8], x0: i32, out: &mut [u8]) {

View File

@ -210,8 +210,8 @@ pub struct SliceDecoder<'a> {
#[allow(clippy::too_many_arguments)]
#[inline]
fn predict_block(f: &Frame, mv: [i16; 2], px: usize, py: usize, bw: usize, bh: usize, y: (&mut [u8], usize), c: [&mut [u8]; 2], cs: usize) {
const LW: usize = 21;
let mut win = [0u8; LW * LW];
const LW: usize = 24;
let mut win = [0u8; LW * 21];
let ix = px as i32 + (mv[0] as i32 >> 2);
let iy = py as i32 + (mv[1] as i32 >> 2);
f.luma_window(ix - 2, iy - 2, bw + 5, bh + 5, &mut win, LW);
@ -221,9 +221,9 @@ fn predict_block(f: &Frame, mv: [i16; 2], px: usize, py: usize, bw: usize, bh: u
let (cw, ch) = (bw / 2, bh / 2);
let (fx, fy) = ((mv[0] & 7) as u32, (mv[1] & 7) as u32);
for (comp, out) in c.into_iter().enumerate() {
let mut w = [0u8; 9 * 9];
f.chroma_window(comp, cx, cy, cw + 1, ch + 1, &mut w, 9);
inter::mc_chroma_win(&w, 9, fx, fy, cw, ch, out, cs);
let mut w = [0u8; 16 * 9];
f.chroma_window(comp, cx, cy, cw + 1, ch + 1, &mut w, 16);
inter::mc_chroma_win(&w, 16, fx, fy, cw, ch, out, cs);
}
}

View File

@ -18,14 +18,19 @@ fn nal(ref_idc: u8, t: u8, rbsp: &[u8]) -> Vec<u8> {
v
}
fn sps(profile: u8, max_refs: u32, gaps: bool) -> Vec<u8> {
fn sps(profile: u8, max_refs: u32, gaps: bool, poc_type0: bool) -> Vec<u8> {
let mut w = BitWriter::new();
w.write_bits(profile as u32, 8);
w.write_bits(0, 8);
w.write_bits(30, 8);
w.write_ue(0); // sps id
w.write_ue(0); // log2_max_frame_num - 4 -> 16
w.write_ue(2); // POC type 2 (output order = decoding order)
if poc_type0 {
w.write_ue(0);
w.write_ue(0); // log2_max_pic_order_cnt_lsb - 4 -> 4-bit lsb
} else {
w.write_ue(2); // POC type 2 (output order = decoding order)
}
w.write_ue(max_refs);
w.write_bit(gaps);
w.write_ue(MB_W as u32 - 1);
@ -38,7 +43,7 @@ fn sps(profile: u8, max_refs: u32, gaps: bool) -> Vec<u8> {
nal(3, 7, &w.finish())
}
fn pps(cabac: bool) -> Vec<u8> {
fn pps(cabac: bool, weighted: bool) -> Vec<u8> {
let mut w = BitWriter::new();
w.write_ue(0);
w.write_ue(0);
@ -47,8 +52,8 @@ fn pps(cabac: bool) -> Vec<u8> {
w.write_ue(0); // slice groups
w.write_ue(0); // num_ref_idx_l0_default - 1
w.write_ue(0);
w.write_bit(false); // weighted_pred
w.write_bits(0, 2);
w.write_bit(weighted); // weighted_pred
w.write_bits(if weighted { 1 } else { 0 }, 2); // explicit weighted bi-prediction
w.write_se(0); // pic_init_qp - 26
w.write_se(0);
w.write_se(0); // chroma_qp_index_offset
@ -70,25 +75,46 @@ struct Pic {
/// MMCOs: (op, a, b) with a = difference_of_pic_nums_minus1 / long_term_pic_num /
/// max_long_term_frame_idx_plus1 and b = long_term_frame_idx
mmcos: Vec<(u32, u32, u32)>,
/// Per MB: Some(samples) for I_PCM, None for P_Skip.
/// Per MB: Some(samples) for I_PCM, None for P_Skip / B_Skip.
mbs: Vec<Option<Vec<u8>>>,
/// B slice (all B_Skip).
b: bool,
poc_lsb: Option<u32>,
/// Explicit weights (luma, cb, cr) as (weight, offset) for refIdx 0 of list 0 and list 1;
/// log2 denominators are 5 (luma) and 3 (chroma).
wt: Option<[[(i32, i32); 3]; 2]>,
}
impl Pic {
fn is_p(&self) -> bool {
!self.idr && self.mbs.iter().any(|m| m.is_none())
!self.idr && !self.b && self.mbs.iter().any(|m| m.is_none())
}
fn inter(&self) -> bool {
self.b || self.is_p()
}
}
fn slice_header(w: &mut BitWriter, p: &Pic, cabac: bool) {
w.write_ue(0); // first_mb
w.write_ue(if p.is_p() { 5 } else { 7 });
w.write_ue(if p.b {
6
} else if p.is_p() {
5
} else {
7
});
w.write_ue(0); // pps id
w.write_bits(p.frame_num, 4);
if p.idr {
w.write_ue(0);
}
if p.is_p() {
if let Some(lsb) = p.poc_lsb {
w.write_bits(lsb, 4);
}
if p.b {
w.write_bit(true); // direct_spatial_mv_pred_flag
}
if p.inter() {
w.write_bit(false); // num_ref_idx_override
w.write_bit(!p.mods.is_empty());
if !p.mods.is_empty() {
@ -98,6 +124,23 @@ fn slice_header(w: &mut BitWriter, p: &Pic, cabac: bool) {
}
w.write_ue(3);
}
if p.b {
w.write_bit(false); // no list 1 modification
}
}
if let (Some(wt), true) = (p.wt, p.inter()) {
w.write_ue(5);
w.write_ue(3);
for l in 0..if p.b { 2 } else { 1 } {
w.write_bit(true);
w.write_se(wt[l][0].0);
w.write_se(wt[l][0].1);
w.write_bit(true);
for c in 1..3 {
w.write_se(wt[l][c].0);
w.write_se(wt[l][c].1);
}
}
}
if p.ref_idc != 0 {
if p.idr {
@ -124,7 +167,7 @@ fn slice_header(w: &mut BitWriter, p: &Pic, cabac: bool) {
}
}
}
if cabac && p.is_p() {
if cabac && p.inter() {
w.write_ue(0); // cabac_init_idc
}
w.write_se(0); // slice_qp_delta
@ -215,7 +258,10 @@ fn write_pcm(w: &mut BitWriter, samples: &[u8]) {
fn slice_nal(p: &Pic, cabac: bool) -> Vec<u8> {
let mut w = BitWriter::new();
slice_header(&mut w, p, cabac);
if !cabac {
if !cabac && p.b {
w.write_ue(NMB as u32);
w.rbsp_trailing();
} else if !cabac {
let mut run = 0;
for mb in &p.mbs {
match mb {
@ -240,15 +286,15 @@ fn slice_nal(p: &Pic, cabac: bool) -> Vec<u8> {
while !w.is_byte_aligned() {
w.write_bit(true); // cabac_alignment_one_bit
}
let mut e = Enc::new(26, if p.is_p() { 0 } else { 3 });
let mut e = Enc::new(26, if p.inter() { 0 } else { 3 });
let skip = |i: usize| p.mbs[i].is_none();
for (i, mb) in p.mbs.iter().enumerate() {
let (x, y) = (i % MB_W, i / MB_W);
let a = if x > 0 { Some(i - 1) } else { None };
let b = if y > 0 { Some(i - MB_W) } else { None };
if p.is_p() {
if p.inter() {
let inc = a.map(|n| !skip(n) as usize).unwrap_or(0) + b.map(|n| !skip(n) as usize).unwrap_or(0);
e.encode(&mut w, 11 + inc, skip(i) as u32);
e.encode(&mut w, if p.b { 24 } else { 11 } + inc, skip(i) as u32);
}
if let Some(s) = mb {
if p.is_p() {
@ -328,8 +374,8 @@ fn build_and_check(cabac: bool, threads: usize) {
// 7: MMCO 5 picture made of PCM, then 8: P_Skip referencing it (only reference left)
Pic { frame_num: 8, ref_idc: 2, mmcos: vec![(5, 0, 0)], mbs: pcm_pic(&mut seed), ..Default::default() },
];
let mut stream = sps(if cabac { 77 } else { 66 }, 8, true);
stream.extend(pps(cabac));
let mut stream = sps(if cabac { 77 } else { 66 }, 8, true, false);
stream.extend(pps(cabac, false));
for p in &pics {
stream.extend(slice_nal(p, cabac));
}
@ -405,3 +451,60 @@ fn pcm_long_term_mmco_gaps_cabac() {
build_and_check(true, 1);
build_and_check(true, 4);
}
fn weighted_check(cabac: bool, threads: usize) {
let mut seed = 7u32;
let a: Vec<Option<Vec<u8>>> = (0..NMB).map(|_| Some(random_mb(&mut seed))).collect();
let b: Vec<Option<Vec<u8>>> = (0..NMB).map(|_| Some(random_mb(&mut seed))).collect();
let skip = vec![None; NMB];
let wp = [[(20, 3), (5, -1), (12, 2)], [(0, 0); 3]];
let wb = [[(40, -2), (4, 1), (9, -3)], [(30, 7), (6, 0), (2, 4)]];
// decoding order: I (POC 0), P (POC 8), B (POC 4, both refs), non-reference weighted P_Skip (POC 12)
let pics = [
Pic { idr: true, frame_num: 0, ref_idc: 3, poc_lsb: Some(0), mbs: a.clone(), ..Default::default() },
Pic { frame_num: 1, ref_idc: 2, poc_lsb: Some(8), mbs: b.clone(), wt: Some(wp), ..Default::default() },
Pic { frame_num: 2, ref_idc: 0, poc_lsb: Some(4), b: true, mbs: skip.clone(), wt: Some(wb), ..Default::default() },
Pic { frame_num: 2, ref_idc: 0, poc_lsb: Some(12), mbs: skip.clone(), wt: Some(wp), ..Default::default() },
];
let mut stream = sps(77, 2, false, true);
stream.extend(pps(cabac, true));
for p in &pics {
stream.extend(slice_nal(p, cabac));
}
let pa = render(&a, None);
let pb = render(&b, None);
let clip = |v: i32| v.clamp(0, 255) as u8;
let mix = |x: &[u8], y: &[u8], c: usize| -> Vec<u8> {
let (w0, o0) = wb[0][c];
let (w1, o1) = wb[1][c];
let d = if c == 0 { 5 } else { 3 };
x.iter().zip(y).map(|(&p, &q)| clip(((p as i32 * w0 + q as i32 * w1 + (1 << d)) >> (d + 1)) + ((o0 + o1 + 1) >> 1))).collect()
};
let single = |x: &[u8], c: usize| -> Vec<u8> {
let (w, o) = wp[0][c];
let d = if c == 0 { 5 } else { 3 };
x.iter().map(|&p| clip(((p as i32 * w + (1 << (d - 1))) >> d) + o)).collect()
};
let pbi = (mix(&pa.0, &pb.0, 0), mix(&pa.1, &pb.1, 1), mix(&pa.2, &pb.2, 2));
let pp = (single(&pb.0, 0), single(&pb.1, 1), single(&pb.2, 2));
let expected = [pa, pbi, pb, pp];
if threads == 1 {
cross_check_ffmpeg(&stream, &expected, if cabac { "synth_wp_cabac" } else { "synth_wp_cavlc" });
}
let mut dec = Decoder::with_threads(threads);
let mut out = dec.decode(&stream, 0).unwrap();
out.extend(dec.flush());
assert!(dec.take_error().is_none());
assert_eq!(out.len(), 4);
for (i, (p, e)) in out.iter().zip(expected.iter()).enumerate() {
assert!(p.y == e.0 && p.u == e.1 && p.v == e.2, "picture {i} differs (cabac={cabac}, threads={threads})");
}
}
#[test]
fn explicit_weighted_prediction_p_and_b_skip() {
for cabac in [false, true] {
weighted_check(cabac, 1);
weighted_check(cabac, 3);
}
}