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:
parent
20f0d08311
commit
aa8ee4fcf6
|
|
@ -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).
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
let (sig_tab, last_tab): (&[u8], &[u8]) = match cat {
|
||||
3 => (&CHROMA_DC_CTX, &CHROMA_DC_CTX),
|
||||
5 => (&SIG8_FRAME, &LAST8),
|
||||
_ => (&IDENT_CTX, &IDENT_CTX),
|
||||
};
|
||||
if c.decode_decision(sig_base + si) == 1 {
|
||||
sig[i] = true;
|
||||
if c.decode_decision(last_base + li) == 1 {
|
||||
num = i + 1;
|
||||
// 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<()> {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue