//! Top-level decoder: NAL dispatch, picture boundaries, POC / RPS / DPB management and output. //! //! The calling thread parses parameter sets and slice headers and does all POC, RPS and DPB //! bookkeeping. The CTU layer of each picture runs as a job (on a thread pool with the `threads` //! feature) that publishes finished CTB rows into the picture's shared [`Frame`]; jobs of later pictures //! block per row on the reference data they read, so several pictures decode concurrently. use crate::dpb::{Dpb, Output, OutputMeta, PocState, RefPicSet, build_ref_lists}; use crate::error::{Error, Result, ensure, invalid}; use crate::params::{Layout, Pps, Sps, Vps}; use crate::picture::{Frame, FrameRef}; use crate::slice::{NalHeader, SliceHeader, nal_type}; use crate::slicedec::{F_INTRA, F_SKIP, PicState, SliceDecoder, SliceJob}; use crate::{ColorInfo, Picture, Plane}; use deckcraft_bitstream::{annexb_nals, length_prefixed_nals, unescape_rbsp}; use std::collections::VecDeque; use std::sync::{Arc, Mutex}; /// A picture whose slices are being collected. struct PendingPic { frame: FrameRef, sps: Arc, pps: Arc, layout: Arc, /// Header of the latest independent slice segment (for dependent slice segments). last_sh: SliceHeader, first_sh: SliceHeader, rps: RefPicSet, poc: i32, output: bool, meta: Arc, slices: Vec, } /// Counters describing what the decoder has seen (useful to check test coverage). #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct DecodeStats { pub pictures: u64, pub slices_i: u64, pub slices_p: u64, pub slices_b: u64, pub dependent_slices: u64, pub idr: u64, pub cra: u64, pub skipped_rasl: u64, pub intra_4x4_blocks: u64, pub skip_4x4_blocks: u64, pub inter_4x4_blocks: u64, pub weighted_slices: u64, pub tmvp_slices: u64, pub sao_slices: u64, pub deblock_disabled_slices: u64, pub tiles_pictures: u64, pub wpp_pictures: u64, pub scaling_list_pictures: u64, pub transform_skip_pictures: u64, pub long_term_slices: u64, pub missing_refs: u64, /// Pictures decoded in draft mode without in-loop filters ([`Decoder::set_draft`]). pub draft_pictures: u64, } #[derive(Default)] struct Shared { error: Mutex>, stats: Mutex, } /// H.265 / HEVC decoder. pub struct Decoder { vpss: Vec>>, spss: Vec>>, ppss: Vec>>, nal_length_size: Option, dpb: Dpb, poc_state: PocState, pending: Option, next_id: u32, active_sps: Option>, first_picture: bool, after_eos: bool, /// RASL pictures associated with the last IRAP are skipped (it had NoRaslOutputFlag = 1). skip_rasl: bool, layout_cache: Vec<(Arc, Arc, Arc, Option>>>)>, out_queue: VecDeque, shared: Arc, #[cfg(feature = "threads")] pool: Option, in_flight: VecDeque, max_in_flight: usize, draft: bool, } impl Default for Decoder { fn default() -> Self { Self::new() } } 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) } #[cfg(not(all(feature = "threads", not(target_arch = "wasm32"))))] { 1 } } impl Decoder { /// A decoder using all available cores (with the `threads` feature). pub fn new() -> Self { Self::with_threads(default_threads()) } /// A decoder using up to `threads` worker threads (1 = decode on the calling thread). pub fn with_threads(threads: usize) -> Self { #[cfg(feature = "threads")] let pool = if threads > 1 && cfg!(not(target_arch = "wasm32")) { rayon::ThreadPoolBuilder::new().num_threads(threads).thread_name(|i| format!("hevc-{i}")).build().ok() } else { None }; Decoder { vpss: vec![None; 16], spss: vec![None; 16], ppss: vec![None; 64], nal_length_size: None, dpb: Dpb::default(), poc_state: PocState::default(), pending: None, next_id: 1, active_sps: None, first_picture: true, after_eos: false, skip_rasl: false, layout_cache: Vec::new(), out_queue: VecDeque::new(), shared: Arc::new(Shared::default()), #[cfg(feature = "threads")] pool, in_flight: VecDeque::new(), max_in_flight: threads.max(1) + 2, draft: false, } } /// Configure from an `hvcC` (HEVCDecoderConfigurationRecord) box payload. pub fn from_hvcc(hvcc: &[u8]) -> Result { let mut d = Decoder::new(); d.configure_hvcc(hvcc)?; Ok(d) } /// Parse an `hvcC` record: parameter-set arrays and the NAL length size used by [`Decoder::decode`]. pub fn configure_hvcc(&mut self, hvcc: &[u8]) -> Result<()> { ensure!(hvcc.len() >= 23, "hvcC too short"); ensure!(hvcc[0] == 1, "unsupported hvcC version {}", hvcc[0]); self.nal_length_size = Some((hvcc[21] & 3) as usize + 1); let num_arrays = hvcc[22] as usize; let mut pos = 23; for _ in 0..num_arrays { ensure!(pos + 3 <= hvcc.len(), "hvcC truncated"); let n = u16::from_be_bytes([hvcc[pos + 1], hvcc[pos + 2]]) as usize; pos += 3; for _ in 0..n { ensure!(pos + 2 <= hvcc.len(), "hvcC truncated"); let len = u16::from_be_bytes([hvcc[pos], hvcc[pos + 1]]) as usize; pos += 2; ensure!(pos + len <= hvcc.len(), "hvcC truncated"); self.handle_nal(&hvcc[pos..pos + len], 0)?; pos += len; } } Ok(()) } /// Length of NAL length prefixes (from hvcC), or None for Annex-B input. pub fn nal_length_size(&self) -> Option { self.nal_length_size } /// Decode one access unit (all NAL units of one picture, Annex-B or length-prefixed per /// configuration; a byte stream with several complete access units is accepted too). Returns /// pictures that became ready for output, in output order; `pts` travels with the picture of this /// access unit through reordering. pub fn decode(&mut self, data: &[u8], pts: i64) -> Result> { let nals = match self.nal_length_size { Some(n) => length_prefixed_nals(data, n)?, None => annexb_nals(data), }; let mut result = Ok(()); for nal in nals { if let Err(e) = self.handle_nal(nal, pts) { result = Err(e); break; } } self.submit_pending(); if let Some(e) = self.take_error() { result = result.and(Err(e)); } result?; let max_queue = self.max_in_flight * 2; let mut pics = Vec::new(); while let Some(o) = self.out_queue.front() { if !o.frame.is_complete() && self.out_queue.len() <= max_queue { break; } let Some(o) = self.out_queue.pop_front() else { break }; pics.push(make_picture(&o)); } Ok(pics) } /// Output all remaining pictures (end of stream). pub fn flush(&mut self) -> Vec { self.submit_pending(); let mut outs = Vec::new(); self.dpb.flush(&mut outs); self.out_queue.extend(outs); let pics = self.out_queue.drain(..).map(|o| make_picture(&o)).collect(); while let Some(f) = self.in_flight.pop_front() { f.wait_complete(); } self.first_picture = true; pics } /// Statistics accumulated so far. pub fn stats(&self) -> DecodeStats { self.shared.stats.lock().map(|s| s.clone()).unwrap_or_default() } fn stat(&self, f: impl FnOnce(&mut DecodeStats)) { if let Ok(mut s) = self.shared.stats.lock() { f(&mut s); } } /// First error reported by a decoding job since the last call (also returned by /// [`Decoder::decode`]); useful after [`Decoder::flush`]. /// Draft mode for reduced-resolution playback (off by default): sub-layer non-reference /// pictures of the highest temporal sub-layer (pictures nothing predicts from) skip /// deblocking and SAO and come out flagged [`Picture::draft`]; every other picture is /// unchanged. pub fn set_draft(&mut self, on: bool) { self.draft = on; } pub fn take_error(&mut self) -> Option { self.shared.error.lock().ok().and_then(|mut e| e.take()) } fn handle_nal(&mut self, nal: &[u8], pts: i64) -> Result<()> { if nal.len() < 2 { return Ok(()); } let hdr = NalHeader::parse(nal)?; if hdr.layer_id != 0 { return Ok(()); } match hdr.nal_type { nal_type::VPS => { let v = Vps::parse(&unescape_rbsp(&nal[2..]))?; let id = v.id as usize; self.vpss[id] = Some(Arc::new(v)); } nal_type::SPS => { let s = Sps::parse(&unescape_rbsp(&nal[2..]))?; let id = s.id as usize; self.spss[id] = Some(Arc::new(s)); } nal_type::PPS => { let p = Pps::parse(&unescape_rbsp(&nal[2..]))?; let id = p.id as usize; self.ppss[id] = Some(Arc::new(p)); } nal_type::EOS | nal_type::EOB => { self.submit_pending(); self.after_eos = true; } 0..=9 | 16..=21 => self.handle_slice(nal, hdr, pts)?, // AUD, SEI, filler, reserved and unspecified NAL units are skipped _ => {} } Ok(()) } fn layout_for(&mut self, pps: &Arc, sps: &Arc) -> Result<(Arc, Option>>>)> { if let Some((_, _, l, s)) = self.layout_cache.iter().find(|(p, s, _, _)| Arc::ptr_eq(p, pps) && Arc::ptr_eq(s, sps)) { return Ok((l.clone(), s.clone())); } let layout = Arc::new(Layout::new(sps, pps)?); let scaling = if sps.scaling_list_enabled { let list = pps.scaling_list.as_ref().or(sps.scaling_list.as_ref()).cloned().unwrap_or_else(crate::params::ScalingList::default_lists); let mut v = Vec::with_capacity(24); for size in 0..4 { for m in 0..6 { v.push(list.factor(size, m)); } } Some(Arc::new(v)) } else { None }; if self.layout_cache.len() > 8 { self.layout_cache.remove(0); } self.layout_cache.push((pps.clone(), sps.clone(), layout.clone(), scaling.clone())); Ok((layout, scaling)) } fn handle_slice(&mut self, nal: &[u8], hdr: NalHeader, pts: i64) -> Result<()> { let rbsp = unescape_rbsp(&nal[2..]); // a first_slice_segment_in_pic_flag of 1 starts a new picture let first = rbsp.first().is_some_and(|b| b & 0x80 != 0); if first { self.submit_pending(); } if hdr.is_rasl() && (self.skip_rasl || self.first_picture) { if first { self.stat(|s| s.skipped_rasl += 1); } return Ok(()); } let ppss = &self.ppss; let spss = &self.spss; let prev = self.pending.as_ref().map(|p| &p.last_sh); let (sh, pps, sps) = SliceHeader::parse( &rbsp, hdr, |id| { let pps = ppss[id as usize].as_ref().ok_or_else(|| Error::MissingParameterSet(format!("PPS {id}")))?; let sps = spss[pps.sps_id as usize].as_ref().ok_or_else(|| Error::MissingParameterSet(format!("SPS {}", pps.sps_id)))?; Ok((pps.clone(), sps.clone())) }, prev, )?; sps.check_supported()?; pps.check_supported()?; if first { self.start_picture(&sh, &sps, &pps, pts)?; } else { let Some(p) = &self.pending else { return invalid("slice segment without the first slice of its picture") }; ensure!(Arc::ptr_eq(&p.pps, &pps), "PPS changed within a picture"); } let Some(pending) = self.pending.as_mut() else { return invalid("slice segment without the first slice of its picture"); }; let refs = build_ref_lists(&sh, &pending.rps)?; if !sh.dependent { pending.last_sh = sh.clone(); } let (st, dependent, weighted, tmvp, sao, dbk_off, lt) = (sh.slice_type, sh.dependent, sh.pwt.is_some(), sh.temporal_mvp, sh.sao_luma || sh.sao_chroma, sh.deblocking_disabled, !sh.lt.is_empty()); pending.slices.push(SliceJob { sh, pps, sps, rbsp, refs }); self.stat(|s| { match st { crate::slice::SliceType::I => s.slices_i += 1, crate::slice::SliceType::P => s.slices_p += 1, crate::slice::SliceType::B => s.slices_b += 1, } s.dependent_slices += dependent as u64; s.weighted_slices += weighted as u64; s.tmvp_slices += tmvp as u64; s.sao_slices += sao as u64; s.deblock_disabled_slices += dbk_off as u64; s.long_term_slices += lt as u64; }); Ok(()) } fn start_picture(&mut self, sh: &SliceHeader, sps: &Arc, pps: &Arc, pts: i64) -> Result<()> { ensure!(sh.first_slice_segment_in_pic && !sh.dependent, "invalid first slice segment"); let hdr = sh.nal; let changed = match &self.active_sps { None => true, Some(a) => { a.width != sps.width || a.height != sps.height || a.bit_depth_luma != sps.bit_depth_luma || a.bit_depth_chroma != sps.bit_depth_chroma || a.log2_ctb != sps.log2_ctb } }; let mut outs = Vec::new(); if changed && self.active_sps.is_some() { self.dpb.flush(&mut outs); } self.active_sps = Some(sps.clone()); self.dpb.max_dec_pic_buffering = sps.max_dec_pic_buffering as usize; self.dpb.max_num_reorder = sps.max_num_reorder as usize; self.dpb.max_latency = if sps.max_latency_increase_plus1 != 0 { sps.max_num_reorder + sps.max_latency_increase_plus1 - 1 } else { 0 }; let irap_no_rasl = hdr.is_irap() && (hdr.is_idr() || hdr.is_bla() || self.first_picture || self.after_eos); if hdr.is_irap() { self.skip_rasl = irap_no_rasl; self.stat(|s| { if hdr.is_idr() { s.idr += 1; } else { s.cra += 1; } }); } let max_lsb = sps.max_poc_lsb(); let poc = self.poc_state.compute(sh, max_lsb, irap_no_rasl); let (w, h) = (sps.width as usize, sps.height as usize); let template = Frame::new(0, 0, w, h, sps.log2_ctb, sps.bit_depth_luma, sps.bit_depth_chroma); let next_id = &mut self.next_id; let mut missing = 0u64; let mut make_missing = |p: i32| -> FrameRef { missing += 1; let id = *next_id; *next_id += 1; Arc::new(Frame::gray(id, p, &template)) }; let rps = self.dpb.apply_rps(sh, poc, max_lsb, irap_no_rasl, &mut make_missing)?; if missing > 0 { self.stat(|s| s.missing_refs += missing); } if irap_no_rasl && !self.first_picture { if sh.no_output_of_prior_pics && hdr.is_idr() { self.dpb.clear(); } else { self.dpb.flush(&mut outs); } } else { self.dpb.bump_before_decode(&mut outs); } self.out_queue.extend(outs); let (layout, scaling) = self.layout_for(pps, sps)?; let _ = scaling; let id = self.next_id; self.next_id += 1; let frame = Arc::new(Frame::new(id, poc, w, h, sps.log2_ctb, sps.bit_depth_luma, sps.bit_depth_chroma)); let output = sh.pic_output && !(hdr.is_rasl() && self.skip_rasl); // Draft mode: a sub-layer non-reference picture of the highest sub-layer is never used // for prediction, so its in-loop filters can be left out without changing anything else. let draft = self.draft && hdr.is_sub_layer_non_ref() && hdr.temporal_id as u32 >= sps.max_sub_layers_minus1; if draft { self.stat(|s| s.draft_pictures += 1); } let meta = Arc::new(output_meta(sps, pts, hdr.is_irap(), draft)); self.pending = Some(PendingPic { frame, sps: sps.clone(), pps: pps.clone(), layout, last_sh: sh.clone(), first_sh: sh.clone(), rps, poc, output, meta, slices: Vec::new(), }); self.first_picture = false; self.after_eos = false; self.stat(|s| { s.tiles_pictures += pps.tiles_enabled as u64; s.wpp_pictures += pps.entropy_coding_sync as u64; s.scaling_list_pictures += sps.scaling_list_enabled as u64; s.transform_skip_pictures += pps.transform_skip as u64; }); Ok(()) } /// Start decoding the pending picture and insert it into the DPB. fn submit_pending(&mut self) { let Some(p) = self.pending.take() else { return }; let PendingPic { frame, sps, pps, layout, first_sh, poc, output, meta, slices, .. } = p; let scaling = self.layout_cache.iter().find(|(pp, ss, _, _)| Arc::ptr_eq(pp, &pps) && Arc::ptr_eq(ss, &sps)).and_then(|(_, _, _, s)| s.clone()); self.dispatch(frame.clone(), sps, pps, layout, scaling, slices, meta.draft); self.poc_state.update(&first_sh, poc); let mut outs = Vec::new(); self.dpb.insert(frame, poc, output, meta, &mut outs); self.out_queue.extend(outs); } #[allow(clippy::too_many_arguments)] fn dispatch( &mut self, frame: FrameRef, sps: Arc, pps: Arc, layout: Arc, scaling: Option>>>, slices: Vec, draft: bool, ) { let shared = self.shared.clone(); #[cfg(feature = "threads")] if let Some(pool) = &self.pool { self.in_flight.retain(|f| !f.is_complete()); while self.in_flight.len() >= self.max_in_flight { if let Some(f) = self.in_flight.pop_front() { f.wait_complete(); } } self.in_flight.push_back(frame.clone()); pool.spawn_fifo(move || run_job(frame, sps, pps, layout, scaling, slices, draft, &shared)); return; } run_job(frame, sps, pps, layout, scaling, slices, draft, &shared); } } /// Decode all slices of one picture and publish its rows. #[allow(clippy::too_many_arguments)] fn run_job( frame: FrameRef, sps: Arc, pps: Arc, layout: Arc, scaling: Option>>>, slices: Vec, draft: bool, shared: &Shared, ) { let mut pic = PicState::new(frame.clone(), sps, pps, layout, scaling); pic.draft = draft; let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { let mut first_err = None; for s in &slices { if let Err(e) = SliceDecoder::decode(&mut pic, s) { first_err.get_or_insert(e); } } crate::filter::finish(&mut pic); first_err })); let err = match result { Ok(e) => e, Err(_) => { // make sure readers never block on this frame let gray = Frame::gray(0, frame.poc, &frame); for r in 0..frame.num_rows() { if !frame.is_published(r) { let row = gray.row(r); frame.publish(r, crate::picture::FrameRow { y: row.y.clone(), cb: row.cb.clone(), cr: row.cr.clone(), col: row.col.clone() }); } } Some(Error::Invalid("internal error while decoding a picture".into())) } }; if let Ok(mut s) = shared.stats.lock() { s.pictures += 1; for b in &pic.blk { if b.flags & F_INTRA != 0 { s.intra_4x4_blocks += 1; } else if b.flags & F_SKIP != 0 { s.skip_4x4_blocks += 1; } else if b.flags != 0 { s.inter_4x4_blocks += 1; } } } if let (Some(e), Ok(mut slot)) = (err, shared.error.lock()) { slot.get_or_insert(e); } } fn output_meta(sps: &Sps, pts: i64, key: bool, draft: bool) -> OutputMeta { let vui = sps.vui.clone().unwrap_or_default(); OutputMeta { pts, key, crop: sps.crop_rect(), full_range: vui.full_range, colour_primaries: vui.colour_primaries, transfer_characteristics: vui.transfer_characteristics, matrix_coefficients: vui.matrix_coefficients, sar: vui.sar, bit_depth: sps.bit_depth_luma, draft, } } fn make_picture(o: &Output) -> Picture { let (cx, cy, cw, ch) = o.meta.crop; let (cx, cy, cw, ch) = (cx as usize, cy as usize, cw as usize, ch as usize); let (y, u, v) = o.frame.copy_cropped((cx, cy, cw, ch)); let (ccw, cch) = (cw.div_ceil(2), ch.div_ceil(2)); let bd = o.meta.bit_depth; let conv = |p: Vec| if bd <= 8 { Plane::U8(p.into_iter().map(|v| v as u8).collect()) } else { Plane::U16(p) }; Picture { width: cw as u32, height: ch as u32, chroma_width: ccw as u32, chroma_height: cch as u32, bit_depth: bd, y: conv(y), u: conv(u), v: conv(v), y_stride: cw, uv_stride: ccw, pts: o.meta.pts, poc: o.poc, key: o.meta.key, color: ColorInfo { full_range: o.meta.full_range, primaries: o.meta.colour_primaries, transfer: o.meta.transfer_characteristics, matrix: o.meta.matrix_coefficients, }, sar: o.meta.sar, draft: o.meta.draft, } }