M4.9: H.264 draft mode (no deblocking of non-reference pictures, off by default)
Decoder::set_draft: non-reference pictures skip the deblocking filter and are flagged Picture::draft; all other pictures stay bit-exact (conformance test, single- and frame-threaded). Conformance fixtures now also decode with 3 threads. Decoder::threads / default_threads for perf.stats. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a95fd5c829
commit
b3a09fab64
|
|
@ -1,5 +1,6 @@
|
|||
//! Decode an Annex-B H.264 file to raw yuv420p: `h264dec in.h264 [out.yuv]`.
|
||||
//! Prints timing information to stderr.
|
||||
//! Prints timing information to stderr. `H264_BENCH_ITERS=n` (with `H264_THREADS=t`, and
|
||||
//! `H264_DRAFT=1` for draft mode) turns it into a benchmark.
|
||||
|
||||
use filmcraft_h264::Decoder;
|
||||
use std::io::Write;
|
||||
|
|
@ -18,6 +19,7 @@ fn main() {
|
|||
let threads = std::env::var("H264_THREADS").ok().and_then(|v| v.parse::<usize>().ok());
|
||||
for _ in 0..n {
|
||||
let mut dec = threads.map(Decoder::with_threads).unwrap_or_default();
|
||||
dec.set_draft(std::env::var_os("H264_DRAFT").is_some());
|
||||
let t0 = std::time::Instant::now();
|
||||
frames = dec.decode(&data, 0).expect("decode").len() + dec.flush().len();
|
||||
best = best.min(t0.elapsed().as_secs_f64());
|
||||
|
|
|
|||
|
|
@ -159,7 +159,10 @@ impl<'a> Cabac<'a> {
|
|||
/// at high bit rates, so it selects values instead of jumping.
|
||||
#[inline(always)]
|
||||
pub fn decode_decision(&mut self, ctx_idx: usize) -> u32 {
|
||||
let s = self.ctx[ctx_idx] as usize;
|
||||
// Every ctxIdx is < 1024 and every state < 128: the masks only let the compiler drop the
|
||||
// bounds checks from the per-bin path.
|
||||
let ctx_idx = ctx_idx & (NUM_CTX - 1);
|
||||
let s = (self.ctx[ctx_idx] & 127) as usize;
|
||||
let q = ((self.range >> 6) & 3) as usize;
|
||||
let lps = RANGE_TAB_LPS[s >> 1][q] as u32;
|
||||
let mps_range = self.range - lps;
|
||||
|
|
|
|||
|
|
@ -63,6 +63,8 @@ pub struct DecodeStats {
|
|||
pub frame_num_gaps: u64,
|
||||
pub weighted_slices: u64,
|
||||
pub temporal_direct_slices: u64,
|
||||
/// Non-reference pictures decoded in draft mode (deblocking skipped).
|
||||
pub draft_pictures: u64,
|
||||
}
|
||||
|
||||
/// State shared with decoding jobs.
|
||||
|
|
@ -93,6 +95,9 @@ pub struct Decoder {
|
|||
pool: Option<rayon::ThreadPool>,
|
||||
in_flight: VecDeque<FrameRef>,
|
||||
max_in_flight: usize,
|
||||
threads: usize,
|
||||
/// Draft mode: skip deblocking of non-reference pictures ([`Decoder::set_draft`]).
|
||||
draft: bool,
|
||||
}
|
||||
|
||||
impl Default for Decoder {
|
||||
|
|
@ -122,7 +127,9 @@ fn is_new_picture(prev: &SliceHeader, sh: &SliceHeader, sps: &Sps) -> bool {
|
|||
false
|
||||
}
|
||||
|
||||
fn default_threads() -> usize {
|
||||
/// The worker threads [`Decoder::new`] uses: the available cores (at most 16) with the `threads`
|
||||
/// feature on native targets, 1 otherwise.
|
||||
pub fn default_threads() -> usize {
|
||||
#[cfg(all(feature = "threads", not(target_arch = "wasm32")))]
|
||||
{
|
||||
std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1).min(16)
|
||||
|
|
@ -148,6 +155,10 @@ impl Decoder {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(feature = "threads")]
|
||||
let workers = if pool.is_some() { threads } else { 1 };
|
||||
#[cfg(not(feature = "threads"))]
|
||||
let workers = 1;
|
||||
Decoder {
|
||||
spss: vec![None; 32],
|
||||
ppss: vec![None; 256],
|
||||
|
|
@ -165,9 +176,30 @@ impl Decoder {
|
|||
pool,
|
||||
in_flight: VecDeque::new(),
|
||||
max_in_flight: threads.max(1) + 2,
|
||||
threads: workers,
|
||||
draft: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Worker threads decoding pictures (1: on the calling thread).
|
||||
pub fn threads(&self) -> usize {
|
||||
self.threads
|
||||
}
|
||||
|
||||
/// Draft mode (off by default) for reduced-resolution playback: pictures submitted from now
|
||||
/// on that are non-reference pictures (`nal_ref_idc` 0) skip the deblocking filter (8.7).
|
||||
/// Nothing predicts from a non-reference picture (and deblocking never changes the motion
|
||||
/// data used for direct prediction), so every other picture stays bit-exact; the draft
|
||||
/// pictures themselves are approximate and flagged [`Picture::draft`]. Never use it for
|
||||
/// export or for a frame shown while paused.
|
||||
pub fn set_draft(&mut self, on: bool) {
|
||||
self.draft = on;
|
||||
}
|
||||
|
||||
pub fn draft(&self) -> bool {
|
||||
self.draft
|
||||
}
|
||||
|
||||
/// Configure from an `avcC` (AVCDecoderConfigurationRecord) box payload.
|
||||
pub fn from_avcc(avcc: &[u8]) -> Result<Self> {
|
||||
let mut d = Decoder::new();
|
||||
|
|
@ -450,19 +482,20 @@ impl Decoder {
|
|||
fn submit_pending(&mut self) {
|
||||
let Some(p) = self.pending.take() else { return };
|
||||
let PendingPic { frame, sps, first, poc, pts, key, slices } = p;
|
||||
self.dispatch(frame.clone(), slices);
|
||||
let draft = self.draft && first.nal_ref_idc == 0;
|
||||
self.dispatch(frame.clone(), slices, draft);
|
||||
self.poc_state.update(&first, &poc);
|
||||
if first.nal_ref_idc != 0 {
|
||||
self.prev_ref_frame_num = if first.has_mmco5() { 0 } else { first.frame_num };
|
||||
}
|
||||
let meta = Arc::new(output_meta(&sps, pts, key));
|
||||
let meta = Arc::new(OutputMeta { draft, ..output_meta(&sps, pts, key) });
|
||||
let mut outs = Vec::new();
|
||||
let fpoc = frame.poc;
|
||||
self.dpb.store_picture(&first, frame, fpoc, sps.max_frame_num(), sps.max_num_ref_frames as usize, meta, &mut outs);
|
||||
self.emit(outs);
|
||||
}
|
||||
|
||||
fn dispatch(&mut self, frame: FrameRef, slices: Vec<SliceJob>) {
|
||||
fn dispatch(&mut self, frame: FrameRef, slices: Vec<SliceJob>, draft: bool) {
|
||||
let shared = self.shared.clone();
|
||||
#[cfg(feature = "threads")]
|
||||
if let Some(pool) = &self.pool {
|
||||
|
|
@ -473,10 +506,10 @@ impl Decoder {
|
|||
}
|
||||
}
|
||||
self.in_flight.push_back(frame.clone());
|
||||
pool.spawn_fifo(move || run_job(frame, slices, &shared));
|
||||
pool.spawn_fifo(move || run_job(frame, slices, &shared, draft));
|
||||
return;
|
||||
}
|
||||
run_job(frame, slices, &shared);
|
||||
run_job(frame, slices, &shared, draft);
|
||||
}
|
||||
|
||||
fn emit(&mut self, outs: Vec<Output>) {
|
||||
|
|
@ -485,7 +518,7 @@ impl Decoder {
|
|||
}
|
||||
|
||||
/// Decode all slices of one picture and publish its rows.
|
||||
fn run_job(frame: FrameRef, slices: Vec<SliceJob>, shared: &Shared) {
|
||||
fn run_job(frame: FrameRef, slices: Vec<SliceJob>, shared: &Shared, draft: bool) {
|
||||
let reuse = shared.pool.lock().ok().and_then(|mut p| p.pop());
|
||||
let mut pic = match reuse {
|
||||
Some(mut p) if p.mb_w == frame.mb_w && p.mb_h == frame.mb_h() => {
|
||||
|
|
@ -494,6 +527,7 @@ fn run_job(frame: FrameRef, slices: Vec<SliceJob>, shared: &Shared) {
|
|||
}
|
||||
_ => PicState::new(frame.clone()),
|
||||
};
|
||||
pic.skip_deblock = draft;
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let mut first_err = None;
|
||||
for s in &slices {
|
||||
|
|
@ -521,6 +555,7 @@ fn run_job(frame: FrameRef, slices: Vec<SliceJob>, shared: &Shared) {
|
|||
};
|
||||
if let Ok(mut s) = shared.stats.lock() {
|
||||
s.pictures += 1;
|
||||
s.draft_pictures += draft as u64;
|
||||
for st in &pic.mbs {
|
||||
match st.kind {
|
||||
MbKind::I4x4 => s.mb_i4x4 += 1,
|
||||
|
|
@ -570,6 +605,7 @@ fn output_meta(sps: &Sps, pts: i64, key: bool) -> OutputMeta {
|
|||
transfer_characteristics: vui.transfer_characteristics,
|
||||
matrix_coefficients: vui.matrix_coefficients,
|
||||
sar: vui.sar,
|
||||
draft: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -598,5 +634,6 @@ fn make_picture(o: &Output) -> Picture {
|
|||
matrix: o.meta.matrix_coefficients,
|
||||
},
|
||||
sar: o.meta.sar,
|
||||
draft: o.meta.draft,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ pub struct OutputMeta {
|
|||
pub transfer_characteristics: u8,
|
||||
pub matrix_coefficients: u8,
|
||||
pub sar: (u16, u16),
|
||||
/// Decoded without deblocking (draft mode, non-reference picture).
|
||||
pub draft: bool,
|
||||
}
|
||||
|
||||
pub struct DpbEntry {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ mod synth_tests;
|
|||
mod tables;
|
||||
mod transform;
|
||||
|
||||
pub use decoder::{DecodeStats, Decoder};
|
||||
pub use decoder::{DecodeStats, Decoder, default_threads};
|
||||
pub use error::{Error, Result};
|
||||
|
||||
/// Colour description from the VUI (ITU-T H.273 code points).
|
||||
|
|
@ -77,4 +77,8 @@ pub struct Picture {
|
|||
pub color: ColorInfo,
|
||||
/// Sample aspect ratio (0, 0 when unspecified).
|
||||
pub sar: (u16, u16),
|
||||
/// Decoded in draft mode ([`Decoder::set_draft`]): a non-reference picture whose deblocking
|
||||
/// filter was skipped. Its samples are approximate (not the conforming output); no other
|
||||
/// picture is affected.
|
||||
pub draft: bool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ pub struct PicState {
|
|||
rows_recon: usize,
|
||||
rows_deblocked: usize,
|
||||
rows_published: usize,
|
||||
/// Draft mode for a non-reference picture: rows are published without deblocking.
|
||||
pub skip_deblock: bool,
|
||||
}
|
||||
|
||||
impl PicState {
|
||||
|
|
@ -58,6 +60,7 @@ impl PicState {
|
|||
rows_recon: 0,
|
||||
rows_deblocked: 0,
|
||||
rows_published: 0,
|
||||
skip_deblock: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +79,7 @@ impl PicState {
|
|||
self.rows_recon = 0;
|
||||
self.rows_deblocked = 0;
|
||||
self.rows_published = 0;
|
||||
self.skip_deblock = false;
|
||||
}
|
||||
|
||||
/// Record a decoded macroblock and deblock / publish rows that became final.
|
||||
|
|
@ -100,8 +104,10 @@ impl PicState {
|
|||
let deblock_limit = if finished { self.mb_h } else { self.rows_recon.saturating_sub(1) };
|
||||
while self.rows_deblocked < deblock_limit {
|
||||
let r = self.rows_deblocked;
|
||||
for addr in r * self.mb_w..(r + 1) * self.mb_w {
|
||||
deblock::deblock_mb(self, addr, self.mb_w);
|
||||
if !self.skip_deblock {
|
||||
for addr in r * self.mb_w..(r + 1) * self.mb_w {
|
||||
deblock::deblock_mb(self, addr, self.mb_w);
|
||||
}
|
||||
}
|
||||
self.rows_deblocked += 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -422,8 +422,15 @@ pub fn decode_file(path: &Path) -> Result<Vec<Picture>, DecodeFailure> {
|
|||
|
||||
/// Decode with `threads` worker threads (0 = decoder default).
|
||||
pub fn decode_file_threads(path: &Path, threads: usize) -> Result<Vec<Picture>, DecodeFailure> {
|
||||
decode_file_opts(path, threads, false)
|
||||
}
|
||||
|
||||
/// [`decode_file_threads`] with draft mode ([`Decoder::set_draft`]) on or off.
|
||||
pub fn decode_file_opts(path: &Path, threads: usize, draft: bool) -> Result<Vec<Picture>, DecodeFailure> {
|
||||
let data = std::fs::read(path).unwrap();
|
||||
let mut dec = if threads == 0 { Decoder::new() } else { Decoder::with_threads(threads) };
|
||||
assert!(!dec.draft(), "draft mode is off by default");
|
||||
dec.set_draft(draft);
|
||||
let mut out = Vec::new();
|
||||
for (i, au) in split_access_units(&data).into_iter().enumerate() {
|
||||
match dec.decode(au, i as i64) {
|
||||
|
|
@ -494,8 +501,9 @@ pub fn check_fixture(name: &str) -> Result<bool, String> {
|
|||
let f = fixture(name);
|
||||
let Some((h264, yuv)) = ensure(f) else { return Ok(false) };
|
||||
let reference = std::fs::read(&yuv).unwrap();
|
||||
// single-threaded and frame-threaded decoding must both be bit-exact
|
||||
for threads in [1, 0] {
|
||||
// single-threaded and frame-threaded decoding (an odd thread count and every core) must all
|
||||
// be bit-exact
|
||||
for threads in [1, 3, 0] {
|
||||
let pics = match decode_file_threads(&h264, threads) {
|
||||
Ok(p) => p,
|
||||
Err((au, e, partial)) => {
|
||||
|
|
@ -505,6 +513,9 @@ pub fn check_fixture(name: &str) -> Result<bool, String> {
|
|||
}
|
||||
};
|
||||
compare(&pics, &reference, f.width as usize, f.height as usize).map_err(|e| format!("{name} (threads {threads}): {e}"))?;
|
||||
if pics.iter().any(|p| p.draft) {
|
||||
return Err(format!("{name} (threads {threads}): draft picture without draft mode"));
|
||||
}
|
||||
check_pts(&pics).map_err(|e| format!("{name} (threads {threads}): {e}"))?;
|
||||
}
|
||||
Ok(true)
|
||||
|
|
|
|||
|
|
@ -56,6 +56,48 @@ fixture_tests!(
|
|||
vt_high_1080p,
|
||||
);
|
||||
|
||||
/// Draft mode (reduced-resolution playback) skips deblocking of non-reference pictures only:
|
||||
/// every other picture stays bit-exact with ffmpeg, single- and frame-threaded, the skipped ones
|
||||
/// are flagged, and they are the only ones that differ.
|
||||
#[test]
|
||||
fn draft_mode_changes_only_flagged_non_reference_pictures() {
|
||||
let mut checked = 0;
|
||||
for name in ["main_cabac_b", "bpyramid", "cavlc_b", "direct_temporal", "weightb"] {
|
||||
let f = common::fixture(name);
|
||||
let Some((h264, yuv)) = common::ensure(f) else {
|
||||
eprintln!("skipped {name}");
|
||||
continue;
|
||||
};
|
||||
let reference = std::fs::read(&yuv).unwrap();
|
||||
let (w, h) = (f.width as usize, f.height as usize);
|
||||
let fsize = w * h + 2 * w.div_ceil(2) * h.div_ceil(2);
|
||||
for threads in [1, 0] {
|
||||
let pics = common::decode_file_opts(&h264, threads, true).unwrap_or_else(|(au, e, _)| panic!("{name}: error at {au}: {e}"));
|
||||
common::check_pts(&pics).unwrap_or_else(|e| panic!("{name}: {e}"));
|
||||
assert_eq!(pics.len() * fsize, reference.len(), "{name}: picture count");
|
||||
let (mut draft, mut changed) = (0, 0);
|
||||
for (i, p) in pics.iter().enumerate() {
|
||||
let r = &reference[i * fsize..(i + 1) * fsize];
|
||||
let same = p.y == r[..w * h] && [&p.u[..], &p.v[..]].concat() == r[w * h..];
|
||||
if p.draft {
|
||||
draft += 1;
|
||||
changed += !same as usize;
|
||||
} else {
|
||||
assert!(same, "{name} (threads {threads}): picture {i} (not draft) differs from the reference");
|
||||
}
|
||||
}
|
||||
assert!(draft > 0, "{name}: has non-reference pictures");
|
||||
if name != "no_deblock" {
|
||||
assert!(changed > 0, "{name}: skipping deblocking changes some draft picture");
|
||||
}
|
||||
checked += 1;
|
||||
}
|
||||
}
|
||||
if checked == 0 {
|
||||
eprintln!("skipped: no fixtures");
|
||||
}
|
||||
}
|
||||
|
||||
/// Print which coding tools each fixture exercises (run with `-- --ignored --nocapture`).
|
||||
#[test]
|
||||
#[ignore]
|
||||
|
|
|
|||
Loading…
Reference in New Issue