diff --git a/crates/h264/README.md b/crates/h264/README.md new file mode 100644 index 00000000..44f97dad --- /dev/null +++ b/crates/h264/README.md @@ -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). diff --git a/crates/h264/src/cabac.rs b/crates/h264/src/cabac.rs index 5f6b9498..42e32bce 100644 --- a/crates/h264/src/cabac.rs +++ b/crates/h264/src/cabac.rs @@ -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 { - 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 } } diff --git a/crates/h264/src/cabac_mb.rs b/crates/h264/src/cabac_mb.rs index 87b8dede..6ecde49d 100644 --- a/crates/h264/src/cabac_mb.rs +++ b/crates/h264/src/cabac_mb.rs @@ -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<()> { diff --git a/crates/h264/src/inter.rs b/crates/h264/src/inter.rs index 4e3b5699..06577420 100644 --- a/crates/h264/src/inter.rs +++ b/crates/h264/src/inter.rs @@ -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(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; } diff --git a/crates/h264/src/picture.rs b/crates/h264/src/picture.rs index 8cf9c59b..44d872f3 100644 --- a/crates/h264/src/picture.rs +++ b/crates/h264/src/picture.rs @@ -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(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]) { diff --git a/crates/h264/src/slicedec.rs b/crates/h264/src/slicedec.rs index 4fa34e53..3edbfc79 100644 --- a/crates/h264/src/slicedec.rs +++ b/crates/h264/src/slicedec.rs @@ -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); } } diff --git a/crates/h264/src/synth_tests.rs b/crates/h264/src/synth_tests.rs index 26d5f429..dd81b58c 100644 --- a/crates/h264/src/synth_tests.rs +++ b/crates/h264/src/synth_tests.rs @@ -18,14 +18,19 @@ fn nal(ref_idc: u8, t: u8, rbsp: &[u8]) -> Vec { v } -fn sps(profile: u8, max_refs: u32, gaps: bool) -> Vec { +fn sps(profile: u8, max_refs: u32, gaps: bool, poc_type0: bool) -> Vec { 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 { nal(3, 7, &w.finish()) } -fn pps(cabac: bool) -> Vec { +fn pps(cabac: bool, weighted: bool) -> Vec { let mut w = BitWriter::new(); w.write_ue(0); w.write_ue(0); @@ -47,8 +52,8 @@ fn pps(cabac: bool) -> Vec { 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>>, + /// B slice (all B_Skip). + b: bool, + poc_lsb: Option, + /// 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 { 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 { 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>> = (0..NMB).map(|_| Some(random_mb(&mut seed))).collect(); + let b: Vec>> = (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 { + 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 { + 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); + } +}