From d65ed5925d4357315a295961ddbdc9e6b5c4f499 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 08:42:11 +0100 Subject: [PATCH 01/17] refactor(decoder): move Annex B splitting into a shared module --- src/decoder/annexb.rs | 98 ++++++++++++++++++++++++++++++++++++++ src/decoder/common.rs | 10 +--- src/decoder/h264/parser.rs | 56 +++------------------- src/decoder/mod.rs | 2 + 4 files changed, 107 insertions(+), 59 deletions(-) create mode 100644 src/decoder/annexb.rs diff --git a/src/decoder/annexb.rs b/src/decoder/annexb.rs new file mode 100644 index 00000000..b403d827 --- /dev/null +++ b/src/decoder/annexb.rs @@ -0,0 +1,98 @@ +//! Annex B byte streams, as H.264 and H.265 both use them: NAL units separated +//! by 3- or 4-byte start codes. +//! +//! Only the splitting lives here. The NAL header differs between the two +//! codecs (one byte in H.264, two in H.265), so each codec parses its own. + +/// Annex B start code prefixed to each slice handed to the driver. +/// +/// Three bytes, not four. The leading zero byte of a 4-byte start code is legal +/// Annex B, and RADV decodes either form, but Intel's ANV decodes almost nothing +/// when a slice offset points at that extra zero: it reports no error and +/// returns a picture with a handful of macroblocks in it. ffmpeg's Vulkan +/// decoder uses three bytes for the same reason. +pub(crate) const START_CODE: [u8; 3] = [0, 0, 1]; + +/// Iterate over the NAL units in an Annex B byte stream. +/// +/// Yields each NAL's start-code offset within `data` together with the NAL +/// unit itself (header and EBSP payload, without the start code). Trailing +/// zero bytes before the next start code are trimmed, and empty NAL units are +/// skipped. +pub(crate) fn iter_nal_units(data: &[u8]) -> impl Iterator { + NalIterator { data, pos: 0 } +} + +struct NalIterator<'a> { + data: &'a [u8], + pos: usize, +} + +/// Find the next start code at or after `from`. Returns (start_code_pos, payload_pos). +fn find_start_code(data: &[u8], from: usize) -> Option<(usize, usize)> { + let mut i = from; + while i + 3 <= data.len() { + if data[i] == 0 && data[i + 1] == 0 { + if data[i + 2] == 1 { + return Some((i, i + 3)); + } + if i + 4 <= data.len() && data[i + 2] == 0 && data[i + 3] == 1 { + return Some((i, i + 4)); + } + } + i += 1; + } + None +} + +impl<'a> Iterator for NalIterator<'a> { + type Item = (usize, &'a [u8]); + + fn next(&mut self) -> Option<(usize, &'a [u8])> { + loop { + let (start_code_pos, payload_start) = find_start_code(self.data, self.pos)?; + let end = match find_start_code(self.data, payload_start) { + Some((next_start, _)) => next_start, + None => self.data.len(), + }; + self.pos = end; + + // Trim trailing zero padding before the next start code. + let mut trimmed_end = end; + while trimmed_end > payload_start && self.data[trimmed_end - 1] == 0 { + trimmed_end -= 1; + } + if trimmed_end > payload_start { + return Some((start_code_pos, &self.data[payload_start..trimmed_end])); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn splits_on_start_codes_and_trims_zero_padding() { + let stream = [ + 0, 0, 0, 1, 0x67, 0xAA, 0, 0, 1, 0x68, 0xBB, 0, 0, 0, 0, 1, 0x65, + ]; + let nals: Vec<_> = iter_nal_units(&stream).collect(); + assert_eq!( + nals, + vec![ + (0, &[0x67, 0xAA][..]), + (6, &[0x68, 0xBB][..]), + (12, &[0x65][..]), + ] + ); + } + + #[test] + fn skips_empty_nal_units() { + let stream = [0, 0, 1, 0, 0, 1, 0x41]; + let nals: Vec<_> = iter_nal_units(&stream).collect(); + assert_eq!(nals, vec![(3, &[0x41][..])]); + } +} diff --git a/src/decoder/common.rs b/src/decoder/common.rs index d618365c..651de28b 100644 --- a/src/decoder/common.rs +++ b/src/decoder/common.rs @@ -22,6 +22,7 @@ use std::sync::Arc; use ash::vk; use crate::decoder::DecodedFrame; +use crate::decoder::annexb::START_CODE; use crate::decoder::frames::SlotPins; use crate::decoder::pipeline::DecodePipeline; use crate::encoder::{BitDepth, ChromaFormat}; @@ -32,15 +33,6 @@ use crate::video::{ }; use crate::vulkan::VideoContext; -/// Annex B start code prefixed to each slice handed to the driver. -/// -/// Three bytes, not four. The leading zero byte of a 4-byte start code is legal -/// Annex B, and RADV decodes either form, but Intel's ANV decodes almost nothing -/// when a slice offset points at that extra zero: it reports no error and -/// returns a picture with a handful of macroblocks in it. ffmpeg's Vulkan -/// decoder uses three bytes for the same reason. -pub(crate) const START_CODE: [u8; 3] = [0, 0, 1]; - /// Query decode capabilities for a profile. /// /// `caps` is supplied by the caller already chained with the codec-specific diff --git a/src/decoder/h264/parser.rs b/src/decoder/h264/parser.rs index 66559f4a..9850a723 100644 --- a/src/decoder/h264/parser.rs +++ b/src/decoder/h264/parser.rs @@ -11,6 +11,7 @@ //! Accordingly the slice-header parser stops right after the POC syntax //! elements; nothing later in the header is needed on the host side. +use crate::decoder::annexb; use crate::decoder::bitreader::BitReader; use crate::error::{PixelForgeError, Result}; @@ -78,62 +79,17 @@ pub fn iter_nal_units(data: &[u8]) -> impl Iterator> { pub(crate) fn iter_nal_units_with_offsets( data: &[u8], ) -> impl Iterator)> { - NalIterator { data, pos: 0 } -} - -struct NalIterator<'a> { - data: &'a [u8], - pos: usize, -} - -/// Find the next start code at or after `from`. Returns (start_code_pos, payload_pos). -fn find_start_code(data: &[u8], from: usize) -> Option<(usize, usize)> { - let mut i = from; - while i + 3 <= data.len() { - if data[i] == 0 && data[i + 1] == 0 { - if data[i + 2] == 1 { - return Some((i, i + 3)); - } - if i + 4 <= data.len() && data[i + 2] == 0 && data[i + 3] == 1 { - return Some((i, i + 4)); - } - } - i += 1; - } - None -} - -impl<'a> Iterator for NalIterator<'a> { - type Item = (usize, NalUnit<'a>); - - fn next(&mut self) -> Option<(usize, NalUnit<'a>)> { - let (start_code_pos, payload_start) = find_start_code(self.data, self.pos)?; - let end = match find_start_code(self.data, payload_start) { - Some((next_start, _)) => next_start, - None => self.data.len(), - }; - self.pos = end; - - // Trim trailing zero padding before the next start code. - let mut trimmed_end = end; - while trimmed_end > payload_start && self.data[trimmed_end - 1] == 0 { - trimmed_end -= 1; - } - if trimmed_end <= payload_start { - return self.next(); - } - - let nal = &self.data[payload_start..trimmed_end]; + annexb::iter_nal_units(data).map(|(offset, nal)| { let header = nal[0]; - Some(( - start_code_pos, + ( + offset, NalUnit { nal_type: NalType::from_raw(header & 0x1F), ref_idc: (header >> 5) & 0x3, data: nal, }, - )) - } + ) + }) } /// Parsed H.264 sequence parameter set (the subset Vulkan decode needs). diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 6c29707a..4ae889f9 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -38,6 +38,8 @@ //! stream this crate's encoder produces — and typical output from x264, NVENC //! and friends, including B-pyramid — decodes correctly. +/// Annex B start-code framing, shared by H.264 and H.265. +pub(crate) mod annexb; pub(crate) mod bitreader; /// The Vulkan video session and the per-decoder state every codec shares. pub(crate) mod common; From b04c02b91d11d3cf9b6dd4943a506dc53f0566eb Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 08:44:13 +0100 Subject: [PATCH 02/17] refactor(decoder): plan decode sessions in a codec-neutral module --- src/decoder/common.rs | 94 +++-------- src/decoder/h264/session.rs | 239 +++----------------------- src/decoder/mod.rs | 2 + src/decoder/plan.rs | 329 ++++++++++++++++++++++++++++++++++++ 4 files changed, 378 insertions(+), 286 deletions(-) create mode 100644 src/decoder/plan.rs diff --git a/src/decoder/common.rs b/src/decoder/common.rs index 651de28b..71ceb5ed 100644 --- a/src/decoder/common.rs +++ b/src/decoder/common.rs @@ -25,6 +25,7 @@ use crate::decoder::DecodedFrame; use crate::decoder::annexb::START_CODE; use crate::decoder::frames::SlotPins; use crate::decoder::pipeline::DecodePipeline; +use crate::decoder::plan::SessionPlan; use crate::encoder::{BitDepth, ChromaFormat}; use crate::error::{PixelForgeError, Result}; use crate::video::{ @@ -33,75 +34,6 @@ use crate::video::{ }; use crate::vulkan::VideoContext; -/// Query decode capabilities for a profile. -/// -/// `caps` is supplied by the caller already chained with the codec-specific -/// capability struct, since only the codec knows which one applies. -pub(crate) fn query_decode_caps( - context: &VideoContext, - profile_info: &vk::VideoProfileInfoKHR, - caps: &mut vk::VideoCapabilitiesKHR, -) -> Result<()> { - let video_queue_instance = - ash::khr::video_queue::Instance::load(context.entry(), context.instance()); - let result = unsafe { - (video_queue_instance - .fp() - .get_physical_device_video_capabilities_khr)( - context.physical_device(), - profile_info, - caps, - ) - }; - if result != vk::Result::SUCCESS { - return Err(PixelForgeError::NoSuitableDevice(format!( - "decode profile not supported: {:?}", - result - ))); - } - Ok(()) -} - -/// What a codec decided the session should look like. -/// -/// The codec derives these from its own parameter sets and the device caps; the -/// generic layer turns them into Vulkan objects. -pub(crate) struct SessionPlan { - /// Coded dimensions, already aligned to the device's picture granularity. - pub coded_width: u32, - pub coded_height: u32, - pub picture_format: vk::Format, - pub bit_depth: BitDepth, - pub chroma_format: ChromaFormat, - /// DPB slots: references, the picture being decoded, and `spare_slots`. - pub slot_count: usize, - /// How many of `slot_count` are spare capacity for pictures pinned past the - /// decode that produced them: those held for display-order reordering, and - /// those handed to the caller. Zero means every emitted picture has to be - /// copied out instead of pinned in place. - pub spare_slots: usize, - /// References the driver may use for a single picture, which is - /// `slot_count` minus the current picture and the output reservation. - pub max_active_references: u32, - /// Driver decodes straight into the DPB image (`DPB_AND_OUTPUT_COINCIDE`). - pub coincide: bool, - /// A decoded picture may be handed to the caller in place, pinned in its - /// DPB slot, rather than copied out. See [`DecodeSession::pinnable`]. - pub pinnable: bool, - /// `dpb_usage` includes `SAMPLED`, so handed-out frames can be sampled. - pub sampleable: bool, - /// Picture images are created with `MUTABLE_FORMAT`, so a consumer can view - /// their planes separately. See [`DecodeSession::plane_views`]. - pub plane_views: bool, - /// Creation flags for picture images, from what the driver reported. - pub image_flags: vk::ImageCreateFlags, - /// Formats views of a picture image may use. Empty unless `plane_views`. - pub view_formats: Vec, - /// One layered DPB image rather than one image per slot. - pub use_layered_dpb: bool, - pub dpb_usage: vk::ImageUsageFlags, -} - /// One DPB image and everything that lives and dies with it. /// /// Held behind an `Arc` shared with every frame decoded into it, so a session @@ -375,10 +307,11 @@ impl DecoderCommon { &mut self, plan: &SessionPlan, profile_info: &vk::VideoProfileInfoKHR, - std_header_version: &vk::ExtensionProperties, make_params: impl FnOnce(&Self, vk::VideoSessionKHR) -> Result, ) -> Result<()> { self.destroy_session(); + self.bitstream_offset_alignment = plan.bitstream_offset_alignment; + self.bitstream_size_alignment = plan.bitstream_size_alignment; let session_create_info = vk::VideoSessionCreateInfoKHR::default() .queue_family_index(self.decode_queue_family) @@ -392,7 +325,7 @@ impl DecoderCommon { .reference_picture_format(plan.picture_format) .max_dpb_slots(plan.slot_count as u32) .max_active_reference_pictures(plan.max_active_references) - .std_header_version(std_header_version); + .std_header_version(&plan.std_header_version); let session = unsafe { self.video_queue_fn @@ -491,6 +424,25 @@ impl DecoderCommon { plane_views: plan.plane_views, output_image, }); + + tracing::debug!( + "{} decode session: {}x{} {:?}, {} DPB slots ({} spare), \ + layered={}, coincide={}, pinnable={}, sampleable={}, \ + plane_views={}, bitstream alignment {}/{} (offset/size)", + plan.codec, + plan.coded_width, + plan.coded_height, + plan.picture_format, + plan.slot_count, + plan.spare_slots, + plan.use_layered_dpb, + plan.coincide, + plan.pinnable, + plan.sampleable, + plan.plane_views, + plan.bitstream_offset_alignment, + plan.bitstream_size_alignment + ); Ok(()) } diff --git a/src/decoder/h264/session.rs b/src/decoder/h264/session.rs index 89a26919..35ea8120 100644 --- a/src/decoder/h264/session.rs +++ b/src/decoder/h264/session.rs @@ -7,7 +7,7 @@ use ash::vk; use ash::vk::TaggedStructure; use tracing::debug; -use crate::decoder::common::{DecoderCommon, SessionPlan, query_decode_caps}; +use crate::decoder::common::DecoderCommon; use crate::decoder::frames::{DecodedPicture, ReorderBuffer}; use crate::decoder::h264::dpb::{DecodeDpb, PictureState}; use crate::decoder::h264::parser::{ @@ -15,10 +15,10 @@ use crate::decoder::h264::parser::{ }; use crate::decoder::h264::record::{DecodeRecord, LinkParams}; use crate::decoder::pipeline::DECODE_PIPELINE_DEPTH; +use crate::decoder::plan::StreamNeeds; use crate::decoder::{DecodeConfig, DecodeStatus, Framing}; use crate::encoder::{BitDepth, ChromaFormat}; use crate::error::{PixelForgeError, Result}; -use crate::video::{align_up, get_video_format, query_supported_video_formats}; use crate::vulkan::VideoContext; /// H.264 decoder. @@ -259,209 +259,35 @@ impl H264Decoder { .chroma_bit_depth(depth_flag) .push(&mut profile); - // Query capabilities for this exact profile. The chain borrows the - // codec caps structs, so scope it and copy the results out. - let mut h264_caps = vk::VideoDecodeH264CapabilitiesKHR::default(); - let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default(); - let ( - max_coded_extent, - picture_access_granularity, - max_dpb_slots, - max_active_reference_pictures, - cap_flags, - std_header_version, - min_bitstream_buffer_offset_alignment, - min_bitstream_buffer_size_alignment, - ) = { - let mut caps = vk::VideoCapabilitiesKHR::default() - .push(&mut h264_caps) - .push(&mut decode_caps); - query_decode_caps(&self.common.context, &profile_info, &mut caps)?; - ( - caps.max_coded_extent, - caps.picture_access_granularity, - caps.max_dpb_slots, - caps.max_active_reference_pictures, - caps.flags, - caps.std_header_version, - caps.min_bitstream_buffer_offset_alignment, - caps.min_bitstream_buffer_size_alignment, - ) - }; - let decode_flags = decode_caps.flags; - - let coded_width = align_up(sps.coded_width(), picture_access_granularity.width.max(1)); - let coded_height = align_up(sps.coded_height(), picture_access_granularity.height.max(1)); - if coded_width > max_coded_extent.width || coded_height > max_coded_extent.height { - return Err(PixelForgeError::InvalidInput(format!( - "H.264 decode: stream is {}x{}, device maximum is {}x{}", - coded_width, coded_height, max_coded_extent.width, max_coded_extent.height - ))); - } - - self.common.bitstream_offset_alignment = min_bitstream_buffer_offset_alignment.max(1); - self.common.bitstream_size_alignment = min_bitstream_buffer_size_alignment.max(1); - - let coincide = - decode_flags.contains(vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_COINCIDE); - debug!("H.264 decode capability flags: {:?}", decode_flags); - let use_layered_dpb = - !cap_flags.contains(vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES); - // The stream needs max_num_ref_frames references plus the current - // picture. On top of that come the spare slots, which is what lets a - // picture stay pinned in place past the decode that produced it instead - // of being copied out: `reorder_depth` of them for pictures waiting - // their turn in display order, and `output_depth` for frames the caller - // is holding. The device caps bound the total. - let required = (sps.max_num_ref_frames as u32 + 1).max(2); - let slot_limit = max_dpb_slots.min(crate::decoder::h264::dpb::MAX_DPB_SLOTS as u32); - if required > slot_limit { - return Err(PixelForgeError::InvalidInput(format!( - "H.264 decode: stream needs {} DPB slots, device supports {}", - required, max_dpb_slots - ))); - } - // Reorder depth comes from the stream (VUI); without it, bound by the - // reference count, which is never smaller than the reorder depth in - // practice. + // picture. Reorder depth comes from the stream (VUI); without it, bound + // by the reference count, which is never smaller than the reorder depth + // in practice. let reorder_depth = sps .max_num_reorder_frames .map(|v| v as usize) .unwrap_or(sps.max_num_ref_frames as usize); - // Spare slots only buy something when pictures can be pinned. Without - // that they are handed out as copies, which needs no DPB slot at all, - // so asking for spares would just reserve memory nothing uses. - let pinnable = coincide && self.common.context.has_unified_image_layouts(); - let wanted_spare = if pinnable { - reorder_depth + self.output_depth - } else { - 0 - }; - let slot_count = (required + wanted_spare as u32).min(slot_limit) as usize; - let spare_slots = slot_count - required as usize; - if spare_slots < wanted_spare { - debug!( - "H.264 decode: {} of {} spare DPB slots available \ - (reorder depth {} plus {} output slots; the stream's references \ - need {} of the device's {}); pictures beyond that are copied", - spare_slots, - wanted_spare, - reorder_depth, - self.output_depth, - required, - max_dpb_slots - ); - } - - // Only the reference pictures are ever active at once: the current - // picture and the output reservation are not references. - let max_active_references = (required - 1).min(max_active_reference_pictures); - if max_active_references < required - 1 { - return Err(PixelForgeError::InvalidInput(format!( - "H.264 decode: stream needs {} active reference pictures, device supports {}", - required - 1, - max_active_reference_pictures - ))); - } - - // Pick a picture format the device actually supports for decode. - // - // Ask for SAMPLED as well: a coincident DPB image is what the caller - // receives, and SAMPLED is what lets them read it in a shader instead - // of copying it out first. Fall back to the plain usage if the device - // reports no sampleable format, which costs the caller a copy but keeps - // decoding working. With a distinct output image nothing asks, since - // those pictures reach the caller as pool copies, which are sampleable - // whatever the device says about video images. - let base_usage = if coincide { - vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR - | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR - | vk::ImageUsageFlags::TRANSFER_SRC - } else { - vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR - }; - let sampled_usage = base_usage | vk::ImageUsageFlags::SAMPLED; - let sampled_formats = if coincide { - query_supported_video_formats(&self.common.context, &profile_info, sampled_usage) - .unwrap_or_default() - } else { - Vec::new() - }; - let (dpb_usage, sampleable, supported) = if sampled_formats.is_empty() { - let formats = - query_supported_video_formats(&self.common.context, &profile_info, base_usage)?; - (base_usage, false, formats) - } else { - (sampled_usage, true, sampled_formats) - }; - let preferred = get_video_format(chroma_format, bit_depth); - let chosen = supported - .iter() - .find(|f| f.format == preferred) - .or_else(|| supported.first()) - .copied() - .ok_or_else(|| { - PixelForgeError::NoSuitableDevice( - "H.264 decode: no supported picture format for this profile".to_string(), - ) - })?; - let picture_format = chosen.format; - - // MUTABLE_FORMAT lets a consumer view the picture's planes separately, - // which is the only way to read a multi-planar image without a - // sampler-YCbCr conversion, and the only way at all for a renderer - // whose shaders come from a toolchain that cannot express one (naga and - // therefore wgpu, among others). It is opportunistic: the driver - // enumerates which creation flags it accepts for this profile, format - // and usage, and DPB-only images on both RADV and ANV accept none. - // - // Naming the plane formats keeps a driver from having to assume the - // image might be reinterpreted as anything, which is what would - // otherwise cost it compression. - let plane_views = chosen - .create_flags - .contains(vk::ImageCreateFlags::MUTABLE_FORMAT); - let image_flags = if plane_views { - vk::ImageCreateFlags::MUTABLE_FORMAT - } else { - vk::ImageCreateFlags::empty() - }; - let view_formats: Vec = if plane_views { - let mut list = vec![picture_format]; - list.extend(crate::video::plane_view_formats(picture_format)); - list - } else { - Vec::new() - }; - - // Hand the shared layer everything it needs to build the session; it - // owns the Vulkan objects, this module owns only the H.264 reasoning - // that produced these numbers. - let plan = SessionPlan { - coded_width, - coded_height, - picture_format, - bit_depth, + let needs = StreamNeeds { + codec: "H.264", + width: sps.coded_width(), + height: sps.coded_height(), chroma_format, - slot_count, - spare_slots, - max_active_references, - coincide, - pinnable, - sampleable, - plane_views, - image_flags, - view_formats, - use_layered_dpb, - dpb_usage, + bit_depth, + references: sps.max_num_ref_frames as u32, + active_references: sps.max_num_ref_frames as u32, + slot_limit: crate::decoder::h264::dpb::MAX_DPB_SLOTS as u32, + reorder_depth, + output_depth: self.output_depth, }; - self.common.create_session( - &plan, - &profile_info, - &std_header_version, - |common, session| Self::build_session_params(common, session, sps, pps), - )?; + let mut h264_caps = vk::VideoDecodeH264CapabilitiesKHR::default(); + let plan = self + .common + .plan_session(&profile_info, &mut h264_caps, &needs)?; + let slot_count = plan.slot_count; + self.common + .create_session(&plan, &profile_info, |common, session| { + Self::build_session_params(common, session, sps, pps) + })?; self.active_sps = Some(sps.clone()); self.active_pps = Some(pps.clone()); @@ -469,23 +295,6 @@ impl H264Decoder { let pins = self.common.session()?.slot_pins.clone(); self.dpb = Some(DecodeDpb::new(slot_count, pins)); - debug!( - "H.264 decode session: {}x{} {:?}, {} DPB slots ({} spare), \ - layered={}, coincide={}, pinnable={}, sampleable={}, \ - plane_views={}, bitstream alignment {}/{} (offset/size)", - coded_width, - coded_height, - picture_format, - slot_count, - spare_slots, - use_layered_dpb, - coincide, - pinnable, - sampleable, - plane_views, - self.common.bitstream_offset_alignment, - self.common.bitstream_size_alignment - ); Ok(()) } diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 4ae889f9..8250e3ce 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -48,6 +48,8 @@ pub(crate) mod frames; /// H.264 decoding. pub(crate) mod h264; pub(crate) mod pipeline; +/// Turning what a stream needs into a session plan. +pub(crate) mod plan; /// Readback and copies, all of which run on the transfer queue. pub(crate) mod transfer; diff --git a/src/decoder/plan.rs b/src/decoder/plan.rs new file mode 100644 index 00000000..5206ccf2 --- /dev/null +++ b/src/decoder/plan.rs @@ -0,0 +1,329 @@ +//! Planning a decode session: from what a stream needs and what the device +//! offers for its profile to the numbers a session is built from. +//! +//! The codec reads its parameter sets and fills a [`StreamNeeds`]. Everything +//! after that (capabilities, DPB slots, picture format, image flags) is the +//! same for every codec and lives here. + +use ash::vk; +use ash::vk::TaggedStructure; +use tracing::debug; + +use crate::decoder::common::DecoderCommon; +use crate::encoder::{BitDepth, ChromaFormat}; +use crate::error::{PixelForgeError, Result}; +use crate::video::{align_up, get_video_format, query_supported_video_formats}; + +/// What a stream asks of a decode session, as the codec reads it from the +/// stream's own parameter sets. +pub(crate) struct StreamNeeds { + /// The codec's name, for messages: "H.264". + pub codec: &'static str, + /// Coded dimensions before alignment to the device's picture granularity. + pub width: u32, + pub height: u32, + pub chroma_format: ChromaFormat, + pub bit_depth: BitDepth, + /// Reference pictures the stream keeps in its DPB at once. + pub references: u32, + /// Reference pictures a single picture may use. Never more than + /// `references`. + pub active_references: u32, + /// The most DPB slots the codec can address. + pub slot_limit: u32, + /// Pictures the stream may hold back for display-order reordering. + pub reorder_depth: usize, + /// Frames the caller may hold at once (`DecodeConfig::output_depth`). + pub output_depth: usize, +} + +/// What a codec decided the session should look like. +/// +/// [`DecoderCommon::plan_session`] derives these from the stream's needs and +/// the device caps; [`DecoderCommon::create_session`] turns them into Vulkan +/// objects. +pub(crate) struct SessionPlan { + /// The codec's name, for messages. + pub codec: &'static str, + /// Coded dimensions, already aligned to the device's picture granularity. + pub coded_width: u32, + pub coded_height: u32, + pub picture_format: vk::Format, + pub bit_depth: BitDepth, + pub chroma_format: ChromaFormat, + /// DPB slots: references, the picture being decoded, and `spare_slots`. + pub slot_count: usize, + /// How many of `slot_count` are spare capacity for pictures pinned past the + /// decode that produced them: those held for display-order reordering, and + /// those handed to the caller. Zero means every emitted picture has to be + /// copied out instead of pinned in place. + pub spare_slots: usize, + /// References the driver may use for a single picture. + pub max_active_references: u32, + /// Driver decodes straight into the DPB image (`DPB_AND_OUTPUT_COINCIDE`). + pub coincide: bool, + /// A decoded picture may be handed to the caller in place, pinned in its + /// DPB slot, rather than copied out. See + /// [`DecodeSession::pinnable`](crate::decoder::common::DecodeSession::pinnable). + pub pinnable: bool, + /// `dpb_usage` includes `SAMPLED`, so handed-out frames can be sampled. + pub sampleable: bool, + /// Picture images are created with `MUTABLE_FORMAT`, so a consumer can view + /// their planes separately. See + /// [`DecodeSession::plane_views`](crate::decoder::common::DecodeSession::plane_views). + pub plane_views: bool, + /// Creation flags for picture images, from what the driver reported. + pub image_flags: vk::ImageCreateFlags, + /// Formats views of a picture image may use. Empty unless `plane_views`. + pub view_formats: Vec, + /// One layered DPB image rather than one image per slot. + pub use_layered_dpb: bool, + pub dpb_usage: vk::ImageUsageFlags, + /// The Std header version the device reports for this profile. + pub std_header_version: vk::ExtensionProperties, + /// Bitstream buffer alignments the device requires, each at least one. + pub bitstream_offset_alignment: u64, + pub bitstream_size_alignment: u64, +} + +/// The fields of `VkVideoCapabilitiesKHR` a plan uses, copied out of the +/// query's pointer chain. +struct Caps { + flags: vk::VideoCapabilityFlagsKHR, + min_bitstream_buffer_offset_alignment: u64, + min_bitstream_buffer_size_alignment: u64, + picture_access_granularity: vk::Extent2D, + max_coded_extent: vk::Extent2D, + max_dpb_slots: u32, + max_active_reference_pictures: u32, + std_header_version: vk::ExtensionProperties, +} + +impl DecoderCommon { + /// Plan a session for a stream with `needs`, decoded with `profile_info`. + /// + /// `codec_caps` is the codec's capability struct (for example + /// `VkVideoDecodeH264CapabilitiesKHR`), which the query must chain even + /// when the codec reads nothing from it. + pub fn plan_session<'a, C>( + &self, + profile_info: &vk::VideoProfileInfoKHR, + codec_caps: &'a mut C, + needs: &StreamNeeds, + ) -> Result + where + C: vk::Extends> + TaggedStructure<'a>, + { + let codec = needs.codec; + + // Query capabilities for this exact profile. The chain borrows the + // caps structs, so scope it and copy the results out. + let mut decode_caps = vk::VideoDecodeCapabilitiesKHR::default(); + let caps = { + let mut chain = vk::VideoCapabilitiesKHR::default() + .push(codec_caps) + .push(&mut decode_caps); + query_decode_caps(self, profile_info, &mut chain)?; + Caps { + flags: chain.flags, + min_bitstream_buffer_offset_alignment: chain.min_bitstream_buffer_offset_alignment, + min_bitstream_buffer_size_alignment: chain.min_bitstream_buffer_size_alignment, + picture_access_granularity: chain.picture_access_granularity, + max_coded_extent: chain.max_coded_extent, + max_dpb_slots: chain.max_dpb_slots, + max_active_reference_pictures: chain.max_active_reference_pictures, + std_header_version: chain.std_header_version, + } + }; + let decode_flags = decode_caps.flags; + + let granularity = caps.picture_access_granularity; + let coded_width = align_up(needs.width, granularity.width.max(1)); + let coded_height = align_up(needs.height, granularity.height.max(1)); + let max_extent = caps.max_coded_extent; + if coded_width > max_extent.width || coded_height > max_extent.height { + return Err(PixelForgeError::InvalidInput(format!( + "{codec} decode: stream is {}x{}, device maximum is {}x{}", + coded_width, coded_height, max_extent.width, max_extent.height + ))); + } + + let coincide = + decode_flags.contains(vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_COINCIDE); + debug!("{codec} decode capability flags: {:?}", decode_flags); + let use_layered_dpb = !caps + .flags + .contains(vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES); + + // The stream needs its references plus the current picture. On top of + // that come the spare slots, which is what lets a picture stay pinned + // in place past the decode that produced it instead of being copied + // out: `reorder_depth` of them for pictures waiting their turn in + // display order, and `output_depth` for frames the caller is holding. + // The device caps bound the total. + let required = (needs.references + 1).max(2); + let slot_limit = caps.max_dpb_slots.min(needs.slot_limit); + if required > slot_limit { + return Err(PixelForgeError::InvalidInput(format!( + "{codec} decode: stream needs {} DPB slots, device supports {}", + required, caps.max_dpb_slots + ))); + } + // Spare slots only buy something when pictures can be pinned. Without + // that they are handed out as copies, which needs no DPB slot at all, + // so asking for spares would just reserve memory nothing uses. + let pinnable = coincide && self.context.has_unified_image_layouts(); + let wanted_spare = if pinnable { + needs.reorder_depth + needs.output_depth + } else { + 0 + }; + let slot_count = (required + wanted_spare as u32).min(slot_limit) as usize; + let spare_slots = slot_count - required as usize; + if spare_slots < wanted_spare { + debug!( + "{codec} decode: {} of {} spare DPB slots available \ + (reorder depth {} plus {} output slots; the stream's references \ + need {} of the device's {}); pictures beyond that are copied", + spare_slots, + wanted_spare, + needs.reorder_depth, + needs.output_depth, + required, + caps.max_dpb_slots + ); + } + + // Only the reference pictures are ever active at once: the current + // picture and the output reservation are not references. + let active_references = needs.active_references.max(1).min(required - 1); + if active_references > caps.max_active_reference_pictures { + return Err(PixelForgeError::InvalidInput(format!( + "{codec} decode: stream needs {} active reference pictures, device supports {}", + active_references, caps.max_active_reference_pictures + ))); + } + + // Pick a picture format the device actually supports for decode. + // + // Ask for SAMPLED as well: a coincident DPB image is what the caller + // receives, and SAMPLED is what lets them read it in a shader instead + // of copying it out first. Fall back to the plain usage if the device + // reports no sampleable format, which costs the caller a copy but keeps + // decoding working. With a distinct output image nothing asks, since + // those pictures reach the caller as pool copies, which are sampleable + // whatever the device says about video images. + let base_usage = if coincide { + vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR + | vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR + | vk::ImageUsageFlags::TRANSFER_SRC + } else { + vk::ImageUsageFlags::VIDEO_DECODE_DPB_KHR + }; + let sampled_usage = base_usage | vk::ImageUsageFlags::SAMPLED; + let sampled_formats = if coincide { + query_supported_video_formats(&self.context, profile_info, sampled_usage) + .unwrap_or_default() + } else { + Vec::new() + }; + let (dpb_usage, sampleable, supported) = if sampled_formats.is_empty() { + let formats = query_supported_video_formats(&self.context, profile_info, base_usage)?; + (base_usage, false, formats) + } else { + (sampled_usage, true, sampled_formats) + }; + let preferred = get_video_format(needs.chroma_format, needs.bit_depth); + let chosen = supported + .iter() + .find(|f| f.format == preferred) + .or_else(|| supported.first()) + .copied() + .ok_or_else(|| { + PixelForgeError::NoSuitableDevice(format!( + "{codec} decode: no supported picture format for this profile" + )) + })?; + let picture_format = chosen.format; + + // MUTABLE_FORMAT lets a consumer view the picture's planes separately, + // which is the only way to read a multi-planar image without a + // sampler-YCbCr conversion, and the only way at all for a renderer + // whose shaders come from a toolchain that cannot express one (naga and + // therefore wgpu, among others). It is opportunistic: the driver + // enumerates which creation flags it accepts for this profile, format + // and usage, and DPB-only images on both RADV and ANV accept none. + // + // Naming the plane formats keeps a driver from having to assume the + // image might be reinterpreted as anything, which is what would + // otherwise cost it compression. + let plane_views = chosen + .create_flags + .contains(vk::ImageCreateFlags::MUTABLE_FORMAT); + let image_flags = if plane_views { + vk::ImageCreateFlags::MUTABLE_FORMAT + } else { + vk::ImageCreateFlags::empty() + }; + let view_formats: Vec = if plane_views { + let mut list = vec![picture_format]; + list.extend(crate::video::plane_view_formats(picture_format)); + list + } else { + Vec::new() + }; + + Ok(SessionPlan { + codec, + coded_width, + coded_height, + picture_format, + bit_depth: needs.bit_depth, + chroma_format: needs.chroma_format, + slot_count, + spare_slots, + max_active_references: active_references, + coincide, + pinnable, + sampleable, + plane_views, + image_flags, + view_formats, + use_layered_dpb, + dpb_usage, + std_header_version: caps.std_header_version, + bitstream_offset_alignment: caps.min_bitstream_buffer_offset_alignment.max(1), + bitstream_size_alignment: caps.min_bitstream_buffer_size_alignment.max(1), + }) + } +} + +/// Query decode capabilities for a profile. +/// +/// `caps` is supplied by the caller already chained with the codec-specific +/// capability struct, since only the codec knows which one applies. +fn query_decode_caps( + common: &DecoderCommon, + profile_info: &vk::VideoProfileInfoKHR, + caps: &mut vk::VideoCapabilitiesKHR, +) -> Result<()> { + let context = &common.context; + let video_queue_instance = + ash::khr::video_queue::Instance::load(context.entry(), context.instance()); + let result = unsafe { + (video_queue_instance + .fp() + .get_physical_device_video_capabilities_khr)( + context.physical_device(), + profile_info, + caps, + ) + }; + if result != vk::Result::SUCCESS { + return Err(PixelForgeError::NoSuitableDevice(format!( + "decode profile not supported: {:?}", + result + ))); + } + Ok(()) +} From 600e1d9d1af3c11ef655a03ef067f6ed6bc2cc4c Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 08:45:38 +0100 Subject: [PATCH 03/17] refactor(decoder): keep the decode profile in the session --- src/decoder/common.rs | 40 +++++++++++--------- src/decoder/h264/session.rs | 74 +++++-------------------------------- src/decoder/mod.rs | 1 + src/decoder/profile.rs | 61 ++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 82 deletions(-) create mode 100644 src/decoder/profile.rs diff --git a/src/decoder/common.rs b/src/decoder/common.rs index 71ceb5ed..c54bb6ab 100644 --- a/src/decoder/common.rs +++ b/src/decoder/common.rs @@ -22,10 +22,10 @@ use std::sync::Arc; use ash::vk; use crate::decoder::DecodedFrame; -use crate::decoder::annexb::START_CODE; use crate::decoder::frames::SlotPins; use crate::decoder::pipeline::DecodePipeline; use crate::decoder::plan::SessionPlan; +use crate::decoder::profile::DecodeProfile; use crate::encoder::{BitDepth, ChromaFormat}; use crate::error::{PixelForgeError, Result}; use crate::video::{ @@ -135,6 +135,8 @@ pub(crate) struct DecodeSession { pub plane_views: bool, /// Distinct decode output image, only when `!coincide`. pub output_image: Option<(vk::Image, vk::DeviceMemory, vk::ImageView)>, + /// The profile everything in this session was created with. + pub profile: DecodeProfile, } impl DecodeSession { /// The image and array layer holding DPB slot `slot`. @@ -306,10 +308,11 @@ impl DecoderCommon { pub fn create_session( &mut self, plan: &SessionPlan, - profile_info: &vk::VideoProfileInfoKHR, + profile: DecodeProfile, make_params: impl FnOnce(&Self, vk::VideoSessionKHR) -> Result, ) -> Result<()> { self.destroy_session(); + let profile_info = profile.info(); self.bitstream_offset_alignment = plan.bitstream_offset_alignment; self.bitstream_size_alignment = plan.bitstream_size_alignment; @@ -423,6 +426,7 @@ impl DecoderCommon { sampleable: plan.sampleable, plane_views: plan.plane_views, output_image, + profile, }); tracing::debug!( @@ -482,18 +486,22 @@ impl DecoderCommon { } } - /// Copy a picture's slices into the staging buffer, each prefixed with a - /// start code, and report the buffer range plus each slice's offset. - pub fn stage_slices( - &mut self, - slices: &[&[u8]], - profile_info: &vk::VideoProfileInfoKHR, - ) -> Result<(u64, Vec)> { - let total: usize = slices.iter().map(|s| s.len() + START_CODE.len()).sum(); + /// Copy a picture's slices into the staging buffer, each preceded by + /// `prefix` (a start code for H.26x, nothing for AV1), and report the + /// buffer range plus each slice's offset. + pub fn stage_slices(&mut self, slices: &[&[u8]], prefix: &[u8]) -> Result<(u64, Vec)> { + let total: usize = slices.iter().map(|s| s.len() + prefix.len()).sum(); let aligned_total = crate::video::align_up(total as u32, self.bitstream_size_alignment as u32) as usize; - self.pipeline - .ensure_bitstream_capacity(&self.context, aligned_total, profile_info)?; + // Borrow the session by field, so the pipeline can be borrowed mutably. + let session = self.session.as_ref().ok_or_else(|| { + PixelForgeError::InvalidInput("decode: no active session".to_string()) + })?; + self.pipeline.ensure_bitstream_capacity( + &self.context, + aligned_total, + session.profile.info(), + )?; let dst = self.pipeline.current().bitstream_ptr; let mut offsets = Vec::with_capacity(slices.len()); @@ -501,12 +509,8 @@ impl DecoderCommon { for slice in slices { offsets.push(cursor as u32); unsafe { - std::ptr::copy_nonoverlapping( - START_CODE.as_ptr(), - dst.add(cursor), - START_CODE.len(), - ); - cursor += START_CODE.len(); + std::ptr::copy_nonoverlapping(prefix.as_ptr(), dst.add(cursor), prefix.len()); + cursor += prefix.len(); std::ptr::copy_nonoverlapping(slice.as_ptr(), dst.add(cursor), slice.len()); cursor += slice.len(); } diff --git a/src/decoder/h264/session.rs b/src/decoder/h264/session.rs index 35ea8120..87509280 100644 --- a/src/decoder/h264/session.rs +++ b/src/decoder/h264/session.rs @@ -7,6 +7,7 @@ use ash::vk; use ash::vk::TaggedStructure; use tracing::debug; +use crate::decoder::annexb::START_CODE; use crate::decoder::common::DecoderCommon; use crate::decoder::frames::{DecodedPicture, ReorderBuffer}; use crate::decoder::h264::dpb::{DecodeDpb, PictureState}; @@ -16,6 +17,7 @@ use crate::decoder::h264::parser::{ use crate::decoder::h264::record::{DecodeRecord, LinkParams}; use crate::decoder::pipeline::DECODE_PIPELINE_DEPTH; use crate::decoder::plan::StreamNeeds; +use crate::decoder::profile::DecodeProfile; use crate::decoder::{DecodeConfig, DecodeStatus, Framing}; use crate::encoder::{BitDepth, ChromaFormat}; use crate::error::{PixelForgeError, Result}; @@ -157,14 +159,8 @@ impl H264Decoder { self.common.session.as_ref().map(|a| a.picture_format) } - /// Build the H.264 decode profile info for a parsed SPS. - fn profile_for( - sps: &Sps, - ) -> Result<( - vk::VideoDecodeH264ProfileInfoKHR<'static>, - ChromaFormat, - BitDepth, - )> { + /// Build the H.264 decode profile for a parsed SPS. + fn profile_for(sps: &Sps) -> Result<(DecodeProfile, ChromaFormat, BitDepth)> { let chroma_format = match sps.chroma_format_idc { 1 => ChromaFormat::Yuv420, 3 => ChromaFormat::Yuv444, @@ -207,10 +203,7 @@ impl H264Decoder { } }; - let profile = vk::VideoDecodeH264ProfileInfoKHR::default() - .std_profile_idc(std_profile) - .picture_layout(vk::VideoDecodeH264PictureLayoutFlagsKHR::PROGRESSIVE); - + let profile = DecodeProfile::h264(std_profile, chroma_format, bit_depth); Ok((profile, chroma_format, bit_depth)) } @@ -237,27 +230,7 @@ impl H264Decoder { )); } - let (mut profile, chroma_format, bit_depth) = Self::profile_for(sps)?; - let chroma_subsampling = match chroma_format { - ChromaFormat::Yuv420 => vk::VideoChromaSubsamplingFlagsKHR::TYPE_420, - ChromaFormat::Yuv444 => vk::VideoChromaSubsamplingFlagsKHR::TYPE_444, - other => { - return Err(PixelForgeError::InvalidInput(format!( - "H.264 decode: unsupported chroma format {:?}", - other - ))); - } - }; - let depth_flag = match bit_depth { - BitDepth::Eight => vk::VideoComponentBitDepthFlagsKHR::TYPE_8, - BitDepth::Ten => vk::VideoComponentBitDepthFlagsKHR::TYPE_10, - }; - let profile_info = vk::VideoProfileInfoKHR::default() - .video_codec_operation(vk::VideoCodecOperationFlagsKHR::DECODE_H264) - .chroma_subsampling(chroma_subsampling) - .luma_bit_depth(depth_flag) - .chroma_bit_depth(depth_flag) - .push(&mut profile); + let (profile, chroma_format, bit_depth) = Self::profile_for(sps)?; // The stream needs max_num_ref_frames references plus the current // picture. Reorder depth comes from the stream (VUI); without it, bound @@ -282,10 +255,10 @@ impl H264Decoder { let mut h264_caps = vk::VideoDecodeH264CapabilitiesKHR::default(); let plan = self .common - .plan_session(&profile_info, &mut h264_caps, &needs)?; + .plan_session(profile.info(), &mut h264_caps, &needs)?; let slot_count = plan.slot_count; self.common - .create_session(&plan, &profile_info, |common, session| { + .create_session(&plan, profile, |common, session| { Self::build_session_params(common, session, sps, pps) })?; @@ -605,7 +578,8 @@ impl H264Decoder { // completed. Staging before this waits overwrote coded data the GPU was // still reading. self.common.begin_decode_commands()?; - let (buffer_range, slice_offsets) = self.stage_slices(picture, sps)?; + let (buffer_range, slice_offsets) = + self.common.stage_slices(&picture.slices, &START_CODE)?; self.common.record_barriers(slot); self.record_decode(picture, &state, slot, buffer_range, &slice_offsets)?; self.common.submit_decode()?; @@ -655,34 +629,6 @@ impl H264Decoder { Ok(Some(frame)) } - /// Copy the picture's slice NALs into the staging buffer. - /// - /// Returns the aligned buffer range to hand to the driver and the offset of - /// each slice within it. - /// Stage this picture's slices for the driver. - /// - /// The copying is generic; only the profile (needed to size the buffer) is - /// H.264's business. - fn stage_slices(&mut self, picture: &Picture, sps: &Sps) -> Result<(u64, Vec)> { - let (mut profile, chroma_format, bit_depth) = Self::profile_for(sps)?; - let chroma_subsampling = match chroma_format { - ChromaFormat::Yuv420 => vk::VideoChromaSubsamplingFlagsKHR::TYPE_420, - _ => vk::VideoChromaSubsamplingFlagsKHR::TYPE_444, - }; - let depth_flag = match bit_depth { - BitDepth::Eight => vk::VideoComponentBitDepthFlagsKHR::TYPE_8, - BitDepth::Ten => vk::VideoComponentBitDepthFlagsKHR::TYPE_10, - }; - let profile_info = vk::VideoProfileInfoKHR::default() - .video_codec_operation(vk::VideoCodecOperationFlagsKHR::DECODE_H264) - .chroma_subsampling(chroma_subsampling) - .luma_bit_depth(depth_flag) - .chroma_bit_depth(depth_flag) - .push(&mut profile); - - self.common.stage_slices(&picture.slices, &profile_info) - } - /// Record `vkCmdBeginVideoCoding` / `vkCmdDecodeVideo` / `vkCmdEndVideoCoding`. fn record_decode( &mut self, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 8250e3ce..fb02b7ba 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -50,6 +50,7 @@ pub(crate) mod h264; pub(crate) mod pipeline; /// Turning what a stream needs into a session plan. pub(crate) mod plan; +pub(crate) mod profile; /// Readback and copies, all of which run on the transfer queue. pub(crate) mod transfer; diff --git a/src/decoder/profile.rs b/src/decoder/profile.rs new file mode 100644 index 00000000..59e2db54 --- /dev/null +++ b/src/decoder/profile.rs @@ -0,0 +1,61 @@ +//! The video profile a decode session uses. + +use ash::vk; + +use crate::encoder::{BitDepth, ChromaFormat, Codec}; + +/// The video profile a decode session uses, owned so it can be named again +/// after creation. +/// +/// The session, its DPB images, its output image and every bitstream buffer +/// it reads have to name the session's exact profile. The chain lives in one +/// heap allocation, so the pointer from the base struct to the codec struct +/// stays valid for as long as this does. +pub(crate) struct DecodeProfile(Box); + +struct ProfileChain { + info: vk::VideoProfileInfoKHR<'static>, + h264: vk::VideoDecodeH264ProfileInfoKHR<'static>, +} + +// SAFETY: the raw pointer in the chain only points into the same boxed +// allocation, which moves with this value and is never shared. +unsafe impl Send for DecodeProfile {} + +impl DecodeProfile { + /// An H.264 decode profile. + pub(crate) fn h264( + std_profile_idc: ash::vk::native::StdVideoH264ProfileIdc, + chroma_format: ChromaFormat, + bit_depth: BitDepth, + ) -> Self { + let mut chain = Self::chain(Codec::H264, chroma_format, bit_depth); + chain.h264 = vk::VideoDecodeH264ProfileInfoKHR::default() + .std_profile_idc(std_profile_idc) + .picture_layout(vk::VideoDecodeH264PictureLayoutFlagsKHR::PROGRESSIVE); + chain.info.p_next = (&chain.h264 as *const vk::VideoDecodeH264ProfileInfoKHR).cast(); + Self(chain) + } + + fn chain(codec: Codec, chroma_format: ChromaFormat, bit_depth: BitDepth) -> Box { + let operation = match codec { + Codec::H264 => vk::VideoCodecOperationFlagsKHR::DECODE_H264, + Codec::H265 => vk::VideoCodecOperationFlagsKHR::DECODE_H265, + Codec::AV1 => vk::VideoCodecOperationFlagsKHR::DECODE_AV1, + }; + let depth: vk::VideoComponentBitDepthFlagsKHR = bit_depth.into(); + Box::new(ProfileChain { + info: vk::VideoProfileInfoKHR::default() + .video_codec_operation(operation) + .chroma_subsampling(chroma_format.into()) + .luma_bit_depth(depth) + .chroma_bit_depth(depth), + h264: vk::VideoDecodeH264ProfileInfoKHR::default(), + }) + } + + /// The profile, with its chain. + pub(crate) fn info(&self) -> &vk::VideoProfileInfoKHR<'_> { + &self.0.info + } +} From 8ef9f8baa03a243178b366681d865a9f8178f141 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 08:46:17 +0100 Subject: [PATCH 04/17] refactor(decoder): count pins per DPB slot --- src/decoder/frames.rs | 67 ++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/src/decoder/frames.rs b/src/decoder/frames.rs index a2b32dd5..1fa09d3f 100644 --- a/src/decoder/frames.rs +++ b/src/decoder/frames.rs @@ -54,48 +54,69 @@ pub(crate) struct PoolIndex(usize); /// so the codec must not decode over it. Releases come from [`FramePin::drop`] /// on whatever thread the frame died on, which is why the decoder waits on the /// condition variable rather than polling. +/// +/// A slot can be pinned more than once: AV1 can show the same picture again +/// (`show_existing_frame`), and each frame that shows it holds its own pin. +/// The slot stays pinned until the last of them is released. #[derive(Debug, Default)] pub(crate) struct SlotPins { - /// One bit per DPB slot. - pinned: Mutex, + state: Mutex, released: Condvar, } +#[derive(Debug, Default)] +struct PinState { + /// Pins held on each DPB slot. + counts: [u16; MAX_PINNABLE_SLOTS], + /// Releases so far, so a waiter can tell that one happened. + releases: u64, +} + +/// The most DPB slots any codec uses: 16 references plus the current picture +/// for H.264 and H.265, with room to spare. +const MAX_PINNABLE_SLOTS: usize = 32; + impl SlotPins { pub fn is_pinned(&self, slot: u8) -> bool { - *self.pinned.lock().unwrap() & (1 << slot) != 0 + self.state.lock().unwrap().counts[slot as usize] != 0 } pub fn any_pinned(&self) -> bool { - *self.pinned.lock().unwrap() != 0 + self.state.lock().unwrap().counts.iter().any(|&c| c != 0) } /// How many slots are pinned right now, across everything holding one: /// pictures awaiting their turn in display order, frames on their way to - /// the caller, and frames the caller has not dropped. + /// the caller, and frames the caller has not dropped. A slot pinned more + /// than once counts once, since it holds one slot. pub fn count(&self) -> usize { - self.pinned.lock().unwrap().count_ones() as usize + let state = self.state.lock().unwrap(); + state.counts.iter().filter(|&&c| c != 0).count() } pub fn pin(&self, slot: u8) { - *self.pinned.lock().unwrap() |= 1 << slot; + self.state.lock().unwrap().counts[slot as usize] += 1; } pub fn release(&self, slot: u8) { - *self.pinned.lock().unwrap() &= !(1 << slot); + let mut state = self.state.lock().unwrap(); + let count = &mut state.counts[slot as usize]; + debug_assert!(*count > 0, "released slot {slot} was not pinned"); + *count = count.saturating_sub(1); + state.releases += 1; self.released.notify_all(); } - /// Block until at least one pinned slot is released. Returns immediately if + /// Block until at least one pin is released. Returns immediately if /// nothing is pinned, since then no release can ever come. pub fn wait_for_release(&self) { - let mut pinned = self.pinned.lock().unwrap(); - let before = *pinned; - if before == 0 { + let mut state = self.state.lock().unwrap(); + if state.counts.iter().all(|&c| c == 0) { return; } - while *pinned == before { - pinned = self.released.wait(pinned).unwrap(); + let before = state.releases; + while state.releases == before { + state = self.released.wait(state).unwrap(); } } } @@ -610,3 +631,21 @@ fn create_pool_image( Ok((image, memory)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn a_slot_pinned_twice_stays_pinned_until_both_release() { + let pins = SlotPins::default(); + pins.pin(3); + pins.pin(3); + assert_eq!(pins.count(), 1, "one slot is held, however often"); + pins.release(3); + assert!(pins.is_pinned(3)); + pins.release(3); + assert!(!pins.is_pinned(3)); + assert!(!pins.any_pinned()); + } +} From 0d59551bfcc129738ce5ddca345cbb2e5fc6ed25 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 08:48:00 +0100 Subject: [PATCH 05/17] refactor(decoder): drive every codec through one stream decoder --- src/decoder/driver.rs | 236 ++++++++++++++++++++++++++++++++++++ src/decoder/h264/mod.rs | 22 ---- src/decoder/h264/session.rs | 217 +++++++-------------------------- src/decoder/mod.rs | 7 +- 4 files changed, 288 insertions(+), 194 deletions(-) create mode 100644 src/decoder/driver.rs diff --git a/src/decoder/driver.rs b/src/decoder/driver.rs new file mode 100644 index 00000000..7ddf953c --- /dev/null +++ b/src/decoder/driver.rs @@ -0,0 +1,236 @@ +//! The decode flow every codec shares: framing the input, waiting for a +//! keyframe, reordering into display order, and the end of the stream. +//! +//! A codec implements [`CodecDecoder`]: how to split its bitstream into coded +//! pictures and how to decode one. [`StreamDecoder`] drives it and implements +//! the codec-erased [`DecoderApi`](crate::decoder::DecoderApi) once for all +//! codecs. + +use ash::vk; +use tracing::debug; + +use crate::decoder::common::DecoderCommon; +use crate::decoder::frames::{DecodedPicture, ReorderBuffer}; +use crate::decoder::{DecodeConfig, DecodeStatus, DecoderApi, FrameReceiver, Framing}; +use crate::error::Result; + +/// What a codec does; everything else is [`StreamDecoder`]'s. +pub(crate) trait CodecDecoder: Send { + /// One coded picture, as [`Self::split_units`] gathers it from the input. + type Unit<'a>; + + /// The codec's name, for messages: "H.264". + const NAME: &'static str; + + /// The shared Vulkan state, which the codec owns. + fn common(&self) -> &DecoderCommon; + fn common_mut(&mut self) -> &mut DecoderCommon; + + /// How many leading bytes of `buffer` hold coded pictures known to be + /// complete. + /// + /// A coded picture is only known to have ended once the next one starts, + /// so everything from the last picture start onward may still be waiting + /// for more data. Zero means nothing in `buffer` can be decoded yet. + fn complete_prefix(buffer: &[u8]) -> usize; + + /// Split `data`, which ends on a picture boundary, into coded pictures, + /// absorbing any parameter sets on the way. + /// + /// Returns the pictures plus, if some data was skipped because state it + /// needs has not been seen (parameter sets sent before the stream was + /// joined), the reason a keyframe is needed. + fn split_units<'a>(&mut self, data: &'a [u8]) -> Result<(Vec>, Option)>; + + /// Whether decoding can start at `unit`. + fn is_keyframe(unit: &Self::Unit<'_>) -> bool; + + /// Decode one coded picture. + /// + /// Returns the picture to output, or `None` for one that is decoded but + /// not shown. + fn decode_unit(&mut self, unit: &Self::Unit<'_>, pts: u64) -> Result>; + + /// How many pictures the active stream may hold back for display-order + /// reordering. + fn reorder_depth(&self) -> usize; +} + +/// Drives a [`CodecDecoder`]. +pub(crate) struct StreamDecoder { + codec: C, + + /// Display-order reordering, which holds pictures by pinning their DPB + /// slots rather than by copying them out. + reorder: ReorderBuffer, + + /// How input is framed, which decides whether the end of a `decode` call is + /// also the end of a coded picture. + framing: Framing, + /// Bytes of a coded picture that has not been seen to end yet, held over + /// from earlier `decode` calls. Only ever non-empty in + /// [`Framing::ByteStream`]. + carry: Vec, + /// The `pts` of the call that delivered the first byte of `carry`, so a + /// picture is timestamped by when it started arriving rather than by which + /// call happened to complete it. + carry_pts: u64, + + /// Whether decoding is waiting for a keyframe before it can produce output. + /// True until the first keyframe is decoded: other pictures reference state + /// that does not exist yet, so they are skipped and the caller is told to + /// request a keyframe. This is the normal state when joining a stream. + awaiting_keyframe: bool, +} + +impl StreamDecoder { + pub(crate) fn new(codec: C, config: &DecodeConfig) -> Self { + Self { + codec, + reorder: ReorderBuffer::new(), + framing: config.framing, + carry: Vec::new(), + carry_pts: 0, + awaiting_keyframe: true, + } + } + + /// Framing for input that may cut anywhere. + /// + /// A coded picture is only known to have ended once the next one starts, so + /// everything up to the last picture start is complete and decodable, and + /// everything from it onward is held over for the next call. `finish` ends + /// the final picture. + fn decode_byte_stream(&mut self, data: &[u8], pts: u64) -> Result { + let carried = self.carry.len(); + let mut buffer = std::mem::take(&mut self.carry); + buffer.extend_from_slice(data); + + // Nothing before the last picture start can still be waiting for more + // data. With no picture start at all, nothing is complete yet. + let split = C::complete_prefix(&buffer); + + let result = if split > 0 { + self.decode_frames(&buffer[..split], self.carry_pts) + } else { + // Not enough bytes to complete a picture yet. + Ok(DecodeStatus::Buffered) + }; + + // The held-over picture is timestamped by the call its first byte came + // in on, which is this one only if the split consumed everything the + // previous calls had left. + if split >= carried { + self.carry_pts = pts; + } + buffer.drain(..split); + self.carry = buffer; + result + } + + /// Decode input already known to end on a picture boundary. + /// + /// Frames go to the consumer as each picture's work is handed to the + /// completion thread, so a call decoding many pictures does not hold their + /// frames until it returns. + fn decode_frames(&mut self, data: &[u8], pts: u64) -> Result { + // What is missing when a picture cannot be decoded yet; set only while + // no frame has been produced, so a keyframe later in the same buffer + // still yields output. Seeded with data skipped during splitting. + let (units, mut awaiting) = self.codec.split_units(data)?; + // Whether any picture actually reached the GPU, which decides between + // reporting a decode and asking for a keyframe. + let mut decoded_any = false; + + for unit in units { + let is_keyframe = C::is_keyframe(&unit); + + // A picture before the first keyframe references pictures and + // state that do not exist yet. Skip it and ask for a keyframe + // rather than decoding garbage. + if self.awaiting_keyframe && !is_keyframe { + awaiting.get_or_insert_with(|| { + "no keyframe decoded yet; picture cannot be decoded".to_string() + }); + continue; + } + + let picture = self.codec.decode_unit(&unit, pts)?; + decoded_any = true; + // A decoded keyframe re-establishes a valid decode point. + if is_keyframe { + self.awaiting_keyframe = false; + } + let ready = match picture { + Some(picture) => { + let depth = self.codec.reorder_depth(); + self.reorder + .push(self.codec.common_mut(), &picture, depth)? + } + None => Vec::new(), + }; + // The picture's submissions are complete as far as recording goes; + // hand its slot and frames to the completion thread and move on to + // the next slot. + self.codec.common_mut().end_picture(ready); + } + + if decoded_any { + return Ok(DecodeStatus::Decoded); + } + // Only ask for a keyframe when nothing was decoded: if one arrived + // later in the same buffer, its pictures take priority. + if let Some(reason) = awaiting { + debug!("{} decode: awaiting keyframe: {}", C::NAME, reason); + return Ok(DecodeStatus::NeedsKeyframe); + } + Ok(DecodeStatus::Buffered) + } +} + +impl DecoderApi for StreamDecoder { + fn decode(&mut self, data: &[u8], pts: u64) -> Result { + match self.framing { + // The caller guarantees the chunk ends on a picture boundary, so it + // can go straight to the decoder with no buffering and no copy. + Framing::FrameAligned => self.decode_frames(data, pts), + Framing::ByteStream => self.decode_byte_stream(data, pts), + } + } + + /// Decode what is still buffered and emit the pictures held for + /// reordering. Call at end of stream. + fn finish(&mut self) -> Result<()> { + // In byte-stream framing the tail is a picture that was never seen to + // end because nothing followed it. End of stream is what ends it. + if !self.carry.is_empty() { + let tail = std::mem::take(&mut self.carry); + let pts = self.carry_pts; + self.decode_frames(&tail, pts)?; + } + let frames = self.reorder.flush(); + self.codec.common_mut().finish_stream(frames); + Ok(()) + } + + fn take_frame_receiver(&mut self) -> Option { + self.codec.common_mut().frames_rx.take() + } + + fn picture_format(&self) -> Option { + self.codec + .common() + .session + .as_ref() + .map(|s| s.picture_format) + } +} + +impl Drop for StreamDecoder { + fn drop(&mut self) { + // The reorder pool's images must be freed while the codec's + // `DecoderCommon` (and its device) is still alive. Fields drop after + // this, so the codec goes afterwards. + self.reorder.destroy(self.codec.common()); + } +} diff --git a/src/decoder/h264/mod.rs b/src/decoder/h264/mod.rs index 7c61af60..6815eb65 100644 --- a/src/decoder/h264/mod.rs +++ b/src/decoder/h264/mod.rs @@ -14,10 +14,6 @@ mod dpb; mod record; mod session; -use crate::decoder::{DecodeStatus, FrameReceiver}; -use crate::error::Result; -use ash::vk; - pub(crate) use session::H264Decoder; /// Byte offsets at which each coded picture begins in an Annex B stream. @@ -58,21 +54,3 @@ pub(crate) fn picture_starts(stream: &[u8]) -> Vec { pub(crate) fn complete_prefix(buffer: &[u8]) -> usize { picture_starts(buffer).last().copied().unwrap_or(0) } - -impl crate::decoder::DecoderApi for H264Decoder { - fn decode(&mut self, data: &[u8], pts: u64) -> Result { - H264Decoder::decode(self, data, pts) - } - - fn finish(&mut self) -> Result<()> { - H264Decoder::finish(self) - } - - fn take_frame_receiver(&mut self) -> Option { - H264Decoder::take_frame_receiver(self) - } - - fn picture_format(&self) -> Option { - H264Decoder::picture_format(self) - } -} diff --git a/src/decoder/h264/session.rs b/src/decoder/h264/session.rs index 87509280..d1a0e951 100644 --- a/src/decoder/h264/session.rs +++ b/src/decoder/h264/session.rs @@ -7,9 +7,11 @@ use ash::vk; use ash::vk::TaggedStructure; use tracing::debug; +use crate::decoder::DecodeConfig; use crate::decoder::annexb::START_CODE; use crate::decoder::common::DecoderCommon; -use crate::decoder::frames::{DecodedPicture, ReorderBuffer}; +use crate::decoder::driver::CodecDecoder; +use crate::decoder::frames::DecodedPicture; use crate::decoder::h264::dpb::{DecodeDpb, PictureState}; use crate::decoder::h264::parser::{ self, NalType, NalUnit, Pps, SliceHeader, SliceType, Sps, iter_nal_units, @@ -18,7 +20,6 @@ use crate::decoder::h264::record::{DecodeRecord, LinkParams}; use crate::decoder::pipeline::DECODE_PIPELINE_DEPTH; use crate::decoder::plan::StreamNeeds; use crate::decoder::profile::DecodeProfile; -use crate::decoder::{DecodeConfig, DecodeStatus, Framing}; use crate::encoder::{BitDepth, ChromaFormat}; use crate::error::{PixelForgeError, Result}; use crate::vulkan::VideoContext; @@ -26,7 +27,9 @@ use crate::vulkan::VideoContext; /// H.264 decoder. /// /// Owns only what is specific to H.264: the parameter sets seen so far, and the -/// POC / reference-marking state. Everything Vulkan lives in [`DecoderCommon`]. +/// POC / reference-marking state. Everything Vulkan lives in [`DecoderCommon`], +/// and the flow around a picture (framing, keyframes, reordering) is +/// [`StreamDecoder`](crate::decoder::driver::StreamDecoder)'s. pub(crate) struct H264Decoder { common: DecoderCommon, @@ -53,31 +56,10 @@ pub(crate) struct H264Decoder { /// into themselves. records: Box<[DecodeRecord]>, - /// Display-order reordering, which holds pictures by pinning their DPB - /// slots rather than by copying them out. - reorder: ReorderBuffer, /// `max_num_reorder_frames` for the active SPS: how deep the reorder buffer /// may hold pictures before emitting. Set by `activate`, which also sizes /// the DPB so that many pictures can be pinned rather than copied. reorder_depth: usize, - - /// How input is framed, which decides whether the end of a `decode` call is - /// also the end of a coded picture. - framing: Framing, - /// Bytes of a coded picture that has not been seen to end yet, held over - /// from earlier `decode` calls. Only ever non-empty in - /// [`Framing::ByteStream`]. - carry: Vec, - /// The `pts` of the call that delivered the first byte of `carry`, so a - /// picture is timestamped by when it started arriving rather than by which - /// call happened to complete it. - carry_pts: u64, - - /// Whether decoding is waiting for a keyframe (IDR) before it can produce - /// output. True until the first IDR is decoded: non-IDR pictures reference - /// state that does not exist yet, so they are skipped and the caller is told - /// to request an IDR. This is the normal state when joining a stream. - awaiting_keyframe: bool, } /// What `group_slices` did with a slice. @@ -90,7 +72,7 @@ enum Grouped { } /// The slices making up one picture, gathered from the input. -struct Picture<'a> { +pub(crate) struct Picture<'a> { header: SliceHeader, nal_type: NalType, ref_idc: u8, @@ -121,44 +103,10 @@ impl H264Decoder { records: (0..DECODE_PIPELINE_DEPTH) .map(|_| DecodeRecord::new()) .collect(), - reorder: ReorderBuffer::new(), reorder_depth: 0, - framing: config.framing, - carry: Vec::new(), - carry_pts: 0, - awaiting_keyframe: true, }) } - /// Emit any pictures still held for reordering. Call at end of stream. - /// - /// These pictures were copied out by earlier calls, so this submits no new - /// GPU work; the future still resolves in call order behind whatever is - /// already in flight. - pub(crate) fn finish(&mut self) -> Result<()> { - // In byte-stream framing the tail is a picture that was never seen to - // end because nothing followed it. End of stream is what ends it. - if !self.carry.is_empty() { - let tail = std::mem::take(&mut self.carry); - let pts = self.carry_pts; - // A tail that cannot be decoded is not worth failing over: the - // frames already decoded still have to come out. - self.decode_frames(&tail, pts)?; - } - let frames = self.reorder.flush(); - self.common.finish_stream(frames); - Ok(()) - } - - /// Hand over the frame channel's receiving end, once. - pub(crate) fn take_frame_receiver(&mut self) -> Option { - self.common.frames_rx.take() - } - - pub(crate) fn picture_format(&self) -> Option { - self.common.session.as_ref().map(|a| a.picture_format) - } - /// Build the H.264 decode profile for a parsed SPS. fn profile_for(sps: &Sps) -> Result<(DecodeProfile, ChromaFormat, BitDepth)> { let chroma_format = match sps.chroma_format_idc { @@ -429,113 +377,7 @@ impl H264Decoder { Ok((pictures, awaiting)) } - /// Decode a chunk of input, doing the framing the configuration calls for. - pub(crate) fn decode(&mut self, data: &[u8], pts: u64) -> Result { - match self.framing { - // The caller guarantees the chunk ends on a picture boundary, so it - // can go straight to the decoder with no buffering and no copy. - Framing::FrameAligned => self.decode_frames(data, pts), - Framing::ByteStream => self.decode_byte_stream(data, pts), - } - } - - /// Framing for input that may cut anywhere. - /// - /// A coded picture is only known to have ended once the next one starts, so - /// everything up to the last picture start is complete and decodable, and - /// everything from it onward is held over for the next call. `flush` ends - /// the final picture. - fn decode_byte_stream(&mut self, data: &[u8], pts: u64) -> Result { - let carried = self.carry.len(); - let mut buffer = std::mem::take(&mut self.carry); - buffer.extend_from_slice(data); - - // Nothing before the last picture start can still be waiting for more - // slices. With no picture start at all, nothing is complete yet. - let split = super::complete_prefix(&buffer); - - let result = if split > 0 { - self.decode_frames(&buffer[..split], self.carry_pts) - } else { - // Not enough bytes to complete a picture yet. - Ok(DecodeStatus::Buffered) - }; - - // The held-over picture is timestamped by the call its first byte came - // in on, which is this one only if the split consumed everything the - // previous calls had left. - if split >= carried { - self.carry_pts = pts; - } - buffer.drain(..split); - self.carry = buffer; - result - } - - /// Decode input already known to end on a picture boundary. - /// - /// Frames go to the consumer as each picture's work is handed to the - /// completion thread, so a call decoding many pictures does not hold their - /// frames until it returns. - fn decode_frames(&mut self, data: &[u8], pts: u64) -> Result { - // What is missing when a picture cannot be decoded yet; set only while - // no frame has been produced, so a keyframe later in the same buffer - // still yields output. Seeded with slices skipped during splitting for - // missing parameter sets. - let (pictures, mut awaiting) = self.split_pictures(data)?; - // Whether any picture actually reached the GPU, which decides between - // returning a batch and asking for a keyframe. - let mut decoded_any = false; - - for picture in pictures { - let is_idr = picture.nal_type == NalType::IdrSlice; - - // A non-IDR picture before the first keyframe references reference - // pictures and POC state that do not exist yet. Skip it and ask for - // a keyframe rather than decoding garbage. - if self.awaiting_keyframe && !is_idr { - awaiting.get_or_insert_with(|| { - "no keyframe decoded yet; non-IDR picture cannot be decoded".to_string() - }); - continue; - } - - // The parameter sets this picture was grouped under, not whatever - // currently sits at those ids: a later SPS in the same chunk must - // not reach back and change how this picture is decoded. - let (sps, pps) = (picture.sps.clone(), picture.pps.clone()); - - // (Re)build the session if the stream's geometry or ids changed. - self.activate(&sps, &pps)?; - - if let Some(frame) = self.decode_picture(&picture, &sps, pts)? { - // A decoded IDR re-establishes a valid decode point. - if is_idr { - self.awaiting_keyframe = false; - } - let depth_reorder = self.reorder_depth; - let ready = self.reorder.push(&mut self.common, &frame, depth_reorder)?; - decoded_any = true; - // The picture's submissions are complete as far as recording - // goes; hand its slot and frames to the completion thread and - // move on to the next slot. - self.common.end_picture(ready); - } - } - - if decoded_any { - return Ok(DecodeStatus::Decoded); - } - // Only ask for a keyframe when nothing was decoded: if one arrived - // later in the same buffer, its pictures take priority. - if let Some(reason) = awaiting { - debug!("H.264 decode: awaiting keyframe: {}", reason); - return Ok(DecodeStatus::NeedsKeyframe); - } - Ok(DecodeStatus::Buffered) - } - - /// Record and submit the decode of a single picture, and wait for it. + /// Record and submit the decode of a single picture, without waiting for it. fn decode_picture( &mut self, picture: &Picture, @@ -754,11 +596,44 @@ impl H264Decoder { } } -impl Drop for H264Decoder { - fn drop(&mut self) { - // The reorder pool's images must be freed while `common` (and its - // device) is still alive; `common`'s own Drop runs afterward. - self.reorder.destroy(&self.common); +impl CodecDecoder for H264Decoder { + type Unit<'a> = Picture<'a>; + + const NAME: &'static str = "H.264"; + + fn common(&self) -> &DecoderCommon { + &self.common + } + + fn common_mut(&mut self) -> &mut DecoderCommon { + &mut self.common + } + + fn complete_prefix(buffer: &[u8]) -> usize { + super::complete_prefix(buffer) + } + + fn split_units<'a>(&mut self, data: &'a [u8]) -> Result<(Vec>, Option)> { + self.split_pictures(data) + } + + fn is_keyframe(picture: &Picture<'_>) -> bool { + picture.nal_type == NalType::IdrSlice + } + + fn decode_unit(&mut self, picture: &Picture<'_>, pts: u64) -> Result> { + // The parameter sets this picture was grouped under, not whatever + // currently sits at those ids: a later SPS in the same chunk must not + // reach back and change how this picture is decoded. + let (sps, pps) = (picture.sps.clone(), picture.pps.clone()); + + // (Re)build the session if the stream's geometry or ids changed. + self.activate(&sps, &pps)?; + self.decode_picture(picture, &sps, pts) + } + + fn reorder_depth(&self) -> usize { + self.reorder_depth } } diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index fb02b7ba..1ea65875 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -43,6 +43,8 @@ pub(crate) mod annexb; pub(crate) mod bitreader; /// The Vulkan video session and the per-decoder state every codec shares. pub(crate) mod common; +/// The decode flow every codec shares. +pub(crate) mod driver; /// Frame ownership: pool images, DPB slot pins, display-order reordering. pub(crate) mod frames; /// H.264 decoding. @@ -403,7 +405,10 @@ impl Decoder { ))); } let mut inner: Box = match config.codec { - Codec::H264 => Box::new(h264::H264Decoder::create(context, &config)?), + Codec::H264 => Box::new(driver::StreamDecoder::new( + h264::H264Decoder::create(context, &config)?, + &config, + )), other => { return Err(PixelForgeError::InvalidInput(format!( "{:?} decoding is not implemented yet", From 96456e367518bb1bd404c842ce5ae6999d850f57 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 08:48:31 +0100 Subject: [PATCH 06/17] refactor(decoder)!: replace DecodeConfig::h264 with DecodeConfig::new --- CHANGELOG.md | 2 ++ README.md | 2 +- examples/decode.rs | 4 +++- examples/sample_frame.rs | 2 +- examples/sample_planes.rs | 2 +- src/decoder/mod.rs | 6 +++--- src/lib.rs | 2 +- tests/adopted.rs | 5 ++++- tests/dmabuf.rs | 5 ++++- tests/rgb_encode.rs | 5 ++++- tests/roundtrip.rs | 2 +- tests/submit.rs | 2 +- 12 files changed, 26 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a10d420..15928b3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -52,6 +52,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Breaking:** `ColorConverter`, `ColorConverterConfig` and `OutputFormat` are no longer public. Describe an RGB source with `Source::Rgb` instead, and the encoder runs the converter itself, with the right target and range. +- **Breaking:** `DecodeConfig::new(codec)` replaces `DecodeConfig::h264()`, + in the same way as `EncodeConfig::new`. - **Breaking:** `RateControl` replaces `RateControlMode`, `quality_level`, `target_bitrate` and `max_bitrate`: `Cqp { qp }`, `Cbr { bitrate }` or `Vbr { target, max }`, so each mode carries exactly the values it uses. diff --git a/README.md b/README.md index 564deb2c..b0c5e5fe 100644 --- a/README.md +++ b/README.md @@ -152,7 +152,7 @@ fn main() -> Result<(), Box> { // A file can cut anywhere, so let the decoder frame it. Input that // arrives already framed (RTP, a container) skips `with_byte_stream`. - let config = DecodeConfig::h264().with_byte_stream(); + let config = DecodeConfig::new(Codec::H264).with_byte_stream(); let mut decoder = Decoder::new(context, config)?; let stream: Vec = std::fs::read("input.264")?; diff --git a/examples/decode.rs b/examples/decode.rs index 418a014a..25afc8a0 100644 --- a/examples/decode.rs +++ b/examples/decode.rs @@ -59,7 +59,9 @@ fn main() -> Result<(), Box> { // sized for one frame per call, so raise it: past the budget the decoder // copies pictures out instead of handing over its own images, which is // still correct but gives up the zero-copy path. - let config = DecodeConfig::h264().with_byte_stream().with_output_depth(8); + let config = DecodeConfig::new(Codec::H264) + .with_byte_stream() + .with_output_depth(8); let mut output = output_path.as_ref().map(File::create).transpose()?; // Readback lives in the consumer, not in pixelforge: the decoder hands over // a GPU image and a renderer would sample it in place. See examples/common. diff --git a/examples/sample_frame.rs b/examples/sample_frame.rs index 6ca9506c..371ccc58 100644 --- a/examples/sample_frame.rs +++ b/examples/sample_frame.rs @@ -63,7 +63,7 @@ fn main() -> Result<(), Box> { // picture's sharing set. Without this, sampling a decoded image from a // family the decoder did not share with is undefined. let consumer_family = context.compute_queue_family(); - let config = DecodeConfig::h264() + let config = DecodeConfig::new(Codec::H264) .with_byte_stream() .with_output_depth(8) .with_consumer_queue_family(consumer_family); diff --git a/examples/sample_planes.rs b/examples/sample_planes.rs index 7c48db1a..bc71d48f 100644 --- a/examples/sample_planes.rs +++ b/examples/sample_planes.rs @@ -61,7 +61,7 @@ fn main() -> Result<(), Box> { ); let consumer_family = context.compute_queue_family(); - let config = DecodeConfig::h264() + let config = DecodeConfig::new(Codec::H264) .with_byte_stream() .with_output_depth(8) .with_consumer_queue_family(consumer_family); diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 1ea65875..d63ad617 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -120,10 +120,10 @@ pub struct DecodeConfig { } impl DecodeConfig { - /// Create an H.264 decode configuration. - pub fn h264() -> Self { + /// Create a decode configuration for `codec`. + pub fn new(codec: Codec) -> Self { Self { - codec: Codec::H264, + codec, output_depth: DEFAULT_OUTPUT_DEPTH, consumer_queue_family: None, framing: Framing::FrameAligned, diff --git a/src/lib.rs b/src/lib.rs index 6378cd6d..e37b436a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,7 +146,7 @@ //! //! // A file can cut anywhere, so let the decoder frame it. Input that //! // arrives already framed (RTP, a container) skips `with_byte_stream`. -//! let config = DecodeConfig::h264().with_byte_stream(); +//! let config = DecodeConfig::new(Codec::H264).with_byte_stream(); //! let mut decoder = Decoder::new(context, config)?; //! let stream: Vec = std::fs::read("input.264")?; //! diff --git a/tests/adopted.rs b/tests/adopted.rs index 6f64b1b2..a9b063dd 100644 --- a/tests/adopted.rs +++ b/tests/adopted.rs @@ -144,7 +144,10 @@ fn decode_all( stream: &[u8], ) -> Result>, Box> { let mut readback = Readback::new(context)?; - let mut decoder = Decoder::new(context.clone(), DecodeConfig::h264().with_byte_stream())?; + let mut decoder = Decoder::new( + context.clone(), + DecodeConfig::new(Codec::H264).with_byte_stream(), + )?; let mut frames = Vec::new(); decode_stream(&mut decoder, stream, |frame| { let data = readback.read(&frame)?; diff --git a/tests/dmabuf.rs b/tests/dmabuf.rs index 4fa7d352..9f3a236f 100644 --- a/tests/dmabuf.rs +++ b/tests/dmabuf.rs @@ -606,7 +606,10 @@ fn unaligned_sizes_are_read_at_the_display_size() -> Result<(), Box Result>, Box> { let mut readback = Readback::new(context)?; - let mut decoder = Decoder::new(context.clone(), DecodeConfig::h264().with_byte_stream())?; + let mut decoder = Decoder::new( + context.clone(), + DecodeConfig::new(Codec::H264).with_byte_stream(), + )?; let mut frames = Vec::new(); decode_stream(&mut decoder, stream, |frame| { frames.push(readback.read(&frame)?.y); diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs index ec51583a..71ec5f9c 100644 --- a/tests/roundtrip.rs +++ b/tests/roundtrip.rs @@ -89,7 +89,7 @@ fn roundtrip_h264() -> Result<(), Box> { let yuv_path = dir.join("out.yuv"); let mut out = Some(File::create(&yuv_path)?); let mut readback = Some(Readback::new(&context)?); - let mut decoder = Decoder::new(context, DecodeConfig::h264().with_byte_stream())?; + let mut decoder = Decoder::new(context, DecodeConfig::new(Codec::H264).with_byte_stream())?; let mut decoded_count = 0usize; decode_stream(&mut decoder, &bitstream, |frame| { diff --git a/tests/submit.rs b/tests/submit.rs index 0612812c..c83e8bf7 100644 --- a/tests/submit.rs +++ b/tests/submit.rs @@ -131,7 +131,7 @@ fn repeat_encodes_the_last_frame_again() -> Result<(), Box Date: Thu, 5 Nov 2026 08:49:50 +0100 Subject: [PATCH 07/17] refactor(vulkan): check decode support for each codec --- src/decoder/profile.rs | 56 +++++++++++++ src/vulkan.rs | 173 ++++++++++++++++++++++------------------- 2 files changed, 147 insertions(+), 82 deletions(-) diff --git a/src/decoder/profile.rs b/src/decoder/profile.rs index 59e2db54..c50c20ba 100644 --- a/src/decoder/profile.rs +++ b/src/decoder/profile.rs @@ -16,6 +16,8 @@ pub(crate) struct DecodeProfile(Box); struct ProfileChain { info: vk::VideoProfileInfoKHR<'static>, h264: vk::VideoDecodeH264ProfileInfoKHR<'static>, + h265: vk::VideoDecodeH265ProfileInfoKHR<'static>, + av1: vk::VideoDecodeAV1ProfileInfoKHR<'static>, } // SAFETY: the raw pointer in the chain only points into the same boxed @@ -23,6 +25,30 @@ struct ProfileChain { unsafe impl Send for DecodeProfile {} impl DecodeProfile { + /// The profile that says whether a device decodes `codec` at all: 8-bit + /// 4:2:0 in H.264 High, H.265 Main or AV1 Main. + pub(crate) fn base(codec: Codec) -> Self { + let (chroma, depth) = (ChromaFormat::Yuv420, BitDepth::Eight); + match codec { + Codec::H264 => Self::h264( + ash::vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH, + chroma, + depth, + ), + Codec::H265 => Self::h265( + ash::vk::native::StdVideoH265ProfileIdc_STD_VIDEO_H265_PROFILE_IDC_MAIN, + chroma, + depth, + ), + Codec::AV1 => Self::av1( + ash::vk::native::StdVideoAV1Profile_STD_VIDEO_AV1_PROFILE_MAIN, + false, + chroma, + depth, + ), + } + } + /// An H.264 decode profile. pub(crate) fn h264( std_profile_idc: ash::vk::native::StdVideoH264ProfileIdc, @@ -37,6 +63,34 @@ impl DecodeProfile { Self(chain) } + /// An H.265 decode profile. + pub(crate) fn h265( + std_profile_idc: ash::vk::native::StdVideoH265ProfileIdc, + chroma_format: ChromaFormat, + bit_depth: BitDepth, + ) -> Self { + let mut chain = Self::chain(Codec::H265, chroma_format, bit_depth); + chain.h265 = vk::VideoDecodeH265ProfileInfoKHR::default().std_profile_idc(std_profile_idc); + chain.info.p_next = (&chain.h265 as *const vk::VideoDecodeH265ProfileInfoKHR).cast(); + Self(chain) + } + + /// An AV1 decode profile. `film_grain` asks for a session that can apply + /// film grain to the output. + pub(crate) fn av1( + std_profile: ash::vk::native::StdVideoAV1Profile, + film_grain: bool, + chroma_format: ChromaFormat, + bit_depth: BitDepth, + ) -> Self { + let mut chain = Self::chain(Codec::AV1, chroma_format, bit_depth); + chain.av1 = vk::VideoDecodeAV1ProfileInfoKHR::default() + .std_profile(std_profile) + .film_grain_support(film_grain); + chain.info.p_next = (&chain.av1 as *const vk::VideoDecodeAV1ProfileInfoKHR).cast(); + Self(chain) + } + fn chain(codec: Codec, chroma_format: ChromaFormat, bit_depth: BitDepth) -> Box { let operation = match codec { Codec::H264 => vk::VideoCodecOperationFlagsKHR::DECODE_H264, @@ -51,6 +105,8 @@ impl DecodeProfile { .luma_bit_depth(depth) .chroma_bit_depth(depth), h264: vk::VideoDecodeH264ProfileInfoKHR::default(), + h265: vk::VideoDecodeH265ProfileInfoKHR::default(), + av1: vk::VideoDecodeAV1ProfileInfoKHR::default(), }) } diff --git a/src/vulkan.rs b/src/vulkan.rs index 66e2709c..58c53757 100644 --- a/src/vulkan.rs +++ b/src/vulkan.rs @@ -1,4 +1,5 @@ //! Vulkan context and initialization for video encoding. +use crate::decoder::profile::DecodeProfile; use crate::encoder::Codec; use crate::error::{PixelForgeError, Result}; use ash::vk; @@ -564,23 +565,100 @@ fn decode_extension_names(decode_codecs: &[Codec]) -> Vec<&'static std::ffi::CSt ash::khr::synchronization2::NAME, ash::khr::timeline_semaphore::NAME, ]; - if decode_codecs.contains(&Codec::H264) { - names.push(ash::khr::video_decode_h264::NAME); + for codec in decode_codecs { + names.push(decode_extension_name(*codec)); } names } +/// The device extension that decodes `codec`. +fn decode_extension_name(codec: Codec) -> &'static std::ffi::CStr { + match codec { + Codec::H264 => ash::khr::video_decode_h264::NAME, + Codec::H265 => ash::khr::video_decode_h265::NAME, + Codec::AV1 => ash::khr::video_decode_av1::NAME, + } +} + +/// The codecs pixelforge can decode, on a device that supports them. +const DECODE_CODECS: &[Codec] = &[Codec::H264]; + /// The decode codecs `physical_device` supports. fn query_decode_codecs( entry: &ash::Entry, instance: &ash::Instance, physical_device: vk::PhysicalDevice, ) -> Vec { - let mut codecs = Vec::new(); - if VideoContext::check_h264_decode_support(entry, instance, physical_device) { - codecs.push(Codec::H264); + let extensions = unsafe { instance.enumerate_device_extension_properties(physical_device) } + .unwrap_or_default(); + let has_extension = |name: &std::ffi::CStr| { + extensions + .iter() + .any(|ext| ext.extension_name_as_c_str() == Ok(name)) + }; + DECODE_CODECS + .iter() + .copied() + .filter(|&codec| { + has_extension(decode_extension_name(codec)) + && check_decode_support(entry, instance, physical_device, codec) + }) + .collect() +} + +/// Whether `physical_device` decodes `codec` in its base profile: 8-bit +/// 4:2:0, H.264 High, H.265 Main or AV1 Main. +fn check_decode_support( + entry: &ash::Entry, + instance: &ash::Instance, + physical_device: vk::PhysicalDevice, + codec: Codec, +) -> bool { + let video_queue = ash::khr::video_queue::Instance::load(entry, instance); + let profile = DecodeProfile::base(codec); + + let mut h264_capabilities = vk::VideoDecodeH264CapabilitiesKHR::default(); + let mut h265_capabilities = vk::VideoDecodeH265CapabilitiesKHR::default(); + let mut av1_capabilities = vk::VideoDecodeAV1CapabilitiesKHR::default(); + let mut decode_capabilities = vk::VideoDecodeCapabilitiesKHR::default(); + let capabilities = vk::VideoCapabilitiesKHR::default().push(&mut decode_capabilities); + let mut capabilities = match codec { + Codec::H264 => capabilities.push(&mut h264_capabilities), + Codec::H265 => capabilities.push(&mut h265_capabilities), + Codec::AV1 => capabilities.push(&mut av1_capabilities), + }; + + let result = unsafe { + (video_queue.fp().get_physical_device_video_capabilities_khr)( + physical_device, + profile.info(), + &mut capabilities, + ) + }; + + match result { + vk::Result::SUCCESS => { + debug!( + "{:?} decode supported: max {}x{}, {} DPB slots", + codec, + capabilities.max_coded_extent.width, + capabilities.max_coded_extent.height, + capabilities.max_dpb_slots + ); + true + } + vk::Result::ERROR_VIDEO_PROFILE_CODEC_NOT_SUPPORTED_KHR => { + debug!("{:?} decode not supported on this device", codec); + false + } + _ => { + warn!( + "Failed to query {:?} decode capabilities: {:?}", + codec, result + ); + false + } } - codecs } /// Inner struct holding the actual Vulkan resources. @@ -1049,25 +1127,13 @@ impl VideoContext { } // Check codec support for decoding. - let mut decode_codecs = Vec::new(); - if decode_queue.is_some() { - let available_extensions = - unsafe { instance.enumerate_device_extension_properties(physical_device) } - .unwrap_or_default(); - let has_extension = |name: &std::ffi::CStr| -> bool { - available_extensions.iter().any(|ext| { - let ext_name = - unsafe { std::ffi::CStr::from_ptr(ext.extension_name.as_ptr()) }; - ext_name == name - }) - }; - - if has_extension(ash::khr::video_decode_h264::NAME) - && Self::check_h264_decode_support(&entry, &instance, physical_device) - { - decode_codecs.push(Codec::H264); - debug!("Device {} supports H.264 decode", device_name); - } + let decode_codecs = if decode_queue.is_some() { + query_decode_codecs(&entry, &instance, physical_device) + } else { + Vec::new() + }; + for codec in &decode_codecs { + debug!("Device {} supports {:?} decode", device_name, codec); } // Check if all required encode/decode codecs are supported. @@ -1594,63 +1660,6 @@ impl VideoContext { } } - /// Check if a codec is supported for encoding. - fn check_h264_decode_support( - entry: &ash::Entry, - instance: &ash::Instance, - physical_device: vk::PhysicalDevice, - ) -> bool { - let video_queue = ash::khr::video_queue::Instance::load(entry, instance); - - // H.264 decode profile: High profile, progressive, 8-bit 4:2:0. - let mut h264_profile = vk::VideoDecodeH264ProfileInfoKHR::default() - .std_profile_idc( - ash::vk::native::StdVideoH264ProfileIdc_STD_VIDEO_H264_PROFILE_IDC_HIGH, - ) - .picture_layout(vk::VideoDecodeH264PictureLayoutFlagsKHR::PROGRESSIVE); - - let profile_info = vk::VideoProfileInfoKHR::default() - .video_codec_operation(vk::VideoCodecOperationFlagsKHR::DECODE_H264) - .chroma_subsampling(vk::VideoChromaSubsamplingFlagsKHR::TYPE_420) - .luma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8) - .chroma_bit_depth(vk::VideoComponentBitDepthFlagsKHR::TYPE_8) - .push(&mut h264_profile); - - let mut h264_capabilities = vk::VideoDecodeH264CapabilitiesKHR::default(); - let mut decode_capabilities = vk::VideoDecodeCapabilitiesKHR::default(); - let mut capabilities = vk::VideoCapabilitiesKHR::default() - .push(&mut h264_capabilities) - .push(&mut decode_capabilities); - - let result = unsafe { - (video_queue.fp().get_physical_device_video_capabilities_khr)( - physical_device, - &profile_info, - &mut capabilities, - ) - }; - - match result { - vk::Result::SUCCESS => { - debug!( - "H.264 decode supported: max {}x{}, {} DPB slots", - capabilities.max_coded_extent.width, - capabilities.max_coded_extent.height, - capabilities.max_dpb_slots - ); - true - } - vk::Result::ERROR_VIDEO_PROFILE_CODEC_NOT_SUPPORTED_KHR => { - debug!("H.264 decode not supported on this device"); - false - } - _ => { - warn!("Failed to query H.264 decode capabilities: {:?}", result); - false - } - } - } - /// Check if the selected device supports encoding the given codec. /// /// This only answers for 8-bit 4:2:0; see From fd0bb9fc844ae7cd2f4f1627b0122e7846802056 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 08:51:11 +0100 Subject: [PATCH 08/17] refactor(decoder): share the decode record between codecs --- src/decoder/h264/record.rs | 251 +++----------------------------- src/decoder/h264/session.rs | 12 +- src/decoder/mod.rs | 2 + src/decoder/record.rs | 275 ++++++++++++++++++++++++++++++++++++ 4 files changed, 303 insertions(+), 237 deletions(-) create mode 100644 src/decoder/record.rs diff --git a/src/decoder/h264/record.rs b/src/decoder/h264/record.rs index 58bb1f8b..0f2c5b51 100644 --- a/src/decoder/h264/record.rs +++ b/src/decoder/h264/record.rs @@ -1,244 +1,29 @@ -//! Storage for the structures one H.264 decode command is recorded from. -//! -//! NVIDIA's decoder does not copy `VkVideoDecodeInfoKHR` and the structures -//! hanging off it when the command is recorded: it reads them again at -//! `vkQueueSubmit`. They therefore have to stay alive and unmoved until the -//! submission has finished, which a stack frame in the recording function does -//! not guarantee. Left on the stack they are garbage by submit time, which -//! crashed the driver or made it decode B-pictures from stale reference data. -//! -//! A [`DecodeRecord`] owns all of them. There is one per pipeline slot, and it -//! is rebuilt only once that slot's previous submission has completed. +//! The H.264 structures of a decode command. use ash::vk; -use ash::vk::TaggedStructure; use ash::vk::native::{StdVideoDecodeH264PictureInfo, StdVideoDecodeH264ReferenceInfo}; -use super::dpb::MAX_DPB_SLOTS; +use crate::decoder::record::RecordCodec; -/// Most references one picture can have: every DPB slot but the one being set up. -const MAX_REFS: usize = MAX_DPB_SLOTS; +/// H.264's part of a [`DecodeRecord`](crate::decoder::record::DecodeRecord). +pub(super) struct H264Record; -/// Everything one decode command points at. -/// -/// The Vulkan structures here point at each other and at the plain data beside -/// them, so the whole is a self-referential value. That is sound because it lives in -/// heap storage it is never moved out of once built, and every pointer is rebuilt -/// by [`DecodeRecord::link`] after the last write to what it points at. The -/// `'static` lifetimes on the fields are internal: nothing outside this module -/// can obtain a reference that outlives the record. -/// -/// Every collection is a fixed-size array holding `ref_count` live entries, so -/// nothing is ever reallocated and an element's address does not change however -/// the record is filled. -pub(super) struct DecodeRecord { - // Plain data. - slice_offsets: Vec, - ref_count: usize, - /// DPB slot and image view of each reference. - refs: [(u8, vk::ImageView); MAX_REFS], - ref_std: [StdVideoDecodeH264ReferenceInfo; MAX_REFS], - setup_std: StdVideoDecodeH264ReferenceInfo, - pic_std: StdVideoDecodeH264PictureInfo, +impl RecordCodec for H264Record { + type RefStd = StdVideoDecodeH264ReferenceInfo; + type DpbSlot = vk::VideoDecodeH264DpbSlotInfoKHR<'static>; + type Picture = StdVideoDecodeH264PictureInfo; + type PictureInfo = vk::VideoDecodeH264PictureInfoKHR<'static>; - // Picture resources. - ref_resources: [vk::VideoPictureResourceInfoKHR<'static>; MAX_REFS], - setup_resource: vk::VideoPictureResourceInfoKHR<'static>, - output_resource: vk::VideoPictureResourceInfoKHR<'static>, - - // Codec-specific slot info. The begin and decode commands each chain their - // own copy, since chaining takes the structure mutably. - begin_dpb: [vk::VideoDecodeH264DpbSlotInfoKHR<'static>; MAX_REFS], - decode_dpb: [vk::VideoDecodeH264DpbSlotInfoKHR<'static>; MAX_REFS], - setup_dpb: vk::VideoDecodeH264DpbSlotInfoKHR<'static>, - - // Reference slots. `begin_slots` has room for the slot being set up too. - begin_slots: [vk::VideoReferenceSlotInfoKHR<'static>; MAX_REFS + 1], - decode_slots: [vk::VideoReferenceSlotInfoKHR<'static>; MAX_REFS], - setup_slot: vk::VideoReferenceSlotInfoKHR<'static>, - - // Top level. - pic_info: vk::VideoDecodeH264PictureInfoKHR<'static>, - begin_info: vk::VideoBeginCodingInfoKHR<'static>, - decode_info: vk::VideoDecodeInfoKHR<'static>, -} - -// SAFETY: the raw pointers inside the Vulkan structures only ever point into -// the same record, which is owned by one decoder and used from one thread at a -// time. -unsafe impl Send for DecodeRecord {} - -/// What [`DecodeRecord::link`] needs besides the references already pushed. -pub(super) struct LinkParams { - pub extent: vk::Extent2D, - pub session: vk::VideoSessionKHR, - pub session_params: vk::VideoSessionParametersKHR, - pub bitstream: vk::Buffer, - pub bitstream_range: u64, - /// DPB slot, and view of it, that the picture is reconstructed into. - pub setup_slot: u8, - pub setup_view: vk::ImageView, - /// The decode destination when it is not the DPB slot itself. - pub output_view: Option, - pub setup_std: StdVideoDecodeH264ReferenceInfo, - pub pic_std: StdVideoDecodeH264PictureInfo, -} - -/// Extend a borrow to any lifetime. -/// -/// # Safety -/// `value` must stay alive and in place for as long as the result is used. -unsafe fn stable<'a, T: ?Sized>(value: &T) -> &'a T { - unsafe { &*(value as *const T) } -} - -/// As [`stable`], for a mutable borrow. -/// -/// # Safety -/// As [`stable`], and nothing else may access `value` while the result is used. -unsafe fn stable_mut<'a, T: ?Sized>(value: &mut T) -> &'a mut T { - unsafe { &mut *(value as *mut T) } -} - -impl DecodeRecord { - pub(super) fn new() -> Self { - // SAFETY: the standard structures are plain C data, for which all-zero - // is a valid (if meaningless) value. - let zeroed_ref: StdVideoDecodeH264ReferenceInfo = unsafe { std::mem::zeroed() }; - let (setup_std, pic_std) = (zeroed_ref, unsafe { std::mem::zeroed() }); - Self { - slice_offsets: Vec::new(), - ref_count: 0, - refs: [(0, vk::ImageView::null()); MAX_REFS], - ref_std: [zeroed_ref; MAX_REFS], - setup_std, - pic_std, - ref_resources: [Default::default(); MAX_REFS], - setup_resource: Default::default(), - output_resource: Default::default(), - begin_dpb: [Default::default(); MAX_REFS], - decode_dpb: [Default::default(); MAX_REFS], - setup_dpb: Default::default(), - begin_slots: [Default::default(); MAX_REFS + 1], - decode_slots: [Default::default(); MAX_REFS], - setup_slot: Default::default(), - pic_info: Default::default(), - begin_info: Default::default(), - decode_info: Default::default(), - } - } - - /// Start a new picture, discarding the last one. - /// - /// Only call once the submission that used this record has completed. - pub(super) fn reset(&mut self, slice_offsets: &[u32]) { - self.slice_offsets.clear(); - self.slice_offsets.extend_from_slice(slice_offsets); - self.ref_count = 0; - } - - /// Add a reference picture, in the order the driver is to see it. - pub(super) fn push_reference( - &mut self, - slot: u8, - view: vk::ImageView, - std: StdVideoDecodeH264ReferenceInfo, - ) { - assert!(self.ref_count < MAX_REFS, "too many H.264 references"); - self.refs[self.ref_count] = (slot, view); - self.ref_std[self.ref_count] = std; - self.ref_count += 1; - } - - /// Build the structures from what has been pushed, wiring up every pointer. - /// - /// Order matters: the plain data and the structures pointed at are written - /// before the structures that point at them. - pub(super) fn link(&mut self, p: &LinkParams) { - let n = self.ref_count; - self.setup_std = p.setup_std; - self.pic_std = p.pic_std; - - let resource = |view: vk::ImageView| { - vk::VideoPictureResourceInfoKHR::default() - .coded_offset(vk::Offset2D { x: 0, y: 0 }) - .coded_extent(p.extent) - .base_array_layer(0) - .image_view_binding(view) - }; - for i in 0..n { - self.ref_resources[i] = resource(self.refs[i].1); - } - self.setup_resource = resource(p.setup_view); - self.output_resource = match p.output_view { - None => self.setup_resource, - Some(view) => resource(view), - }; - - // SAFETY (for every `stable`/`stable_mut` below): the target is a field - // of `self`, which is boxed and not written again until the next - // `reset`/`link`, and fixed arrays never move their elements. - unsafe { - for i in 0..n { - let info = vk::VideoDecodeH264DpbSlotInfoKHR::default() - .std_reference_info(stable(&self.ref_std[i])); - self.begin_dpb[i] = info; - self.decode_dpb[i] = info; - } - self.setup_dpb = vk::VideoDecodeH264DpbSlotInfoKHR::default() - .std_reference_info(stable(&self.setup_std)); - - for i in 0..n { - let slot = self.refs[i].0 as i32; - let resource = stable(&self.ref_resources[i]); - self.begin_slots[i] = vk::VideoReferenceSlotInfoKHR::default() - .slot_index(slot) - .picture_resource(resource) - .push(stable_mut(&mut self.begin_dpb[i])); - self.decode_slots[i] = vk::VideoReferenceSlotInfoKHR::default() - .slot_index(slot) - .picture_resource(resource) - .push(stable_mut(&mut self.decode_dpb[i])); - } - // The slot being reconstructed is listed to `begin` with slot index - // -1: it is not active until the decode's setup slot activates it, - // and a non-negative index here must name an already-active slot - // (VUID-vkCmdBeginVideoCodingKHR-slotIndex-07239). -1 still binds - // its picture resource. - self.begin_slots[n] = vk::VideoReferenceSlotInfoKHR::default() - .slot_index(-1) - .picture_resource(stable(&self.setup_resource)); - self.setup_slot = vk::VideoReferenceSlotInfoKHR::default() - .slot_index(p.setup_slot as i32) - .picture_resource(stable(&self.setup_resource)) - .push(stable_mut(&mut self.setup_dpb)); - - self.pic_info = vk::VideoDecodeH264PictureInfoKHR::default() - .std_picture_info(stable(&self.pic_std)) - .slice_offsets(stable(&self.slice_offsets[..])); - - self.begin_info = vk::VideoBeginCodingInfoKHR::default() - .video_session(p.session) - .video_session_parameters(p.session_params) - .reference_slots(stable(&self.begin_slots[..n + 1])); - self.decode_info = vk::VideoDecodeInfoKHR::default() - .src_buffer(p.bitstream) - .src_buffer_offset(0) - .src_buffer_range(p.bitstream_range) - .dst_picture_resource(self.output_resource) - .setup_reference_slot(stable(&self.setup_slot)) - .reference_slots(stable(&self.decode_slots[..n])) - .push(stable_mut(&mut self.pic_info)); - } - } - - /// The `vkCmdBeginVideoCodingKHR` argument. Valid after [`Self::link`]. - pub(super) fn begin_info(&self) -> &vk::VideoBeginCodingInfoKHR<'_> { - &self.begin_info + fn dpb_slot(std: &'static StdVideoDecodeH264ReferenceInfo) -> Self::DpbSlot { + vk::VideoDecodeH264DpbSlotInfoKHR::default().std_reference_info(std) } - /// The `vkCmdDecodeVideoKHR` argument. Valid after [`Self::link`]. - pub(super) fn decode_info(&self) -> &vk::VideoDecodeInfoKHR<'_> { - &self.decode_info + fn picture_info( + picture: &'static mut StdVideoDecodeH264PictureInfo, + offsets: &'static [u32], + ) -> Self::PictureInfo { + vk::VideoDecodeH264PictureInfoKHR::default() + .std_picture_info(picture) + .slice_offsets(offsets) } } diff --git a/src/decoder/h264/session.rs b/src/decoder/h264/session.rs index d1a0e951..4f9940b8 100644 --- a/src/decoder/h264/session.rs +++ b/src/decoder/h264/session.rs @@ -16,10 +16,11 @@ use crate::decoder::h264::dpb::{DecodeDpb, PictureState}; use crate::decoder::h264::parser::{ self, NalType, NalUnit, Pps, SliceHeader, SliceType, Sps, iter_nal_units, }; -use crate::decoder::h264::record::{DecodeRecord, LinkParams}; +use crate::decoder::h264::record::H264Record; use crate::decoder::pipeline::DECODE_PIPELINE_DEPTH; use crate::decoder::plan::StreamNeeds; use crate::decoder::profile::DecodeProfile; +use crate::decoder::record::{DecodeRecord, LinkParams}; use crate::encoder::{BitDepth, ChromaFormat}; use crate::error::{PixelForgeError, Result}; use crate::vulkan::VideoContext; @@ -54,7 +55,7 @@ pub(crate) struct H264Decoder { /// What each pipeline slot's decode command was recorded from, indexed like /// the pipeline's slots. Never resized or moved out of: the records point /// into themselves. - records: Box<[DecodeRecord]>, + records: Box<[DecodeRecord]>, /// `max_num_reorder_frames` for the active SPS: how deep the reorder buffer /// may hold pictures before emitting. Set by `activate`, which also sizes @@ -100,8 +101,10 @@ impl H264Decoder { active_sps: None, active_pps: None, dpb: None, + // SAFETY: the picture info is plain C data, for which all-zero is + // a valid (if meaningless) value. records: (0..DECODE_PIPELINE_DEPTH) - .map(|_| DecodeRecord::new()) + .map(|_| DecodeRecord::new(unsafe { std::mem::zeroed() })) .collect(), reorder_depth: 0, }) @@ -549,6 +552,8 @@ impl H264Decoder { PicOrderCnt: [state.poc, state.poc], }; + *record.picture_mut() = pic_std; + // The decode destination is the DPB slot itself when the implementation // supports coincide, otherwise the separate output image. record.link(&LinkParams { @@ -564,7 +569,6 @@ impl H264Decoder { setup_view: active.dpb_entry(slot).2, output_view: active.output_image.map(|(_, _, view)| view), setup_std, - pic_std, }); unsafe { diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index d63ad617..f1df6edd 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -53,6 +53,8 @@ pub(crate) mod pipeline; /// Turning what a stream needs into a session plan. pub(crate) mod plan; pub(crate) mod profile; +/// The structures a decode command is recorded from. +pub(crate) mod record; /// Readback and copies, all of which run on the transfer queue. pub(crate) mod transfer; diff --git a/src/decoder/record.rs b/src/decoder/record.rs new file mode 100644 index 00000000..3734d9c4 --- /dev/null +++ b/src/decoder/record.rs @@ -0,0 +1,275 @@ +//! Storage for the structures one decode command is recorded from. +//! +//! NVIDIA's decoder does not copy `VkVideoDecodeInfoKHR` and the structures +//! hanging off it when the command is recorded: it reads them again at +//! `vkQueueSubmit`. They therefore have to stay alive and unmoved until the +//! submission has finished, which a stack frame in the recording function does +//! not guarantee. Left on the stack they are garbage by submit time, which +//! crashed the driver or made it decode B-pictures from stale reference data. +//! +//! A [`DecodeRecord`] owns all of them. There is one per pipeline slot, and it +//! is rebuilt only once that slot's previous submission has completed. The +//! structures every codec shares live here; a [`RecordCodec`] supplies the +//! codec's own. + +use ash::vk; +use ash::vk::TaggedStructure; + +/// Most references one picture can have: every DPB slot but the one being set +/// up, for the codec with the most slots (H.264 and H.265, 16 references). +const MAX_REFS: usize = 17; + +/// The codec-specific structures of a decode command. +pub(crate) trait RecordCodec { + /// The Std reference info of one DPB slot. + type RefStd: Copy + 'static; + /// The codec's `Vk...DpbSlotInfoKHR`, which points at a [`Self::RefStd`]. + type DpbSlot: Copy + + Default + + 'static + + vk::Extends> + + TaggedStructure<'static>; + /// What the codec's picture info points at: the Std picture info and + /// anything it points at in turn. Kept in the record, so it stays in place + /// until the submission has completed. + type Picture: 'static; + /// The codec's `Vk...PictureInfoKHR`. + type PictureInfo: Default + + 'static + + vk::Extends> + + TaggedStructure<'static>; + + /// A DPB slot info pointing at `std`. + fn dpb_slot(std: &'static Self::RefStd) -> Self::DpbSlot; + + /// The picture info for `picture`, whose slices or tiles start at + /// `offsets` in the bitstream buffer. + /// + /// May also wire the pointers inside `picture` itself. + fn picture_info( + picture: &'static mut Self::Picture, + offsets: &'static [u32], + ) -> Self::PictureInfo; +} + +/// Everything one decode command points at. +/// +/// The Vulkan structures here point at each other and at the plain data beside +/// them, so the whole is a self-referential value. That is sound because it lives in +/// heap storage it is never moved out of once built, and every pointer is rebuilt +/// by [`DecodeRecord::link`] after the last write to what it points at. The +/// `'static` lifetimes on the fields are internal: nothing outside this module +/// can obtain a reference that outlives the record. +/// +/// Every collection is a fixed-size array holding `ref_count` live entries, so +/// nothing is ever reallocated and an element's address does not change however +/// the record is filled. +pub(crate) struct DecodeRecord { + // Plain data. + offsets: Vec, + ref_count: usize, + /// DPB slot and image view of each reference. + refs: [(u8, vk::ImageView); MAX_REFS], + ref_std: [C::RefStd; MAX_REFS], + setup_std: C::RefStd, + picture: C::Picture, + + // Picture resources. + ref_resources: [vk::VideoPictureResourceInfoKHR<'static>; MAX_REFS], + setup_resource: vk::VideoPictureResourceInfoKHR<'static>, + output_resource: vk::VideoPictureResourceInfoKHR<'static>, + + // Codec-specific slot info. The begin and decode commands each chain their + // own copy, since chaining takes the structure mutably. + begin_dpb: [C::DpbSlot; MAX_REFS], + decode_dpb: [C::DpbSlot; MAX_REFS], + setup_dpb: C::DpbSlot, + + // Reference slots. `begin_slots` has room for the slot being set up too. + begin_slots: [vk::VideoReferenceSlotInfoKHR<'static>; MAX_REFS + 1], + decode_slots: [vk::VideoReferenceSlotInfoKHR<'static>; MAX_REFS], + setup_slot: vk::VideoReferenceSlotInfoKHR<'static>, + + // Top level. + pic_info: C::PictureInfo, + begin_info: vk::VideoBeginCodingInfoKHR<'static>, + decode_info: vk::VideoDecodeInfoKHR<'static>, +} + +// SAFETY: the raw pointers inside the Vulkan structures only ever point into +// the same record, which is owned by one decoder and used from one thread at a +// time. +unsafe impl Send for DecodeRecord {} + +/// What [`DecodeRecord::link`] needs besides the references already pushed. +pub(crate) struct LinkParams { + pub extent: vk::Extent2D, + pub session: vk::VideoSessionKHR, + pub session_params: vk::VideoSessionParametersKHR, + pub bitstream: vk::Buffer, + pub bitstream_range: u64, + /// DPB slot, and view of it, that the picture is reconstructed into. + pub setup_slot: u8, + pub setup_view: vk::ImageView, + /// The decode destination when it is not the DPB slot itself. + pub output_view: Option, + pub setup_std: C::RefStd, +} + +/// Extend a borrow to any lifetime. +/// +/// # Safety +/// `value` must stay alive and in place for as long as the result is used. +unsafe fn stable<'a, T: ?Sized>(value: &T) -> &'a T { + unsafe { &*(value as *const T) } +} + +/// As [`stable`], for a mutable borrow. +/// +/// # Safety +/// As [`stable`], and nothing else may access `value` while the result is used. +unsafe fn stable_mut<'a, T: ?Sized>(value: &mut T) -> &'a mut T { + unsafe { &mut *(value as *mut T) } +} + +impl DecodeRecord { + /// An empty record. `picture` is the codec's picture storage, which + /// [`Self::link`] overwrites. + pub(crate) fn new(picture: C::Picture) -> Self { + // SAFETY: the standard structures are plain C data, for which all-zero + // is a valid (if meaningless) value. + let zeroed_ref: C::RefStd = unsafe { std::mem::zeroed() }; + Self { + offsets: Vec::new(), + ref_count: 0, + refs: [(0, vk::ImageView::null()); MAX_REFS], + ref_std: [zeroed_ref; MAX_REFS], + setup_std: zeroed_ref, + picture, + ref_resources: [Default::default(); MAX_REFS], + setup_resource: Default::default(), + output_resource: Default::default(), + begin_dpb: [Default::default(); MAX_REFS], + decode_dpb: [Default::default(); MAX_REFS], + setup_dpb: Default::default(), + begin_slots: [Default::default(); MAX_REFS + 1], + decode_slots: [Default::default(); MAX_REFS], + setup_slot: Default::default(), + pic_info: Default::default(), + begin_info: Default::default(), + decode_info: Default::default(), + } + } + + /// Start a new picture, discarding the last one. + /// + /// Only call once the submission that used this record has completed. + /// `offsets` are where the picture's slices or tiles start in the + /// bitstream buffer. + pub(crate) fn reset(&mut self, offsets: &[u32]) { + self.offsets.clear(); + self.offsets.extend_from_slice(offsets); + self.ref_count = 0; + } + + /// The codec's picture storage, to fill before [`Self::link`]. + pub(crate) fn picture_mut(&mut self) -> &mut C::Picture { + &mut self.picture + } + + /// Add a reference picture, in the order the driver is to see it. + pub(crate) fn push_reference(&mut self, slot: u8, view: vk::ImageView, std: C::RefStd) { + assert!(self.ref_count < MAX_REFS, "too many references"); + self.refs[self.ref_count] = (slot, view); + self.ref_std[self.ref_count] = std; + self.ref_count += 1; + } + + /// Build the structures from what has been pushed, wiring up every pointer. + /// + /// Order matters: the plain data and the structures pointed at are written + /// before the structures that point at them. + pub(crate) fn link(&mut self, p: &LinkParams) { + let n = self.ref_count; + self.setup_std = p.setup_std; + + let resource = |view: vk::ImageView| { + vk::VideoPictureResourceInfoKHR::default() + .coded_offset(vk::Offset2D { x: 0, y: 0 }) + .coded_extent(p.extent) + .base_array_layer(0) + .image_view_binding(view) + }; + for i in 0..n { + self.ref_resources[i] = resource(self.refs[i].1); + } + self.setup_resource = resource(p.setup_view); + self.output_resource = match p.output_view { + None => self.setup_resource, + Some(view) => resource(view), + }; + + // SAFETY (for every `stable`/`stable_mut` below): the target is a field + // of `self`, which is boxed and not written again until the next + // `reset`/`link`, and fixed arrays never move their elements. + unsafe { + for i in 0..n { + let info = C::dpb_slot(stable(&self.ref_std[i])); + self.begin_dpb[i] = info; + self.decode_dpb[i] = info; + } + self.setup_dpb = C::dpb_slot(stable(&self.setup_std)); + + for i in 0..n { + let slot = self.refs[i].0 as i32; + let resource = stable(&self.ref_resources[i]); + self.begin_slots[i] = vk::VideoReferenceSlotInfoKHR::default() + .slot_index(slot) + .picture_resource(resource) + .push(stable_mut(&mut self.begin_dpb[i])); + self.decode_slots[i] = vk::VideoReferenceSlotInfoKHR::default() + .slot_index(slot) + .picture_resource(resource) + .push(stable_mut(&mut self.decode_dpb[i])); + } + // The slot being reconstructed is listed to `begin` with slot index + // -1: it is not active until the decode's setup slot activates it, + // and a non-negative index here must name an already-active slot + // (VUID-vkCmdBeginVideoCodingKHR-slotIndex-07239). -1 still binds + // its picture resource. + self.begin_slots[n] = vk::VideoReferenceSlotInfoKHR::default() + .slot_index(-1) + .picture_resource(stable(&self.setup_resource)); + self.setup_slot = vk::VideoReferenceSlotInfoKHR::default() + .slot_index(p.setup_slot as i32) + .picture_resource(stable(&self.setup_resource)) + .push(stable_mut(&mut self.setup_dpb)); + + self.pic_info = + C::picture_info(stable_mut(&mut self.picture), stable(&self.offsets[..])); + + self.begin_info = vk::VideoBeginCodingInfoKHR::default() + .video_session(p.session) + .video_session_parameters(p.session_params) + .reference_slots(stable(&self.begin_slots[..n + 1])); + self.decode_info = vk::VideoDecodeInfoKHR::default() + .src_buffer(p.bitstream) + .src_buffer_offset(0) + .src_buffer_range(p.bitstream_range) + .dst_picture_resource(self.output_resource) + .setup_reference_slot(stable(&self.setup_slot)) + .reference_slots(stable(&self.decode_slots[..n])) + .push(stable_mut(&mut self.pic_info)); + } + } + + /// The `vkCmdBeginVideoCodingKHR` argument. Valid after [`Self::link`]. + pub(crate) fn begin_info(&self) -> &vk::VideoBeginCodingInfoKHR<'_> { + &self.begin_info + } + + /// The `vkCmdDecodeVideoKHR` argument. Valid after [`Self::link`]. + pub(crate) fn decode_info(&self) -> &vk::VideoDecodeInfoKHR<'_> { + &self.decode_info + } +} From f2ba4744e242a77c00f3e3fb0f17e88fcdc91092 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 08:51:59 +0100 Subject: [PATCH 09/17] refactor(decoder): share coding commands and picture output between codecs --- src/decoder/common.rs | 76 ++++++++++++++++++++++++++++++++++++- src/decoder/h264/session.rs | 64 ++----------------------------- 2 files changed, 79 insertions(+), 61 deletions(-) diff --git a/src/decoder/common.rs b/src/decoder/common.rs index c54bb6ab..8d5ebf96 100644 --- a/src/decoder/common.rs +++ b/src/decoder/common.rs @@ -22,10 +22,11 @@ use std::sync::Arc; use ash::vk; use crate::decoder::DecodedFrame; -use crate::decoder::frames::SlotPins; +use crate::decoder::frames::{DecodedPicture, SlotPins}; use crate::decoder::pipeline::DecodePipeline; use crate::decoder::plan::SessionPlan; use crate::decoder::profile::DecodeProfile; +use crate::decoder::record::{DecodeRecord, RecordCodec}; use crate::encoder::{BitDepth, ChromaFormat}; use crate::error::{PixelForgeError, Result}; use crate::video::{ @@ -625,6 +626,79 @@ impl DecoderCommon { .submit_decode(&device, self.context.sync2(), queue) } + /// Record `vkCmdBeginVideoCoding`, `vkCmdDecodeVideo` and + /// `vkCmdEndVideoCoding` from a linked `record`, resetting the session + /// first if this is its first decode. + pub fn record_coding(&self, record: &DecodeRecord) { + let command_buffer = self.decode_command_buffer(); + // A session's first decode sees no active slot. + let first_use = self + .session + .as_ref() + .is_none_or(|s| !s.dpb_slot_active.iter().any(|&a| a)); + unsafe { + self.video_queue_fn + .cmd_begin_video_coding(command_buffer, record.begin_info()); + // On the first use of the session, all DPB slots must be reset. + if first_use { + let control = vk::VideoCodingControlInfoKHR::default() + .flags(vk::VideoCodingControlFlagsKHR::RESET); + self.video_queue_fn + .cmd_control_video_coding(command_buffer, &control); + } + self.video_decode_fn + .cmd_decode_video(command_buffer, record.decode_info()); + self.video_queue_fn + .cmd_end_video_coding(command_buffer, &vk::VideoEndCodingInfoKHR::default()); + } + } + + /// Mark `slot` as decoded into, and describe the picture now in it for + /// output. Call after the picture's decode is submitted. + /// + /// `width` and `height` are the visible (cropped) size. + pub fn decoded_picture( + &mut self, + slot: u8, + width: u32, + height: u32, + pts: u64, + display_order: i32, + is_keyframe: bool, + ) -> DecodedPicture { + let picture_layout = self.picture_layout(); + let output_layout = self.output_layout(); + let generation = self.generation; + let session = self.session.as_mut().expect("a picture was just decoded"); + session.dpb_slot_active[slot as usize] = true; + let (image, image_view, layout, array_layer) = if session.coincide { + let (image, layer) = session.dpb_image_for_slot(slot); + (image, session.dpb_entry(slot).2, picture_layout, layer) + } else { + let (image, _, view) = session + .output_image + .expect("non-coincide implies output image"); + (image, view, output_layout, 0) + }; + DecodedPicture { + slot, + image, + image_view, + layout, + array_layer, + chroma_format: session.chroma_format, + bit_depth: session.bit_depth, + generation, + width, + height, + coded_width: session.coded_width, + coded_height: session.coded_height, + pts, + display_order, + is_keyframe, + } + } + /// Close off the current picture and hand `frames` to the completion thread. pub fn end_picture(&mut self, frames: Vec) { self.pipeline.end_picture(frames); diff --git a/src/decoder/h264/session.rs b/src/decoder/h264/session.rs index 4f9940b8..71973dbe 100644 --- a/src/decoder/h264/session.rs +++ b/src/decoder/h264/session.rs @@ -430,41 +430,10 @@ impl H264Decoder { self.common.submit_decode()?; // --- Publish the frame, then retire references --- - let active = self.common.session.as_mut().expect("active"); - active.dpb_slot_active[slot as usize] = true; - - let picture_layout = self.common.picture_layout(); - let output_layout = self.common.output_layout(); - let generation = self.common.generation; - let active = self.common.session.as_mut().expect("active"); - let (image, image_view, layout, array_layer) = if active.coincide { - let (image, layer) = active.dpb_image_for_slot(slot); - (image, active.dpb_entry(slot).2, picture_layout, layer) - } else { - let (image, _, view) = active - .output_image - .expect("non-coincide implies output image"); - (image, view, output_layout, 0) - }; - let (width, height) = sps.display_dimensions(); - let frame = DecodedPicture { - slot, - image, - image_view, - layout, - array_layer, - chroma_format: active.chroma_format, - bit_depth: active.bit_depth, - generation, - width, - height, - coded_width: active.coded_width, - coded_height: active.coded_height, - pts, - display_order: state.poc, - is_keyframe: state.is_idr, - }; + let frame = self + .common + .decoded_picture(slot, width, height, pts, state.poc, state.is_idr); self.dpb .as_mut() @@ -484,7 +453,6 @@ impl H264Decoder { slice_offsets: &[u32], ) -> Result<()> { let active = self.common.session.as_ref().expect("active"); - let command_buffer = self.common.decode_command_buffer(); // The slot's previous decode has finished (`begin_decode_commands` // waited for it), so its record can be rewritten. @@ -571,31 +539,7 @@ impl H264Decoder { setup_std, }); - unsafe { - self.common - .video_queue_fn - .cmd_begin_video_coding(command_buffer, record.begin_info()); - } - - // On the first use of the session, all DPB slots must be reset. - if !active.dpb_slot_active.iter().any(|&a| a) { - let control = vk::VideoCodingControlInfoKHR::default() - .flags(vk::VideoCodingControlFlagsKHR::RESET); - unsafe { - self.common - .video_queue_fn - .cmd_control_video_coding(command_buffer, &control); - } - } - - unsafe { - self.common - .video_decode_fn - .cmd_decode_video(command_buffer, record.decode_info()); - self.common - .video_queue_fn - .cmd_end_video_coding(command_buffer, &vk::VideoEndCodingInfoKHR::default()); - } + self.common.record_coding(record); Ok(()) } } From f170ef643a0aaf93b24d13393e60533e1dadd4f0 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 08:53:20 +0100 Subject: [PATCH 10/17] feat(examples): pick the decode codec from the file extension --- examples/common/mod.rs | 77 +++++++++++++++++++++++++++++++++++---- examples/decode.rs | 24 ++++++------ examples/sample_frame.rs | 16 ++++---- examples/sample_planes.rs | 16 ++++---- 4 files changed, 95 insertions(+), 38 deletions(-) diff --git a/examples/common/mod.rs b/examples/common/mod.rs index 7a6fbe46..c79d2ce7 100644 --- a/examples/common/mod.rs +++ b/examples/common/mod.rs @@ -17,8 +17,8 @@ //! from the same thread. use ash::vk; -use pixelforge::decoder::{DecodedFrame, Decoder, FramePoll}; -use pixelforge::encoder::BitDepth; +use pixelforge::decoder::{DecodeConfig, DecodedFrame, Decoder, FramePoll}; +use pixelforge::encoder::{BitDepth, Codec}; use pixelforge::vulkan::VideoContext; use std::fs::File; use std::io::Write; @@ -26,20 +26,81 @@ use std::io::Write; /// Bytes handed to the decoder per call, standing in for a network read. pub const CHUNK_SIZE: usize = 64 * 1024; -/// Drive `decoder` over `stream`, calling `on_frame` for every frame it emits. +/// A coded stream read from a file. +pub enum Input { + /// A raw stream that the decoder frames itself: Annex B for H.264 and + /// H.265, low-overhead OBUs for AV1. + ByteStream(Vec), + /// Whole coded frames, as a container gives them (IVF). + Frames(Vec>), +} + +/// Read `path`, and pick the codec from its extension: `.264`/`.h264`, +/// `.265`/`.h265`/`.hevc`, `.obu`/`.av1` or `.ivf` (AV1 in IVF). +pub fn open_input(path: &str) -> Result<(Codec, Input), Box> { + let extension = std::path::Path::new(path) + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_ascii_lowercase(); + let data = std::fs::read(path)?; + Ok(match extension.as_str() { + "264" | "h264" => (Codec::H264, Input::ByteStream(data)), + "265" | "h265" | "hevc" => (Codec::H265, Input::ByteStream(data)), + "obu" | "av1" => (Codec::AV1, Input::ByteStream(data)), + "ivf" => (Codec::AV1, Input::Frames(ivf_frames(&data)?)), + other => return Err(format!("unknown stream extension {other:?}").into()), + }) +} + +/// The frames of an IVF file: a 32-byte file header, then each frame behind a +/// 12-byte header holding its size and timestamp. +fn ivf_frames(data: &[u8]) -> Result>, Box> { + if data.len() < 32 || &data[..4] != b"DKIF" { + return Err("not an IVF file".into()); + } + let header_len = u16::from_le_bytes([data[6], data[7]]) as usize; + let mut frames = Vec::new(); + let mut pos = header_len; + while pos + 12 <= data.len() { + let size = u32::from_le_bytes(data[pos..pos + 4].try_into()?) as usize; + let start = pos + 12; + let frame = data.get(start..start + size).ok_or("truncated IVF frame")?; + frames.push(frame.to_vec()); + pos = start + size; + } + Ok(frames) +} + +/// The decode configuration `input` needs: byte-stream framing for a raw +/// stream, frame-aligned for frames from a container. +pub fn decode_config(codec: Codec, input: &Input) -> DecodeConfig { + match input { + Input::ByteStream(_) => DecodeConfig::new(codec).with_byte_stream(), + Input::Frames(_) => DecodeConfig::new(codec), + } +} + +/// Drive `decoder` over `input`, calling `on_frame` for every frame it emits. /// -/// Frames are polled after each chunk and again after `finish`, so the -/// in-stream and end-of-stream paths share one callback. `NeedsKeyframe` is not +/// A raw stream is fed in chunks of [`CHUNK_SIZE`], standing in for network +/// reads; frames from a container are fed one per call. Frames are polled +/// after each call and again after `finish`, so the in-stream and +/// end-of-stream paths share one callback. `NeedsKeyframe` is not /// special-cased: polling then is a no-op when nothing is ready. -pub fn decode_stream( +pub fn decode_input( decoder: &mut Decoder, - stream: &[u8], + input: &Input, mut on_frame: F, ) -> Result<(), Box> where F: FnMut(DecodedFrame) -> Result<(), Box>, { - for (i, chunk) in stream.chunks(CHUNK_SIZE).enumerate() { + let chunks: Box> = match input { + Input::ByteStream(stream) => Box::new(stream.chunks(CHUNK_SIZE)), + Input::Frames(frames) => Box::new(frames.iter().map(Vec::as_slice)), + }; + for (i, chunk) in chunks.enumerate() { decoder.decode(chunk, i as u64)?; while let FramePoll::Frame(frame) = decoder.try_next_frame()? { on_frame(frame)?; diff --git a/examples/decode.rs b/examples/decode.rs index 25afc8a0..58aac2e0 100644 --- a/examples/decode.rs +++ b/examples/decode.rs @@ -1,4 +1,7 @@ -//! Decode an H.264 Annex B stream to raw YUV. +//! Decode an H.264, H.265 or AV1 stream to raw YUV. +//! +//! The codec comes from the file extension: `.264`, `.265`/`.hevc`, `.obu` +//! (AV1 OBUs) or `.ivf` (AV1 in IVF). //! //! Usage: //! cargo run --example decode -- input.264 [output.yuv] @@ -13,10 +16,9 @@ use std::fs::File; #[allow(dead_code)] mod common; -use common::{Readback, decode_stream, write_nv12}; +use common::{Readback, decode_input, write_nv12}; -use pixelforge::decoder::{DecodeConfig, Decoder}; -use pixelforge::encoder::Codec; +use pixelforge::decoder::Decoder; use pixelforge::vulkan::VideoContextBuilder; fn main() -> Result<(), Box> { @@ -24,10 +26,11 @@ fn main() -> Result<(), Box> { let mut args = std::env::args().skip(1); let input_path = args.next().unwrap_or_else(|| { - eprintln!("usage: decode [output.yuv]"); + eprintln!("usage: decode [output.yuv]"); std::process::exit(1); }); let output_path = args.next(); + let (codec, input) = common::open_input(&input_path)?; // PIXELFORGE_VALIDATION=1 enables the Vulkan validation layers, which // report the exact offending call for any API misuse. @@ -36,7 +39,7 @@ fn main() -> Result<(), Box> { // hardware that would otherwise take the zero-copy path. let mut builder = VideoContextBuilder::new() .app_name("pixelforge-decode") - .require_decode(Codec::H264) + .require_decode(codec) .enable_validation(validation); if std::env::var("PIXELFORGE_NO_UNIFIED_LAYOUTS").is_ok() { builder = builder.without_unified_image_layouts(); @@ -59,9 +62,7 @@ fn main() -> Result<(), Box> { // sized for one frame per call, so raise it: past the budget the decoder // copies pictures out instead of handing over its own images, which is // still correct but gives up the zero-copy path. - let config = DecodeConfig::new(Codec::H264) - .with_byte_stream() - .with_output_depth(8); + let config = common::decode_config(codec, &input).with_output_depth(8); let mut output = output_path.as_ref().map(File::create).transpose()?; // Readback lives in the consumer, not in pixelforge: the decoder hands over // a GPU image and a renderer would sample it in place. See examples/common. @@ -71,7 +72,6 @@ fn main() -> Result<(), Box> { .then(|| Readback::new(&context)) .transpose()?; let mut decoder = Decoder::new(context, config)?; - let stream = std::fs::read(&input_path)?; let start = std::time::Instant::now(); let mut frame_count = 0usize; @@ -83,8 +83,8 @@ fn main() -> Result<(), Box> { // flight, not that there are none, so they are collected on a later pass. // A renderer would instead `split()` the decoder and await `next_frame` on // its own thread, which is what the `Decoder::split` docs show. - // `common::decode_stream` owns that loop. - decode_stream(&mut decoder, &stream, |frame| { + // `common::decode_input` owns that loop. + decode_input(&mut decoder, &input, |frame| { write_nv12(&frame, &mut readback, &mut output)?; // A rebuilt session means a new set of images, so anything caching // per-image state has to notice. Reporting it here is what makes the diff --git a/examples/sample_frame.rs b/examples/sample_frame.rs index 371ccc58..894eaeb9 100644 --- a/examples/sample_frame.rs +++ b/examples/sample_frame.rs @@ -26,13 +26,12 @@ use std::fs::File; use std::io::Write; use ash::vk::{self, TaggedStructure as _}; -use pixelforge::decoder::{DecodeConfig, DecodedFrame, Decoder}; -use pixelforge::encoder::Codec; +use pixelforge::decoder::{DecodedFrame, Decoder}; use pixelforge::vulkan::{VideoContext, VideoContextBuilder}; #[allow(dead_code)] mod common; -use common::decode_stream; +use common::decode_input; const SHADER: &[u8] = include_bytes!("shader/sample_frame.spv"); @@ -41,15 +40,16 @@ fn main() -> Result<(), Box> { let mut args = std::env::args().skip(1); let input_path = args.next().unwrap_or_else(|| { - eprintln!("usage: sample_frame [output.rgba]"); + eprintln!("usage: sample_frame [output.rgba]"); std::process::exit(1); }); let output_path = args.next(); + let (codec, input) = common::open_input(&input_path)?; let validation = std::env::var("PIXELFORGE_VALIDATION").is_ok(); let context = VideoContextBuilder::new() .app_name("pixelforge-sample") - .require_decode(Codec::H264) + .require_decode(codec) .enable_validation(validation) .build()?; @@ -63,12 +63,10 @@ fn main() -> Result<(), Box> { // picture's sharing set. Without this, sampling a decoded image from a // family the decoder did not share with is undefined. let consumer_family = context.compute_queue_family(); - let config = DecodeConfig::new(Codec::H264) - .with_byte_stream() + let config = common::decode_config(codec, &input) .with_output_depth(8) .with_consumer_queue_family(consumer_family); - let stream = std::fs::read(&input_path)?; let mut output = output_path.as_ref().map(File::create).transpose()?; // Built lazily: the picture format is only known once the stream's first // parameter set has been parsed, and the ycbcr conversion depends on it. @@ -78,7 +76,7 @@ fn main() -> Result<(), Box> { let mut count = 0usize; let start = std::time::Instant::now(); - decode_stream(&mut decoder, &stream, |frame| { + decode_input(&mut decoder, &input, |frame| { let sampler = match &mut sampler { Some(s) => s, none => none.insert(Sampler::new(&context, consumer_family, &frame)?), diff --git a/examples/sample_planes.rs b/examples/sample_planes.rs index bc71d48f..edb34db1 100644 --- a/examples/sample_planes.rs +++ b/examples/sample_planes.rs @@ -27,11 +27,10 @@ #[allow(dead_code)] mod common; -use common::decode_stream; +use common::decode_input; use ash::vk::{self, TaggedStructure as _}; -use pixelforge::decoder::{DecodeConfig, DecodedFrame, Decoder}; -use pixelforge::encoder::Codec; +use pixelforge::decoder::{DecodedFrame, Decoder}; use pixelforge::vulkan::{VideoContext, VideoContextBuilder}; use std::fs::File; use std::io::Write; @@ -43,15 +42,16 @@ fn main() -> Result<(), Box> { let mut args = std::env::args().skip(1); let input_path = args.next().unwrap_or_else(|| { - eprintln!("usage: sample_planes [output.yuv]"); + eprintln!("usage: sample_planes [output.yuv]"); std::process::exit(1); }); let output_path = args.next(); + let (codec, input) = common::open_input(&input_path)?; let validation = std::env::var("PIXELFORGE_VALIDATION").is_ok(); let context = VideoContextBuilder::new() .app_name("pixelforge-planes") - .require_decode(Codec::H264) + .require_decode(codec) .enable_validation(validation) .build()?; println!( @@ -61,19 +61,17 @@ fn main() -> Result<(), Box> { ); let consumer_family = context.compute_queue_family(); - let config = DecodeConfig::new(Codec::H264) - .with_byte_stream() + let config = common::decode_config(codec, &input) .with_output_depth(8) .with_consumer_queue_family(consumer_family); - let stream = std::fs::read(&input_path)?; let mut output = output_path.as_ref().map(File::create).transpose()?; let mut planes: Option = None; let mut decoder = Decoder::new(context.clone(), config)?; let mut count = 0usize; let start = std::time::Instant::now(); - decode_stream(&mut decoder, &stream, |frame| { + decode_input(&mut decoder, &input, |frame| { if !frame.plane_views { return Err( "this device does not allow per-plane views of decoded pictures; \ From 71c2cb15370d3226cb182d45509a3a41cf646c28 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 12:23:10 +0100 Subject: [PATCH 11/17] feat(decoder): add the AV1 OBU parser --- src/decoder/av1/bits.rs | 252 ++++++++ src/decoder/av1/consts.rs | 66 +++ src/decoder/av1/frame_header.rs | 663 +++++++++++++++++++++ src/decoder/av1/mod.rs | 39 ++ src/decoder/av1/obu.rs | 362 ++++++++++++ src/decoder/av1/params.rs | 773 +++++++++++++++++++++++++ src/decoder/av1/parser.rs | 328 +++++++++++ src/decoder/av1/refs.rs | 377 ++++++++++++ src/decoder/av1/sequence.rs | 355 ++++++++++++ src/decoder/av1/std_info.rs | 351 +++++++++++ src/decoder/av1/stream_tests.rs | 621 ++++++++++++++++++++ src/decoder/av1/tile.rs | 335 +++++++++++ src/decoder/mod.rs | 3 + tests/data/av1/aom_10bit.expected | 35 ++ tests/data/av1/aom_10bit.obu | Bin 0 -> 14648 bytes tests/data/av1/aom_altref.expected | 36 ++ tests/data/av1/aom_altref.obu | Bin 0 -> 15090 bytes tests/data/av1/aom_annexb.obu | Bin 0 -> 3191 bytes tests/data/av1/aom_errres.expected | 42 ++ tests/data/av1/aom_errres.obu | Bin 0 -> 31841 bytes tests/data/av1/aom_grain.expected | 35 ++ tests/data/av1/aom_grain.obu | Bin 0 -> 17189 bytes tests/data/av1/aom_mono.expected | 28 + tests/data/av1/aom_mono.obu | Bin 0 -> 7476 bytes tests/data/av1/aom_resize.expected | 37 ++ tests/data/av1/aom_resize.obu | Bin 0 -> 17301 bytes tests/data/av1/aom_superres.expected | 37 ++ tests/data/av1/aom_superres.obu | Bin 0 -> 27475 bytes tests/data/av1/aom_tilegroups.expected | 36 ++ tests/data/av1/aom_tilegroups.obu | Bin 0 -> 17845 bytes tests/data/av1/aom_tiles.expected | 36 ++ tests/data/av1/aom_tiles.obu | Bin 0 -> 15932 bytes tests/data/av1/extract_trace.py | 129 +++++ tests/data/av1/svt_grain.expected | 37 ++ tests/data/av1/svt_grain.obu | Bin 0 -> 28734 bytes 35 files changed, 5013 insertions(+) create mode 100644 src/decoder/av1/bits.rs create mode 100644 src/decoder/av1/consts.rs create mode 100644 src/decoder/av1/frame_header.rs create mode 100644 src/decoder/av1/mod.rs create mode 100644 src/decoder/av1/obu.rs create mode 100644 src/decoder/av1/params.rs create mode 100644 src/decoder/av1/parser.rs create mode 100644 src/decoder/av1/refs.rs create mode 100644 src/decoder/av1/sequence.rs create mode 100644 src/decoder/av1/std_info.rs create mode 100644 src/decoder/av1/stream_tests.rs create mode 100644 src/decoder/av1/tile.rs create mode 100644 tests/data/av1/aom_10bit.expected create mode 100644 tests/data/av1/aom_10bit.obu create mode 100644 tests/data/av1/aom_altref.expected create mode 100644 tests/data/av1/aom_altref.obu create mode 100644 tests/data/av1/aom_annexb.obu create mode 100644 tests/data/av1/aom_errres.expected create mode 100644 tests/data/av1/aom_errres.obu create mode 100644 tests/data/av1/aom_grain.expected create mode 100644 tests/data/av1/aom_grain.obu create mode 100644 tests/data/av1/aom_mono.expected create mode 100644 tests/data/av1/aom_mono.obu create mode 100644 tests/data/av1/aom_resize.expected create mode 100644 tests/data/av1/aom_resize.obu create mode 100644 tests/data/av1/aom_superres.expected create mode 100644 tests/data/av1/aom_superres.obu create mode 100644 tests/data/av1/aom_tilegroups.expected create mode 100644 tests/data/av1/aom_tilegroups.obu create mode 100644 tests/data/av1/aom_tiles.expected create mode 100644 tests/data/av1/aom_tiles.obu create mode 100644 tests/data/av1/extract_trace.py create mode 100644 tests/data/av1/svt_grain.expected create mode 100644 tests/data/av1/svt_grain.obu diff --git a/src/decoder/av1/bits.rs b/src/decoder/av1/bits.rs new file mode 100644 index 00000000..65bc3c5d --- /dev/null +++ b/src/decoder/av1/bits.rs @@ -0,0 +1,252 @@ +//! Bit reader for AV1 syntax (AV1 specification section 4.10 and 8.1). +//! +//! AV1 has no emulation prevention, so unlike the H.26x reader in +//! `crate::decoder::bitreader` this reads the bytes exactly as given. All +//! reads are bounds-checked and fail with `InvalidInput` past the end, so a +//! truncated or corrupt OBU can never panic the parser. + +use crate::error::{PixelForgeError, Result}; + +/// MSB-first bit reader over one OBU payload (or any byte slice). +#[derive(Debug, Clone)] +pub(crate) struct BitReader<'a> { + data: &'a [u8], + /// Position in bits from the start of `data`. + pos: usize, +} + +fn truncated() -> PixelForgeError { + PixelForgeError::InvalidInput("AV1: bitstream ended inside a syntax element".to_string()) +} + +impl<'a> BitReader<'a> { + pub fn new(data: &'a [u8]) -> Self { + Self { data, pos: 0 } + } + + /// Current position in bits (the spec's `get_position()`). + pub fn position(&self) -> usize { + self.pos + } + + /// Current position in whole bytes, rounded up. + pub fn byte_position(&self) -> usize { + self.pos.div_ceil(8) + } + + /// Bits left before the end of the data. + pub fn bits_left(&self) -> usize { + (self.data.len() * 8).saturating_sub(self.pos) + } + + /// One bit as a flag. + pub fn flag(&mut self) -> Result { + Ok(self.f(1)? != 0) + } + + /// `f(n)`: an unsigned n-bit number, MSB first, for n up to 32. + pub fn f(&mut self, n: u32) -> Result { + debug_assert!(n <= 32); + if n == 0 { + return Ok(0); + } + if self.bits_left() < n as usize { + return Err(truncated()); + } + let mut value: u64 = 0; + for _ in 0..n { + let byte = self.data[self.pos >> 3]; + let bit = (byte >> (7 - (self.pos & 7))) & 1; + value = (value << 1) | bit as u64; + self.pos += 1; + } + Ok(value as u32) + } + + /// `su(n)`: an n-bit two's complement signed number (4.10.6). + pub fn su(&mut self, n: u32) -> Result { + let value = self.f(n)? as i64; + let sign_mask = 1i64 << (n - 1); + let value = if value & sign_mask != 0 { + value - 2 * sign_mask + } else { + value + }; + Ok(value as i32) + } + + /// `ns(n)`: a non-symmetric unsigned value in `0..n` (4.10.7). + pub fn ns(&mut self, n: u32) -> Result { + if n <= 1 { + // A single possible value takes no bits. + return Ok(0); + } + let w = 32 - n.leading_zeros(); // FloorLog2(n) + 1 + let m = (1u32 << w) - n; + let v = self.f(w - 1)?; + if v < m { + return Ok(v); + } + let extra_bit = self.f(1)?; + Ok((v << 1) - m + extra_bit) + } + + /// `le(n)`: an n-byte little-endian unsigned number (4.10.4). + pub fn le(&mut self, n: u32) -> Result { + let mut t: u64 = 0; + for i in 0..n { + t += (self.f(8)? as u64) << (i * 8); + } + u32::try_from(t) + .map_err(|_| PixelForgeError::InvalidInput("AV1: le(n) value out of range".to_string())) + } + + /// `leb128()`: an unsigned little-endian base-128 value (4.10.5). + pub fn leb128(&mut self) -> Result { + let mut value: u64 = 0; + for i in 0..8 { + let byte = self.f(8)? as u64; + value |= (byte & 0x7f) << (i * 7); + if byte & 0x80 == 0 { + return Ok(value); + } + } + Err(PixelForgeError::InvalidInput( + "AV1: leb128 value longer than 8 bytes".to_string(), + )) + } + + /// `uvlc()`: a variable-length unsigned value (4.10.3). + pub fn uvlc(&mut self) -> Result { + let mut leading_zeros = 0u32; + while !self.flag()? { + leading_zeros += 1; + if leading_zeros >= 32 { + // The spec defines this as (1 << 32) - 1; reading on would + // only consume bits that carry no information. + return Ok(u32::MAX); + } + } + let value = self.f(leading_zeros)?; + Ok(value + ((1u64 << leading_zeros) - 1) as u32) + } + + /// `byte_alignment()`: skip to the next byte boundary. The skipped bits + /// must be zero (5.3.5). + pub fn byte_alignment(&mut self) -> Result<()> { + while self.pos & 7 != 0 { + if self.flag()? { + return Err(PixelForgeError::InvalidInput( + "AV1: non-zero bit in byte_alignment()".to_string(), + )); + } + } + Ok(()) + } + + /// `trailing_bits()` (5.3.4): check that the bits from here to the end of + /// a payload of `payload_bytes` bytes are a one followed by zeros. + pub fn trailing_bits(&mut self, payload_bytes: usize) -> Result<()> { + let end = payload_bytes * 8; + if self.pos >= end || end > self.data.len() * 8 { + return Err(PixelForgeError::InvalidInput( + "AV1: OBU payload has no room for its trailing bits".to_string(), + )); + } + if !self.flag()? { + return Err(PixelForgeError::InvalidInput( + "AV1: OBU payload overruns its trailing bits".to_string(), + )); + } + while self.pos < end { + if self.flag()? { + return Err(PixelForgeError::InvalidInput( + "AV1: non-zero trailing_zero_bit".to_string(), + )); + } + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reads_fixed_width_fields() { + let mut r = BitReader::new(&[0b1010_1100, 0xff, 0x01]); + assert_eq!(r.f(1).unwrap(), 1); + assert_eq!(r.f(3).unwrap(), 0b010); + assert_eq!(r.f(4).unwrap(), 0b1100); + assert_eq!(r.position(), 8); + assert_eq!(r.f(16).unwrap(), 0xff01); + assert!(r.f(1).is_err(), "reading past the end must fail"); + } + + #[test] + fn reads_signed_fields() { + // su(7) of 0b1111111 is -1, of 0b0111111 is 63, of 0b1000000 is -64. + let mut r = BitReader::new(&[0b1111_1110, 0b1111_1110, 0b0000_0000]); + assert_eq!(r.su(7).unwrap(), -1); + assert_eq!(r.su(7).unwrap(), 63); + assert_eq!(r.su(7).unwrap(), -64); + } + + #[test] + fn reads_leb128() { + // 0xE5 0x8E 0x26 is the classic 624485 example. + let mut r = BitReader::new(&[0xe5, 0x8e, 0x26, 0x05]); + assert_eq!(r.leb128().unwrap(), 624_485); + assert_eq!(r.leb128().unwrap(), 5); + assert!(BitReader::new(&[0x80; 9]).leb128().is_err()); + assert!(BitReader::new(&[0x80]).leb128().is_err()); + } + + #[test] + fn reads_uvlc() { + // "1" is 0, "010" is 1, "011" is 2, "00100" is 3. + let mut r = BitReader::new(&[0b1010_0110, 0b0100_0000]); + assert_eq!(r.uvlc().unwrap(), 0); + assert_eq!(r.uvlc().unwrap(), 1); + assert_eq!(r.uvlc().unwrap(), 2); + assert_eq!(r.uvlc().unwrap(), 3); + } + + #[test] + fn reads_ns() { + // ns(5): w = 3, m = 3. Values 0..3 take 2 bits; 3 and 4 take 3 bits. + let mut r = BitReader::new(&[0b1011_0111, 0b0000_0000]); + assert_eq!(r.ns(5).unwrap(), 2); // 10 + assert_eq!(r.ns(5).unwrap(), 3); // 11 0 -> (3 << 1) - 3 + 0 + assert_eq!(r.ns(5).unwrap(), 4); // 11 1 -> (3 << 1) - 3 + 1 + assert_eq!(r.position(), 8); + assert_eq!(r.ns(1).unwrap(), 0); + assert_eq!(r.position(), 8, "ns(1) reads nothing"); + } + + #[test] + fn reads_le() { + let mut r = BitReader::new(&[0x34, 0x12, 0x00, 0x01]); + assert_eq!(r.le(2).unwrap(), 0x1234); + assert_eq!(r.le(2).unwrap(), 0x0100); + } + + #[test] + fn checks_alignment_and_trailing_bits() { + let mut r = BitReader::new(&[0b1000_0000, 0b1100_0000]); + r.f(1).unwrap(); + r.byte_alignment().unwrap(); + assert_eq!(r.position(), 8); + r.f(1).unwrap(); + r.trailing_bits(2).unwrap(); + + let mut r = BitReader::new(&[0b1010_0000]); + r.f(1).unwrap(); + assert!(r.byte_alignment().is_err(), "non-zero padding bit"); + + let mut r = BitReader::new(&[0b0100_0001]); + r.f(1).unwrap(); + assert!(r.trailing_bits(1).is_err(), "stray bit after the one"); + } +} diff --git a/src/decoder/av1/consts.rs b/src/decoder/av1/consts.rs new file mode 100644 index 00000000..54ccf31b --- /dev/null +++ b/src/decoder/av1/consts.rs @@ -0,0 +1,66 @@ +//! Constants from section 3 of the AV1 specification, as the parser uses them. + +pub const NUM_REF_FRAMES: usize = 8; +pub const REFS_PER_FRAME: usize = 7; +pub const TOTAL_REFS_PER_FRAME: usize = 8; +pub const PRIMARY_REF_NONE: u8 = 7; +pub const MAX_SEGMENTS: usize = 8; +pub const SEG_LVL_MAX: usize = 8; +pub const SEG_LVL_ALT_Q: usize = 0; +pub const SEG_LVL_REF_FRAME: usize = 5; +pub const MAX_LOOP_FILTER: i32 = 63; + +pub const MAX_TILE_WIDTH: u32 = 4096; +pub const MAX_TILE_AREA: u32 = 4096 * 2304; +pub const MAX_TILE_ROWS: u32 = 64; +pub const MAX_TILE_COLS: u32 = 64; + +pub const SUPERRES_NUM: u32 = 8; +pub const SUPERRES_DENOM_MIN: u32 = 9; +pub const SUPERRES_DENOM_BITS: u32 = 3; + +pub const SELECT_SCREEN_CONTENT_TOOLS: u8 = 2; +pub const SELECT_INTEGER_MV: u8 = 2; + +/// `interpolation_filter` value meaning "chosen per block". +pub const SWITCHABLE: u8 = 4; + +pub const RESTORATION_TILESIZE_MAX: u16 = 256; + +pub const WARPEDMODEL_PREC_BITS: u32 = 16; +pub const GM_ABS_ALPHA_BITS: u32 = 12; +pub const GM_ALPHA_PREC_BITS: u32 = 15; +pub const GM_ABS_TRANS_ONLY_BITS: u32 = 9; +pub const GM_TRANS_ONLY_PREC_BITS: u32 = 3; +pub const GM_ABS_TRANS_BITS: u32 = 12; +pub const GM_TRANS_PREC_BITS: u32 = 6; + +/// Global motion types (`GmType`). +pub const IDENTITY: u8 = 0; +pub const TRANSLATION: u8 = 1; +pub const ROTZOOM: u8 = 2; +pub const AFFINE: u8 = 3; + +/// Reference frame names (section 6.10.24), which index `OrderHints`, +/// `RefFrameSignBias`, `gm_params` and the loop filter reference deltas. +pub const INTRA_FRAME: usize = 0; +pub const LAST_FRAME: usize = 1; +pub const LAST2_FRAME: usize = 2; +pub const LAST3_FRAME: usize = 3; +pub const GOLDEN_FRAME: usize = 4; +pub const BWDREF_FRAME: usize = 5; +pub const ALTREF2_FRAME: usize = 6; +pub const ALTREF_FRAME: usize = 7; + +/// `loop_filter_ref_deltas` after `setup_past_independence()`. +pub const DEFAULT_LOOP_FILTER_REF_DELTAS: [i8; TOTAL_REFS_PER_FRAME] = [1, 0, 0, 0, -1, 0, -1, -1]; + +/// Identity warp parameters: `1 << WARPEDMODEL_PREC_BITS` on the diagonal. +pub const DEFAULT_GM_PARAMS: [i32; 6] = [ + 0, + 0, + 1 << WARPEDMODEL_PREC_BITS, + 0, + 0, + 1 << WARPEDMODEL_PREC_BITS, +]; diff --git a/src/decoder/av1/frame_header.rs b/src/decoder/av1/frame_header.rs new file mode 100644 index 00000000..cb378dd8 --- /dev/null +++ b/src/decoder/av1/frame_header.rs @@ -0,0 +1,663 @@ +//! The uncompressed frame header (AV1 specification section 5.9). +//! +//! The parser keeps coded values where Vulkan's `StdVideoDecodeAV1PictureInfo` +//! takes coded values, and derived ones (`OrderHint`, `OrderHints`, +//! `SkipModeFrame`, `TxMode`, `expectedFrameId`, the frame size after +//! superres) where it takes those. Values a frame inherits from its primary +//! reference frame (loop filter deltas, segmentation features, global motion +//! references, film grain) are resolved here, so a header is self-contained. +//! +//! Header parsing never changes the reference state it is given. The few +//! changes the syntax itself makes to that state (a shown key frame resetting +//! it, error resilient order hints and frame ids invalidating slots) are +//! applied to a copy, which the caller keeps for the reference frame update +//! once the frame is decoded. + +use super::bits::BitReader; +use super::consts::*; +use super::obu::ObuHeader; +use super::params::*; +use super::refs::{RefState, mark_ref_frames, relative_dist, set_frame_refs}; +use super::sequence::SequenceHeader; +use super::tile::{TileInfo, parse_tile_info}; +use crate::error::{PixelForgeError, Result}; + +/// `frame_type` (section 6.8.2), numbered as Vulkan's `StdVideoAV1FrameType`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +#[repr(u8)] +pub enum FrameType { + #[default] + Key = 0, + Inter = 1, + IntraOnly = 2, + Switch = 3, +} + +impl FrameType { + fn from_raw(raw: u32) -> Self { + match raw { + 0 => FrameType::Key, + 1 => FrameType::Inter, + 2 => FrameType::IntraOnly, + _ => FrameType::Switch, + } + } + + /// `FrameIsIntra`. + pub fn is_intra(self) -> bool { + matches!(self, FrameType::Key | FrameType::IntraOnly) + } +} + +/// The frame size syntax (5.9.5 to 5.9.8) and what it derives. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct FrameSize { + /// `FrameWidth`: the coded width, after superres downscaling. + pub frame_width: u32, + pub frame_height: u32, + /// `UpscaledWidth`: the width after superres upscaling. + pub upscaled_width: u32, + pub render_width: u32, + pub render_height: u32, + pub mi_cols: u32, + pub mi_rows: u32, + pub use_superres: bool, + /// The coded `coded_denom` (0 without superres), as Vulkan takes it. + pub coded_denom: u8, + /// `SuperresDenom` (8 without superres). + pub superres_denom: u32, + pub render_and_frame_size_different: bool, + /// With `frame_size_with_refs()`, which of the seven references the size + /// was copied from (`found_ref`). + pub found_ref: Option, +} + +/// A parsed `uncompressed_header()`. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct FrameHeader { + pub show_existing_frame: bool, + pub frame_to_show_map_idx: u8, + pub display_frame_id: u32, + pub frame_presentation_time: u32, + pub frame_type: FrameType, + pub show_frame: bool, + pub showable_frame: bool, + pub error_resilient_mode: bool, + pub disable_cdf_update: bool, + pub allow_screen_content_tools: bool, + pub force_integer_mv: bool, + pub current_frame_id: u32, + pub frame_size_override_flag: bool, + /// `OrderHint`. + pub order_hint: u8, + pub primary_ref_frame: u8, + pub buffer_removal_time_present_flag: bool, + pub refresh_frame_flags: u8, + /// The coded `ref_order_hint[]` (error resilient frames only). + pub ref_order_hint: [u8; NUM_REF_FRAMES], + pub frame_size: FrameSize, + pub allow_intrabc: bool, + pub frame_refs_short_signaling: bool, + pub last_frame_idx: u8, + pub gold_frame_idx: u8, + /// The reference slot behind each of LAST_FRAME..=ALTREF_FRAME (coded, + /// or derived by `set_frame_refs()`). Zero for intra frames. + pub ref_frame_idx: [u8; REFS_PER_FRAME], + /// `expectedFrameId[i]` for reference `LAST_FRAME + i`, as section 5.9.2 + /// indexes it (Vulkan's array has an eighth, unused entry). + pub expected_frame_id: [u32; REFS_PER_FRAME], + pub allow_high_precision_mv: bool, + pub is_filter_switchable: bool, + /// `interpolation_filter`, `SWITCHABLE` (4) when switchable. + pub interpolation_filter: u8, + pub is_motion_mode_switchable: bool, + pub use_ref_frame_mvs: bool, + /// `OrderHints[refFrame]`, indexed by reference frame name (entry 0, + /// INTRA_FRAME, is unused). + pub order_hints: [u8; TOTAL_REFS_PER_FRAME], + /// `RefFrameSignBias[refFrame]`, indexed by reference frame name. + pub ref_frame_sign_bias: [bool; TOTAL_REFS_PER_FRAME], + pub disable_frame_end_update_cdf: bool, + pub tile_info: TileInfo, + pub quantization: QuantizationParams, + pub segmentation: SegmentationParams, + pub delta: DeltaParams, + pub coded_lossless: bool, + pub all_lossless: bool, + pub lossless_array: [bool; MAX_SEGMENTS], + /// `SegQMLevel[plane][segment]`, set when `using_qmatrix`. + pub seg_qm_level: [[u8; MAX_SEGMENTS]; 3], + pub loop_filter: LoopFilterParams, + pub cdef: CdefParams, + pub loop_restoration: LoopRestorationParams, + /// `TxMode`: 0 ONLY_4X4, 1 TX_MODE_LARGEST, 2 TX_MODE_SELECT. + pub tx_mode: u8, + pub reference_select: bool, + pub skip_mode_present: bool, + /// `SkipModeFrame[0..2]` as reference frame names; zero when skip mode is + /// not allowed. + pub skip_mode_frame: [u8; 2], + pub allow_warped_motion: bool, + pub reduced_tx_set: bool, + pub global_motion: GlobalMotionParams, + pub film_grain: FilmGrainParams, + /// Length of `uncompressed_header()` in bits, without the byte + /// alignment or trailing bits that follow it. + pub header_bits: usize, +} + +impl FrameHeader { + /// `FrameIsIntra`. + pub fn frame_is_intra(&self) -> bool { + self.frame_type.is_intra() + } + + /// `RefFrameSignBias` as a bitmask indexed by reference frame name. + pub fn ref_frame_sign_bias_mask(&self) -> u8 { + self.ref_frame_sign_bias + .iter() + .enumerate() + .fold(0, |mask, (i, &b)| mask | ((b as u8) << i)) + } +} + +/// What parsing a frame header produced. +#[derive(Debug)] +pub(crate) enum HeaderOutcome { + /// The header, and the reference state with the header's own changes + /// applied, for the update once the frame is decoded. + Parsed(Box, Box), + /// The header needs reference slots that hold no valid frame (bit `i` + /// for slot `i`), so the frame cannot be decoded; a decoder has to wait + /// for the next key frame. + MissingReferences(u8), +} + +fn frame_size( + r: &mut BitReader, + seq: &SequenceHeader, + frame_size_override_flag: bool, +) -> Result { + let mut size = FrameSize::default(); + if frame_size_override_flag { + size.frame_width = r.f(seq.frame_width_bits_minus_1 as u32 + 1)? + 1; + size.frame_height = r.f(seq.frame_height_bits_minus_1 as u32 + 1)? + 1; + } else { + size.frame_width = seq.max_frame_width(); + size.frame_height = seq.max_frame_height(); + } + superres_params(r, seq, &mut size)?; + Ok(size) +} + +/// `superres_params()` followed by `compute_image_size()`. +fn superres_params(r: &mut BitReader, seq: &SequenceHeader, size: &mut FrameSize) -> Result<()> { + size.use_superres = seq.enable_superres && r.flag()?; + size.superres_denom = if size.use_superres { + size.coded_denom = r.f(SUPERRES_DENOM_BITS)? as u8; + size.coded_denom as u32 + SUPERRES_DENOM_MIN + } else { + SUPERRES_NUM + }; + size.upscaled_width = size.frame_width; + size.frame_width = + (size.upscaled_width * SUPERRES_NUM + size.superres_denom / 2) / size.superres_denom; + size.mi_cols = 2 * ((size.frame_width + 7) >> 3); + size.mi_rows = 2 * ((size.frame_height + 7) >> 3); + Ok(()) +} + +fn render_size(r: &mut BitReader, size: &mut FrameSize) -> Result<()> { + size.render_and_frame_size_different = r.flag()?; + if size.render_and_frame_size_different { + size.render_width = r.f(16)? + 1; + size.render_height = r.f(16)? + 1; + } else { + size.render_width = size.upscaled_width; + size.render_height = size.frame_height; + } + Ok(()) +} + +/// `frame_size_with_refs()` (5.9.7). +fn frame_size_with_refs( + r: &mut BitReader, + seq: &SequenceHeader, + refs: &RefState, + ref_frame_idx: &[u8; REFS_PER_FRAME], + frame_size_override_flag: bool, +) -> Result { + for (i, &idx) in ref_frame_idx.iter().enumerate() { + if r.flag()? { + let slot = &refs.slots[idx as usize]; + let mut size = FrameSize { + frame_width: slot.upscaled_width, + frame_height: slot.frame_height, + render_width: slot.render_width, + render_height: slot.render_height, + found_ref: Some(i as u8), + ..Default::default() + }; + superres_params(r, seq, &mut size)?; + return Ok(size); + } + } + let mut size = frame_size(r, seq, frame_size_override_flag)?; + render_size(r, &mut size)?; + Ok(size) +} + +/// Fill a `show_existing_frame` header from the slot it shows (the parts of +/// the loading process, 7.21, a decoder needs to output the frame). +fn load_shown_frame(seq: &SequenceHeader, refs: &RefState, h: &mut FrameHeader) { + let slot = &refs.slots[h.frame_to_show_map_idx as usize]; + h.frame_type = slot.frame_type; + h.order_hint = slot.order_hint; + h.order_hints = slot.saved_order_hints; + h.current_frame_id = slot.frame_id; + h.frame_size = FrameSize { + frame_width: slot.frame_width, + frame_height: slot.frame_height, + upscaled_width: slot.upscaled_width, + render_width: slot.render_width, + render_height: slot.render_height, + mi_cols: slot.mi_cols, + mi_rows: slot.mi_rows, + superres_denom: SUPERRES_NUM, + ..Default::default() + }; + if seq.film_grain_params_present { + h.film_grain = slot.film_grain.clone(); + } + if h.frame_type == FrameType::Key { + h.refresh_frame_flags = 0xff; + } +} + +/// Parse `uncompressed_header()` against the current reference state. +/// +/// `prev_order_hints` is `OrderHints` as the previous frame header left it: +/// the spec keeps it across frames, and an intra frame stores it unchanged. +pub(crate) fn parse_uncompressed_header( + r: &mut BitReader, + seq: &SequenceHeader, + refs: &RefState, + obu: &ObuHeader, + prev_order_hints: [u8; TOTAL_REFS_PER_FRAME], +) -> Result { + let start = r.position(); + let mut refs = refs.clone(); + let mut h = FrameHeader { + order_hints: prev_order_hints, + ..Default::default() + }; + let id_len = seq.frame_id_length(); + let decoder_model = seq.decoder_model_info.as_ref(); + let all_frames = 0xffu8; + + if seq.reduced_still_picture_header { + h.frame_type = FrameType::Key; + h.show_frame = true; + h.error_resilient_mode = true; + } else { + h.show_existing_frame = r.flag()?; + if h.show_existing_frame { + h.frame_to_show_map_idx = r.f(3)? as u8; + if let Some(model) = decoder_model + && !seq.equal_picture_interval() + { + h.frame_presentation_time = + r.f(model.frame_presentation_time_length_minus_1 as u32 + 1)?; + } + if seq.frame_id_numbers_present_flag { + h.display_frame_id = r.f(id_len)?; + } + if !refs.slots[h.frame_to_show_map_idx as usize].valid { + return Ok(HeaderOutcome::MissingReferences( + 1 << h.frame_to_show_map_idx, + )); + } + load_shown_frame(seq, &refs, &mut h); + h.header_bits = r.position() - start; + return Ok(HeaderOutcome::Parsed(Box::new(h), Box::new(refs))); + } + h.frame_type = FrameType::from_raw(r.f(2)?); + h.show_frame = r.flag()?; + if h.show_frame + && let Some(model) = decoder_model + && !seq.equal_picture_interval() + { + h.frame_presentation_time = + r.f(model.frame_presentation_time_length_minus_1 as u32 + 1)?; + } + h.showable_frame = if h.show_frame { + h.frame_type != FrameType::Key + } else { + r.flag()? + }; + h.error_resilient_mode = if h.frame_type == FrameType::Switch + || (h.frame_type == FrameType::Key && h.show_frame) + { + true + } else { + r.flag()? + }; + } + let frame_is_intra = h.frame_is_intra(); + + if h.frame_type == FrameType::Key && h.show_frame { + for slot in refs.slots.iter_mut() { + slot.valid = false; + slot.order_hint = 0; + } + for i in 0..REFS_PER_FRAME { + h.order_hints[LAST_FRAME + i] = 0; + } + } + h.disable_cdf_update = r.flag()?; + h.allow_screen_content_tools = + if seq.seq_force_screen_content_tools == SELECT_SCREEN_CONTENT_TOOLS { + r.flag()? + } else { + seq.seq_force_screen_content_tools != 0 + }; + if h.allow_screen_content_tools { + h.force_integer_mv = if seq.seq_force_integer_mv == SELECT_INTEGER_MV { + r.flag()? + } else { + seq.seq_force_integer_mv != 0 + }; + } + if frame_is_intra { + h.force_integer_mv = true; + } + if seq.frame_id_numbers_present_flag { + h.current_frame_id = r.f(id_len)?; + mark_ref_frames(seq, &mut refs, h.current_frame_id); + } + h.frame_size_override_flag = if h.frame_type == FrameType::Switch { + true + } else if seq.reduced_still_picture_header { + false + } else { + r.flag()? + }; + h.order_hint = r.f(seq.order_hint_bits as u32)? as u8; + h.primary_ref_frame = if frame_is_intra || h.error_resilient_mode { + PRIMARY_REF_NONE + } else { + r.f(3)? as u8 + }; + if let Some(model) = decoder_model { + h.buffer_removal_time_present_flag = r.flag()?; + if h.buffer_removal_time_present_flag { + // The removal times are only for the decoder model; skip them. + for op in &seq.operating_points { + if op.decoder_model_present_for_this_op { + let idc = op.operating_point_idc; + let in_temporal = (idc >> obu.temporal_id) & 1 != 0; + let in_spatial = (idc >> (obu.spatial_id + 8)) & 1 != 0; + if idc == 0 || (in_temporal && in_spatial) { + r.f(model.buffer_removal_time_length_minus_1 as u32 + 1)?; + } + } + } + } + } + h.refresh_frame_flags = + if h.frame_type == FrameType::Switch || (h.frame_type == FrameType::Key && h.show_frame) { + all_frames + } else { + r.f(8)? as u8 + }; + if h.frame_type == FrameType::IntraOnly && h.refresh_frame_flags == all_frames { + return Err(PixelForgeError::InvalidInput( + "AV1: intra-only frame refreshes every reference slot".to_string(), + )); + } + if (!frame_is_intra || h.refresh_frame_flags != all_frames) + && h.error_resilient_mode + && seq.enable_order_hint + { + for i in 0..NUM_REF_FRAMES { + let hint = r.f(seq.order_hint_bits as u32)? as u8; + h.ref_order_hint[i] = hint; + let slot = &mut refs.slots[i]; + if hint != slot.order_hint { + // The frame the encoder had here is lost. Remember the order + // hint it had, as libaom does, so later order arithmetic + // matches the encoder's; the slot is unusable either way. + slot.valid = false; + slot.order_hint = hint; + } + } + } + + if frame_is_intra { + h.frame_size = frame_size(r, seq, h.frame_size_override_flag)?; + render_size(r, &mut h.frame_size)?; + if h.allow_screen_content_tools && h.frame_size.upscaled_width == h.frame_size.frame_width { + h.allow_intrabc = r.flag()?; + } + } else { + if seq.enable_order_hint { + h.frame_refs_short_signaling = r.flag()?; + if h.frame_refs_short_signaling { + h.last_frame_idx = r.f(3)? as u8; + h.gold_frame_idx = r.f(3)? as u8; + h.ref_frame_idx = + set_frame_refs(seq, &refs, h.order_hint, h.last_frame_idx, h.gold_frame_idx); + } + } + let mut missing = 0u8; + for i in 0..REFS_PER_FRAME { + if !h.frame_refs_short_signaling { + h.ref_frame_idx[i] = r.f(3)? as u8; + } + let slot = &refs.slots[h.ref_frame_idx[i] as usize]; + if seq.frame_id_numbers_present_flag { + let delta = r.f(seq.delta_frame_id_length_minus_2 as u32 + 2)? + 1; + let expected = (h.current_frame_id + (1 << id_len) - delta) % (1 << id_len); + h.expected_frame_id[i] = expected; + if expected != slot.frame_id { + missing |= 1 << h.ref_frame_idx[i]; + } + } + if !slot.valid { + missing |= 1 << h.ref_frame_idx[i]; + } + } + if missing != 0 { + return Ok(HeaderOutcome::MissingReferences(missing)); + } + h.frame_size = if h.frame_size_override_flag && !h.error_resilient_mode { + frame_size_with_refs(r, seq, &refs, &h.ref_frame_idx, h.frame_size_override_flag)? + } else { + let mut size = frame_size(r, seq, h.frame_size_override_flag)?; + render_size(r, &mut size)?; + size + }; + h.allow_high_precision_mv = if h.force_integer_mv { false } else { r.flag()? }; + h.is_filter_switchable = r.flag()?; + h.interpolation_filter = if h.is_filter_switchable { + SWITCHABLE + } else { + r.f(2)? as u8 + }; + h.is_motion_mode_switchable = r.flag()?; + h.use_ref_frame_mvs = if h.error_resilient_mode || !seq.enable_ref_frame_mvs { + false + } else { + r.flag()? + }; + for i in 0..REFS_PER_FRAME { + let ref_frame = LAST_FRAME + i; + let hint = refs.slots[h.ref_frame_idx[i] as usize].order_hint; + h.order_hints[ref_frame] = hint; + h.ref_frame_sign_bias[ref_frame] = + seq.enable_order_hint && relative_dist(seq, hint, h.order_hint) > 0; + } + } + + h.disable_frame_end_update_cdf = if seq.reduced_still_picture_header || h.disable_cdf_update { + true + } else { + r.flag()? + }; + + // load_previous() or setup_past_independence(): what the frame inherits. + let (prev_gm_params, prev_lf_ref, prev_lf_mode, prev_segmentation) = + if h.primary_ref_frame == PRIMARY_REF_NONE { + ( + [DEFAULT_GM_PARAMS; TOTAL_REFS_PER_FRAME], + DEFAULT_LOOP_FILTER_REF_DELTAS, + [0i8; 2], + SegmentationParams::default(), + ) + } else { + let slot = &refs.slots[h.ref_frame_idx[h.primary_ref_frame as usize] as usize]; + ( + slot.gm_params, + slot.loop_filter_ref_deltas, + slot.loop_filter_mode_deltas, + SegmentationParams { + feature_enabled: slot.feature_enabled, + feature_data: slot.feature_data, + ..Default::default() + }, + ) + }; + + h.tile_info = parse_tile_info( + r, + seq.use_128x128_superblock, + h.frame_size.mi_cols, + h.frame_size.mi_rows, + )?; + h.quantization = parse_quantization(r, seq)?; + h.segmentation = parse_segmentation(r, h.primary_ref_frame, &prev_segmentation)?; + h.delta = parse_delta_params(r, h.quantization.base_q_idx, h.allow_intrabc)?; + + let q = &h.quantization; + h.coded_lossless = true; + for segment_id in 0..MAX_SEGMENTS { + let qindex = h.segmentation.qindex(q.base_q_idx, segment_id); + let lossless = qindex == 0 + && q.delta_q_y_dc == 0 + && q.delta_q_u_ac == 0 + && q.delta_q_u_dc == 0 + && q.delta_q_v_ac == 0 + && q.delta_q_v_dc == 0; + h.lossless_array[segment_id] = lossless; + if !lossless { + h.coded_lossless = false; + } + if q.using_qmatrix { + let levels = if lossless { + [15, 15, 15] + } else { + [q.qm_y, q.qm_u, q.qm_v] + }; + for (plane, level) in levels.into_iter().enumerate() { + h.seg_qm_level[plane][segment_id] = level; + } + } + } + h.all_lossless = h.coded_lossless && h.frame_size.frame_width == h.frame_size.upscaled_width; + + h.loop_filter = parse_loop_filter( + r, + seq, + h.coded_lossless, + h.allow_intrabc, + prev_lf_ref, + prev_lf_mode, + )?; + h.cdef = parse_cdef(r, seq, h.coded_lossless, h.allow_intrabc)?; + h.loop_restoration = parse_lr(r, seq, h.all_lossless, h.allow_intrabc)?; + h.tx_mode = if h.coded_lossless { + 0 + } else if r.flag()? { + 2 + } else { + 1 + }; + h.reference_select = !frame_is_intra && r.flag()?; + h.skip_mode_frame = skip_mode_frames(seq, &refs, &h); + h.skip_mode_present = h.skip_mode_frame[0] != 0 && r.flag()?; + h.allow_warped_motion = + !(frame_is_intra || h.error_resilient_mode || !seq.enable_warped_motion) && r.flag()?; + h.reduced_tx_set = r.flag()?; + h.global_motion = parse_global_motion( + r, + frame_is_intra, + h.allow_high_precision_mv, + &prev_gm_params, + )?; + + let ctx = GrainContext { + show_frame: h.show_frame, + showable_frame: h.showable_frame, + is_inter_frame: h.frame_type == FrameType::Inter, + }; + let load = |idx: u8| { + let slot = &refs.slots[idx as usize]; + slot.valid.then(|| slot.film_grain.clone()) + }; + h.film_grain = match parse_film_grain(r, seq, &ctx, load)? { + Ok(grain) => grain, + Err(slot) => return Ok(HeaderOutcome::MissingReferences(1 << slot)), + }; + + h.header_bits = r.position() - start; + Ok(HeaderOutcome::Parsed(Box::new(h), Box::new(refs))) +} + +/// `skip_mode_params()` (5.9.22) up to `skipModeAllowed`: the two +/// reference frame names skip mode would use, or zeros when it is not +/// allowed. +fn skip_mode_frames(seq: &SequenceHeader, refs: &RefState, h: &FrameHeader) -> [u8; 2] { + if h.frame_is_intra() || !h.reference_select || !seq.enable_order_hint { + return [0, 0]; + } + let hint_of = |i: usize| refs.slots[h.ref_frame_idx[i] as usize].order_hint; + let dist = |a: u8, b: u8| relative_dist(seq, a, b); + let mut forward: Option<(usize, u8)> = None; + let mut backward: Option<(usize, u8)> = None; + for i in 0..REFS_PER_FRAME { + let ref_hint = hint_of(i); + if dist(ref_hint, h.order_hint) < 0 { + if forward.is_none_or(|(_, f)| dist(ref_hint, f) > 0) { + forward = Some((i, ref_hint)); + } + } else if dist(ref_hint, h.order_hint) > 0 + && backward.is_none_or(|(_, b)| dist(ref_hint, b) < 0) + { + backward = Some((i, ref_hint)); + } + } + let Some((forward_idx, forward_hint)) = forward else { + return [0, 0]; + }; + let second = if let Some((backward_idx, _)) = backward { + backward_idx + } else { + let mut second_forward: Option<(usize, u8)> = None; + for i in 0..REFS_PER_FRAME { + let ref_hint = hint_of(i); + if dist(ref_hint, forward_hint) < 0 + && second_forward.is_none_or(|(_, s)| dist(ref_hint, s) > 0) + { + second_forward = Some((i, ref_hint)); + } + } + let Some((second_idx, _)) = second_forward else { + return [0, 0]; + }; + second_idx + }; + [ + (LAST_FRAME + forward_idx.min(second)) as u8, + (LAST_FRAME + forward_idx.max(second)) as u8, + ] +} diff --git a/src/decoder/av1/mod.rs b/src/decoder/av1/mod.rs new file mode 100644 index 00000000..26f3323c --- /dev/null +++ b/src/decoder/av1/mod.rs @@ -0,0 +1,39 @@ +//! Host-side AV1 parsing for Vulkan Video decode (`VK_KHR_video_decode_av1`). +//! +//! Vulkan decodes AV1 tiles in the driver but takes every header value from +//! the application, already parsed: the sequence header, the frame header +//! with everything it inherits from its references, and where each tile is. +//! This module produces exactly that from a low-overhead OBU stream +//! (section 5.2 of the AV1 specification), and keeps the reference state +//! (the eight reference slots) that later headers depend on. +//! +//! - [`obu`]: OBU framing, temporal unit boundaries, operating point +//! selection, Annex B detection. +//! - [`sequence`]: the sequence header. +//! - [`frame_header`] and [`params`]: the uncompressed frame header. +//! - [`tile`]: the tile grid and tile group OBUs. +//! - [`refs`]: the reference slots and their update. +//! - [`parser`]: [`Av1Parser`], which ties them together frame by frame. +//! - [`std_info`]: conversion to the Vulkan `StdVideoAV1*` structures. +//! +//! Typical use: iterate the OBUs of a temporal unit with +//! [`obu::iter_obus`], pass each to [`Av1Parser::parse_obu`], and for each +//! [`Av1Event::Frame`] decode it, then call [`Av1Parser::commit_frame`] +//! with the DPB slot the picture went to. + +pub(crate) mod bits; +pub(crate) mod consts; +pub(crate) mod frame_header; +pub(crate) mod obu; +pub(crate) mod params; +pub(crate) mod parser; +pub(crate) mod refs; +pub(crate) mod sequence; +pub(crate) mod std_info; +pub(crate) mod tile; + +/// Parser tests against real libaom and SVT-AV1 streams (host-side, no GPU). +#[cfg(test)] +mod stream_tests; + +pub(crate) use parser::{Av1Event, Av1Frame, Av1Parser}; diff --git a/src/decoder/av1/obu.rs b/src/decoder/av1/obu.rs new file mode 100644 index 00000000..c8e38f56 --- /dev/null +++ b/src/decoder/av1/obu.rs @@ -0,0 +1,362 @@ +//! OBU framing (AV1 specification sections 5.2 and 5.3). +//! +//! The parser takes the low-overhead bitstream format of section 5.2, the +//! format of `.obu` files, of MP4/Matroska samples and of RTP after +//! depacketisation: a plain sequence of OBUs, each carrying its own size. +//! Annex B length-delimited streams wrap every temporal unit, frame unit and +//! OBU in a further length; they are detected where the data makes it +//! possible and rejected with an error that says so. + +use super::bits::BitReader; +use crate::error::{PixelForgeError, Result}; + +/// OBU types (section 6.2.2). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ObuType { + SequenceHeader, + TemporalDelimiter, + FrameHeader, + TileGroup, + Metadata, + Frame, + RedundantFrameHeader, + TileList, + Padding, + /// A reserved type, which decoders must ignore. + Reserved(u8), +} + +impl ObuType { + fn from_raw(raw: u8) -> Self { + match raw { + 1 => ObuType::SequenceHeader, + 2 => ObuType::TemporalDelimiter, + 3 => ObuType::FrameHeader, + 4 => ObuType::TileGroup, + 5 => ObuType::Metadata, + 6 => ObuType::Frame, + 7 => ObuType::RedundantFrameHeader, + 8 => ObuType::TileList, + 15 => ObuType::Padding, + other => ObuType::Reserved(other), + } + } +} + +/// `obu_header()` (section 5.3.2), plus whether the size field was present. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ObuHeader { + pub obu_type: ObuType, + pub obu_extension_flag: bool, + pub obu_has_size_field: bool, + /// Zero when there is no extension header. + pub temporal_id: u8, + /// Zero when there is no extension header. + pub spatial_id: u8, +} + +/// One OBU within a buffer. +#[derive(Debug, Clone, Copy)] +pub struct Obu<'a> { + pub header: ObuHeader, + /// Byte offset of the first byte of `obu_header()` within the data the + /// iterator was given. + pub offset: usize, + /// Bytes from `offset` to the payload: the header, the optional + /// extension byte and the optional `obu_size` field. + pub header_len: usize, + /// The payload (`obu_size` bytes), without the header. + pub payload: &'a [u8], +} + +impl Obu<'_> { + /// Byte offset of the payload within the data the iterator was given. + pub fn payload_offset(&self) -> usize { + self.offset + self.header_len + } + + /// Total size of the OBU, header included. + pub fn total_len(&self) -> usize { + self.header_len + self.payload.len() + } +} + +/// Whether an OBU belongs to the operating point with the given +/// `operating_point_idc` (section 6.2.1, the `drop_obu()` rule). +/// +/// OBUs without an extension header, sequence headers and temporal +/// delimiters always belong, as does everything when `idc` is zero. +pub fn in_operating_point(header: &ObuHeader, idc: u16) -> bool { + if idc == 0 + || !header.obu_extension_flag + || matches!( + header.obu_type, + ObuType::SequenceHeader | ObuType::TemporalDelimiter + ) + { + return true; + } + let in_temporal = (idc >> header.temporal_id) & 1 != 0; + let in_spatial = (idc >> (header.spatial_id + 8)) & 1 != 0; + in_temporal && in_spatial +} + +/// Read one OBU from the start of `data`. +/// +/// `Ok(None)` means the data ends before the OBU does, which a caller +/// framing a byte stream treats as "wait for more". +fn read_obu(data: &[u8]) -> Result> { + let Some(&first) = data.first() else { + return Ok(None); + }; + if first & 0x80 != 0 { + return Err(PixelForgeError::InvalidInput( + "AV1: obu_forbidden_bit is set".to_string(), + )); + } + let obu_extension_flag = first & 0x04 != 0; + let obu_has_size_field = first & 0x02 != 0; + let mut header = ObuHeader { + obu_type: ObuType::from_raw((first >> 3) & 0x0f), + obu_extension_flag, + obu_has_size_field, + temporal_id: 0, + spatial_id: 0, + }; + let mut header_len = 1; + if obu_extension_flag { + let Some(&ext) = data.get(1) else { + return Ok(None); + }; + header.temporal_id = ext >> 5; + header.spatial_id = (ext >> 3) & 0x03; + header_len = 2; + } + let payload_len = if obu_has_size_field { + let mut r = BitReader::new(&data[header_len..]); + let size = match r.leb128() { + Ok(size) => size, + // A leb128 cut short by the end of the data is incomplete, not + // invalid; one longer than eight bytes is invalid. + Err(_) if r.bits_left() == 0 => return Ok(None), + Err(e) => return Err(e), + }; + header_len += r.byte_position(); + usize::try_from(size).map_err(|_| { + PixelForgeError::InvalidInput("AV1: obu_size does not fit in memory".to_string()) + })? + } else { + // Without a size field the OBU runs to the end of the data, which is + // how a container may store the last OBU of a sample. + data.len() - header_len + }; + if data.len() - header_len < payload_len { + return Ok(None); + } + Ok(Some((header, header_len, payload_len))) +} + +/// Whether `data` parses as the start of an Annex B length-delimited stream +/// (`temporal_unit_size`, `frame_unit_size`, `obu_length`, then a temporal +/// delimiter), which low-overhead data never does: there, the first OBU is a +/// temporal delimiter `0x12 0x00`, which reads as a frame unit of size zero. +pub fn looks_like_annex_b(data: &[u8]) -> bool { + let mut r = BitReader::new(data); + let (Ok(tu_size), Ok(fu_size)) = (r.leb128(), r.leb128()) else { + return false; + }; + let Ok(obu_length) = r.leb128() else { + return false; + }; + let obu_start = r.byte_position(); + let Some(&first) = data.get(obu_start) else { + return false; + }; + // Sizes must nest: the OBU inside the frame unit inside the temporal + // unit, each also holding the size field of the one inside it. + if fu_size == 0 || fu_size >= tu_size || obu_length == 0 || obu_length >= fu_size { + return false; + } + let obu_type = (first >> 3) & 0x0f; + let has_ext = first & 0x04 != 0; + // A temporal delimiter has an empty payload, so the whole OBU is its + // header (plus a zero obu_size, if the encoder wrote one). + first & 0x80 == 0 && obu_type == 2 && obu_length <= 1 + has_ext as u64 + 1 +} + +/// Iterator over the OBUs of a low-overhead bitstream. +/// +/// Yields an error, then stops, on invalid data or on an OBU that runs past +/// the end of the buffer. +pub struct ObuIter<'a> { + data: &'a [u8], + pos: usize, + done: bool, +} + +/// Iterate over the OBUs in `data` (low-overhead format, section 5.2). +pub fn iter_obus(data: &[u8]) -> ObuIter<'_> { + ObuIter { + data, + pos: 0, + done: false, + } +} + +fn annex_b_error() -> PixelForgeError { + PixelForgeError::InvalidInput( + "AV1: input looks like an Annex B length-delimited stream; only the \ + low-overhead OBU format (section 5.2) is supported" + .to_string(), + ) +} + +impl<'a> Iterator for ObuIter<'a> { + type Item = Result>; + + fn next(&mut self) -> Option { + if self.done || self.pos >= self.data.len() { + return None; + } + let rest = &self.data[self.pos..]; + // Annex B data starts with a size, not an OBU header. The checks + // that would catch it, a forbidden bit or a missing size field, only + // fire on some first bytes, so look at the structure directly. + if self.pos == 0 && looks_like_annex_b(rest) { + self.done = true; + return Some(Err(annex_b_error())); + } + match read_obu(rest) { + Ok(Some((header, header_len, payload_len))) => { + let obu = Obu { + header, + offset: self.pos, + header_len, + payload: &rest[header_len..header_len + payload_len], + }; + self.pos += header_len + payload_len; + Some(Ok(obu)) + } + Ok(None) => { + self.done = true; + Some(Err(PixelForgeError::InvalidInput(format!( + "AV1: OBU at byte {} runs past the end of the data", + self.pos + )))) + } + Err(e) => { + self.done = true; + Some(Err(e)) + } + } + } +} + +/// Byte offsets at which each temporal unit begins in a low-overhead +/// byte stream: the offset of every temporal delimiter OBU. +/// +/// This is what lets a byte-stream feed decide where it may cut: everything +/// before the last offset is complete, and everything from it on may still be +/// waiting for OBUs that have not arrived. An OBU cut short by the end of the +/// data ends the scan without an error for the same reason. Invalid data, or +/// data in Annex B format, is an error. +pub fn temporal_unit_starts(data: &[u8]) -> Result> { + if looks_like_annex_b(data) { + return Err(annex_b_error()); + } + let mut starts = Vec::new(); + let mut pos = 0; + while pos < data.len() { + let rest = &data[pos..]; + // Without a size field an OBU would run to the end of whatever has + // arrived so far, which says nothing about where it really ends. + if rest[0] & 0x02 == 0 { + return Err(PixelForgeError::InvalidInput(format!( + "AV1: OBU at byte {pos} has no obu_size, which a byte stream needs" + ))); + } + let Some((header, header_len, payload_len)) = read_obu(rest)? else { + break; + }; + if header.obu_type == ObuType::TemporalDelimiter { + starts.push(pos); + } + pos += header_len + payload_len; + } + Ok(starts) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn reads_headers_and_sizes() { + // TD, then a padding OBU with an extension header and 2 bytes. + let data = [0x12, 0x00, 0x7e, 0b0100_1000, 0x02, 0xaa, 0xbb]; + let obus: Vec<_> = iter_obus(&data).collect::>().unwrap(); + assert_eq!(obus.len(), 2); + assert_eq!(obus[0].header.obu_type, ObuType::TemporalDelimiter); + assert_eq!(obus[0].payload.len(), 0); + let pad = &obus[1]; + assert_eq!(pad.header.obu_type, ObuType::Padding); + assert!(pad.header.obu_extension_flag); + assert_eq!(pad.header.temporal_id, 2); + assert_eq!(pad.header.spatial_id, 1); + assert_eq!(pad.offset, 2); + assert_eq!(pad.header_len, 3); + assert_eq!(pad.payload, &[0xaa, 0xbb]); + } + + #[test] + fn reports_truncated_and_forbidden() { + let truncated = [0x12, 0x00, 0x32, 0x05, 0x00]; + let results: Vec<_> = iter_obus(&truncated).collect(); + assert!(results[0].is_ok()); + assert!(results[1].is_err()); + assert_eq!(results.len(), 2); + + let forbidden = [0x92, 0x00]; + assert!(iter_obus(&forbidden).next().unwrap().is_err()); + } + + #[test] + fn finds_temporal_units_and_tolerates_a_cut_tail() { + // TD, padding(1), TD, padding cut short. + let data = [0x12, 0x00, 0x7a, 0x01, 0x00, 0x12, 0x00, 0x7a, 0x04, 0x00]; + assert_eq!(temporal_unit_starts(&data).unwrap(), vec![0, 5]); + } + + #[test] + fn detects_annex_b() { + // temporal_unit_size 3, frame_unit_size 2, obu_length 1, a TD header + // without a size field, then the next temporal unit. + let data = [0x03, 0x02, 0x01, 0x10, 0x03]; + assert!(looks_like_annex_b(&data)); + assert!(iter_obus(&data).next().unwrap().is_err()); + assert!(temporal_unit_starts(&data).is_err()); + // A low-overhead stream must not trip the check. + assert!(!looks_like_annex_b(&[0x12, 0x00, 0x0a, 0x0b])); + } + + #[test] + fn drops_obus_outside_the_operating_point() { + let mut header = ObuHeader { + obu_type: ObuType::Frame, + obu_extension_flag: true, + obu_has_size_field: true, + temporal_id: 1, + spatial_id: 0, + }; + // Temporal layer 0 and spatial layer 0 only. + let idc = 0x101; + assert!(!in_operating_point(&header, idc)); + header.temporal_id = 0; + assert!(in_operating_point(&header, idc)); + header.spatial_id = 1; + assert!(!in_operating_point(&header, idc)); + header.obu_type = ObuType::SequenceHeader; + assert!(in_operating_point(&header, idc)); + assert!(in_operating_point(&header, 0)); + } +} diff --git a/src/decoder/av1/params.rs b/src/decoder/av1/params.rs new file mode 100644 index 00000000..9fc62535 --- /dev/null +++ b/src/decoder/av1/params.rs @@ -0,0 +1,773 @@ +//! The coding-tool parameter blocks of the uncompressed header (AV1 +//! specification sections 5.9.12 to 5.9.30): quantization, segmentation, +//! delta q/lf, loop filter, CDEF, loop restoration, global motion and film +//! grain. +//! +//! Each block stores what Vulkan's matching `StdVideoAV1*` structure takes, +//! in the form the spec's syntax produces. Where Vulkan wants the coded value +//! rather than the derived one (the CDEF secondary strengths) the coded value +//! is kept and the field says so. + +use super::bits::BitReader; +use super::consts::*; +use super::sequence::SequenceHeader; +use crate::error::{PixelForgeError, Result}; + +/// `quantization_params()` (5.9.12). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct QuantizationParams { + pub base_q_idx: u8, + pub delta_q_y_dc: i8, + pub delta_q_u_dc: i8, + pub delta_q_u_ac: i8, + pub delta_q_v_dc: i8, + pub delta_q_v_ac: i8, + pub diff_uv_delta: bool, + pub using_qmatrix: bool, + pub qm_y: u8, + pub qm_u: u8, + pub qm_v: u8, +} + +fn read_delta_q(r: &mut BitReader) -> Result { + Ok(if r.flag()? { r.su(7)? as i8 } else { 0 }) +} + +pub(crate) fn parse_quantization( + r: &mut BitReader, + seq: &SequenceHeader, +) -> Result { + let cc = &seq.color_config; + let mut q = QuantizationParams { + base_q_idx: r.f(8)? as u8, + delta_q_y_dc: read_delta_q(r)?, + ..Default::default() + }; + if cc.num_planes() > 1 { + if cc.separate_uv_delta_q { + q.diff_uv_delta = r.flag()?; + } + q.delta_q_u_dc = read_delta_q(r)?; + q.delta_q_u_ac = read_delta_q(r)?; + if q.diff_uv_delta { + q.delta_q_v_dc = read_delta_q(r)?; + q.delta_q_v_ac = read_delta_q(r)?; + } else { + q.delta_q_v_dc = q.delta_q_u_dc; + q.delta_q_v_ac = q.delta_q_u_ac; + } + } + q.using_qmatrix = r.flag()?; + if q.using_qmatrix { + q.qm_y = r.f(4)? as u8; + q.qm_u = r.f(4)? as u8; + q.qm_v = if cc.separate_uv_delta_q { + r.f(4)? as u8 + } else { + q.qm_u + }; + } + Ok(q) +} + +/// `segmentation_params()` (5.9.14), with the feature tables in effect for +/// the frame (loaded from the primary reference frame when not updated). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SegmentationParams { + pub segmentation_enabled: bool, + pub segmentation_update_map: bool, + pub segmentation_temporal_update: bool, + pub segmentation_update_data: bool, + /// `FeatureEnabled[segment][feature]`. + pub feature_enabled: [[bool; SEG_LVL_MAX]; MAX_SEGMENTS], + /// `FeatureData[segment][feature]`, already clipped. + pub feature_data: [[i16; SEG_LVL_MAX]; MAX_SEGMENTS], + pub seg_id_pre_skip: bool, + pub last_active_seg_id: u8, +} + +impl SegmentationParams { + /// `FeatureEnabled` as Vulkan takes it: one bitmask per segment, bit `j` + /// for feature `j`. + pub fn feature_enabled_masks(&self) -> [u8; MAX_SEGMENTS] { + self.feature_enabled.map(|features| { + features + .iter() + .enumerate() + .fold(0u8, |mask, (j, &on)| mask | ((on as u8) << j)) + }) + } + + /// `get_qindex(1, segment_id)` (7.12.2), the qindex ignoring delta q. + pub fn qindex(&self, base_q_idx: u8, segment_id: usize) -> u8 { + if self.segmentation_enabled && self.feature_enabled[segment_id][SEG_LVL_ALT_Q] { + let q = base_q_idx as i32 + self.feature_data[segment_id][SEG_LVL_ALT_Q] as i32; + q.clamp(0, 255) as u8 + } else { + base_q_idx + } + } +} + +const SEGMENTATION_FEATURE_BITS: [u32; SEG_LVL_MAX] = [8, 6, 6, 6, 6, 3, 0, 0]; +const SEGMENTATION_FEATURE_SIGNED: [bool; SEG_LVL_MAX] = + [true, true, true, true, true, false, false, false]; +const SEGMENTATION_FEATURE_MAX: [i32; SEG_LVL_MAX] = [ + 255, + MAX_LOOP_FILTER, + MAX_LOOP_FILTER, + MAX_LOOP_FILTER, + MAX_LOOP_FILTER, + 7, + 0, + 0, +]; + +/// Parse `segmentation_params()`. `previous` holds the feature tables from +/// `load_previous()` (or zeros after `setup_past_independence()`), which +/// stay in effect when the frame does not update them. +pub(crate) fn parse_segmentation( + r: &mut BitReader, + primary_ref_frame: u8, + previous: &SegmentationParams, +) -> Result { + let mut s = SegmentationParams { + segmentation_enabled: r.flag()?, + ..Default::default() + }; + if s.segmentation_enabled { + if primary_ref_frame == PRIMARY_REF_NONE { + s.segmentation_update_map = true; + s.segmentation_update_data = true; + } else { + s.segmentation_update_map = r.flag()?; + if s.segmentation_update_map { + s.segmentation_temporal_update = r.flag()?; + } + s.segmentation_update_data = r.flag()?; + } + if s.segmentation_update_data { + for i in 0..MAX_SEGMENTS { + for j in 0..SEG_LVL_MAX { + let enabled = r.flag()?; + s.feature_enabled[i][j] = enabled; + let mut clipped = 0; + if enabled { + let bits = SEGMENTATION_FEATURE_BITS[j]; + let limit = SEGMENTATION_FEATURE_MAX[j]; + clipped = if SEGMENTATION_FEATURE_SIGNED[j] { + r.su(1 + bits)?.clamp(-limit, limit) + } else { + (r.f(bits)? as i32).clamp(0, limit) + }; + } + s.feature_data[i][j] = clipped as i16; + } + } + } else { + s.feature_enabled = previous.feature_enabled; + s.feature_data = previous.feature_data; + } + } + for i in 0..MAX_SEGMENTS { + for j in 0..SEG_LVL_MAX { + if s.feature_enabled[i][j] { + s.last_active_seg_id = i as u8; + if j >= SEG_LVL_REF_FRAME { + s.seg_id_pre_skip = true; + } + } + } + } + Ok(s) +} + +/// `delta_q_params()` and `delta_lf_params()` (5.9.17, 5.9.18). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct DeltaParams { + pub delta_q_present: bool, + /// The coded `delta_q_res` (the shift is `1 << delta_q_res`). + pub delta_q_res: u8, + pub delta_lf_present: bool, + pub delta_lf_res: u8, + pub delta_lf_multi: bool, +} + +pub(crate) fn parse_delta_params( + r: &mut BitReader, + base_q_idx: u8, + allow_intrabc: bool, +) -> Result { + let mut d = DeltaParams::default(); + if base_q_idx > 0 { + d.delta_q_present = r.flag()?; + } + if d.delta_q_present { + d.delta_q_res = r.f(2)? as u8; + if !allow_intrabc { + d.delta_lf_present = r.flag()?; + } + if d.delta_lf_present { + d.delta_lf_res = r.f(2)? as u8; + d.delta_lf_multi = r.flag()?; + } + } + Ok(d) +} + +/// `loop_filter_params()` (5.9.11). The deltas are the values in effect for +/// the frame: loaded from the primary reference frame (or defaults), then +/// updated by whatever the frame codes. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct LoopFilterParams { + pub loop_filter_level: [u8; 4], + pub loop_filter_sharpness: u8, + pub loop_filter_delta_enabled: bool, + pub loop_filter_delta_update: bool, + /// Bit `i` set when `loop_filter_ref_deltas[i]` was coded (Vulkan form). + pub update_ref_delta: u8, + pub loop_filter_ref_deltas: [i8; TOTAL_REFS_PER_FRAME], + /// Bit `i` set when `loop_filter_mode_deltas[i]` was coded (Vulkan form). + pub update_mode_delta: u8, + pub loop_filter_mode_deltas: [i8; 2], +} + +pub(crate) fn parse_loop_filter( + r: &mut BitReader, + seq: &SequenceHeader, + coded_lossless: bool, + allow_intrabc: bool, + ref_deltas: [i8; TOTAL_REFS_PER_FRAME], + mode_deltas: [i8; 2], +) -> Result { + let mut lf = LoopFilterParams { + loop_filter_ref_deltas: ref_deltas, + loop_filter_mode_deltas: mode_deltas, + ..Default::default() + }; + if coded_lossless || allow_intrabc { + lf.loop_filter_ref_deltas = DEFAULT_LOOP_FILTER_REF_DELTAS; + lf.loop_filter_mode_deltas = [0, 0]; + return Ok(lf); + } + lf.loop_filter_level[0] = r.f(6)? as u8; + lf.loop_filter_level[1] = r.f(6)? as u8; + if seq.color_config.num_planes() > 1 + && (lf.loop_filter_level[0] != 0 || lf.loop_filter_level[1] != 0) + { + lf.loop_filter_level[2] = r.f(6)? as u8; + lf.loop_filter_level[3] = r.f(6)? as u8; + } + lf.loop_filter_sharpness = r.f(3)? as u8; + lf.loop_filter_delta_enabled = r.flag()?; + if lf.loop_filter_delta_enabled { + lf.loop_filter_delta_update = r.flag()?; + if lf.loop_filter_delta_update { + for i in 0..TOTAL_REFS_PER_FRAME { + if r.flag()? { + lf.update_ref_delta |= 1 << i; + lf.loop_filter_ref_deltas[i] = r.su(7)? as i8; + } + } + for i in 0..2 { + if r.flag()? { + lf.update_mode_delta |= 1 << i; + lf.loop_filter_mode_deltas[i] = r.su(7)? as i8; + } + } + } + } + Ok(lf) +} + +/// `cdef_params()` (5.9.19). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct CdefParams { + pub cdef_damping_minus_3: u8, + pub cdef_bits: u8, + pub cdef_y_pri_strength: [u8; 8], + /// Coded value (0..=3), as Vulkan takes it; the effective strength + /// maps 3 to 4. + pub cdef_y_sec_strength: [u8; 8], + pub cdef_uv_pri_strength: [u8; 8], + /// Coded value, like `cdef_y_sec_strength`. + pub cdef_uv_sec_strength: [u8; 8], +} + +pub(crate) fn parse_cdef( + r: &mut BitReader, + seq: &SequenceHeader, + coded_lossless: bool, + allow_intrabc: bool, +) -> Result { + let mut c = CdefParams::default(); + if coded_lossless || allow_intrabc || !seq.enable_cdef { + return Ok(c); + } + c.cdef_damping_minus_3 = r.f(2)? as u8; + c.cdef_bits = r.f(2)? as u8; + for i in 0..(1usize << c.cdef_bits) { + c.cdef_y_pri_strength[i] = r.f(4)? as u8; + c.cdef_y_sec_strength[i] = r.f(2)? as u8; + if seq.color_config.num_planes() > 1 { + c.cdef_uv_pri_strength[i] = r.f(4)? as u8; + c.cdef_uv_sec_strength[i] = r.f(2)? as u8; + } + } + Ok(c) +} + +/// `lr_params()` (5.9.20). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct LoopRestorationParams { + /// `FrameRestorationType` per plane: 0 none, 1 Wiener, 2 self-guided, + /// 3 switchable (the same numbering as Vulkan's enum). + pub frame_restoration_type: [u8; 3], + /// `LoopRestorationSize` per plane, in samples (32..=256). Vulkan takes + /// `log2(size) - 5`; see [`Self::size_log2_minus_5`]. + pub loop_restoration_size: [u16; 3], + pub uses_lr: bool, + pub uses_chroma_lr: bool, + /// The derived `lr_unit_shift` (after the extra shift). + pub lr_unit_shift: u8, + pub lr_uv_shift: u8, +} + +impl LoopRestorationParams { + pub fn size_log2_minus_5(&self) -> [u16; 3] { + self.loop_restoration_size + .map(|s| (s.max(32).trailing_zeros() as u16).saturating_sub(5)) + } +} + +/// Maps the coded `lr_type` to `FrameRestorationType`. +const REMAP_LR_TYPE: [u8; 4] = [0, 3, 1, 2]; + +pub(crate) fn parse_lr( + r: &mut BitReader, + seq: &SequenceHeader, + all_lossless: bool, + allow_intrabc: bool, +) -> Result { + let mut lr = LoopRestorationParams::default(); + if all_lossless || allow_intrabc || !seq.enable_restoration { + return Ok(lr); + } + for i in 0..seq.color_config.num_planes() { + let ty = REMAP_LR_TYPE[r.f(2)? as usize]; + lr.frame_restoration_type[i] = ty; + if ty != 0 { + lr.uses_lr = true; + if i > 0 { + lr.uses_chroma_lr = true; + } + } + } + if lr.uses_lr { + let mut shift = r.f(1)? as u8; + if seq.use_128x128_superblock { + shift += 1; + } else if shift != 0 { + shift += r.f(1)? as u8; + } + lr.lr_unit_shift = shift; + let size = RESTORATION_TILESIZE_MAX >> (2 - shift); + let cc = &seq.color_config; + if cc.subsampling_x && cc.subsampling_y && lr.uses_chroma_lr { + lr.lr_uv_shift = r.f(1)? as u8; + } + lr.loop_restoration_size = [size, size >> lr.lr_uv_shift, size >> lr.lr_uv_shift]; + } + Ok(lr) +} + +/// `global_motion_params()` (5.9.24), indexed by reference frame name. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GlobalMotionParams { + pub gm_type: [u8; TOTAL_REFS_PER_FRAME], + pub gm_params: [[i32; 6]; TOTAL_REFS_PER_FRAME], +} + +impl Default for GlobalMotionParams { + fn default() -> Self { + Self { + gm_type: [IDENTITY; TOTAL_REFS_PER_FRAME], + gm_params: [DEFAULT_GM_PARAMS; TOTAL_REFS_PER_FRAME], + } + } +} + +/// `decode_subexp(numSyms)` (5.9.27). +fn decode_subexp(r: &mut BitReader, num_syms: i32) -> Result { + let mut i = 0; + let mut mk = 0; + let k = 3; + loop { + let b2 = if i != 0 { k + i - 1 } else { k }; + let a = 1 << b2; + if num_syms <= mk + 3 * a { + return Ok(r.ns((num_syms - mk) as u32)? as i32 + mk); + } + if r.flag()? { + i += 1; + mk += a; + } else { + return Ok(r.f(b2 as u32)? as i32 + mk); + } + } +} + +/// `inverse_recenter(r, v)` (5.9.28). +fn inverse_recenter(r: i32, v: i32) -> i32 { + if v > 2 * r { + v + } else if v & 1 != 0 { + r - ((v + 1) >> 1) + } else { + r + (v >> 1) + } +} + +/// `decode_signed_subexp_with_ref(low, high, r)` (5.9.26). +fn decode_signed_subexp_with_ref( + reader: &mut BitReader, + low: i32, + high: i32, + r: i32, +) -> Result { + let mx = high - low; + let r = r - low; + let v = decode_subexp(reader, mx)?; + let x = if (r << 1) <= mx { + inverse_recenter(r, v) + } else { + mx - 1 - inverse_recenter(mx - 1 - r, v) + }; + Ok(x + low) +} + +/// Parse `global_motion_params()` against `prev`, the `PrevGmParams` from +/// the primary reference frame (or the defaults). +pub(crate) fn parse_global_motion( + r: &mut BitReader, + frame_is_intra: bool, + allow_high_precision_mv: bool, + prev: &[[i32; 6]; TOTAL_REFS_PER_FRAME], +) -> Result { + let mut gm = GlobalMotionParams::default(); + if frame_is_intra { + return Ok(gm); + } + #[allow(clippy::needless_range_loop)] // rf indexes three arrays. + for rf in LAST_FRAME..=ALTREF_FRAME { + let ty = if r.flag()? { + if r.flag()? { + ROTZOOM + } else if r.flag()? { + TRANSLATION + } else { + AFFINE + } + } else { + IDENTITY + }; + gm.gm_type[rf] = ty; + let mut read = |idx: usize, gm: &mut GlobalMotionParams| -> Result<()> { + let (abs_bits, prec_bits) = if idx < 2 { + if ty == TRANSLATION { + let hp = !allow_high_precision_mv as u32; + (GM_ABS_TRANS_ONLY_BITS - hp, GM_TRANS_ONLY_PREC_BITS - hp) + } else { + (GM_ABS_TRANS_BITS, GM_TRANS_PREC_BITS) + } + } else { + (GM_ABS_ALPHA_BITS, GM_ALPHA_PREC_BITS) + }; + let prec_diff = WARPEDMODEL_PREC_BITS - prec_bits; + let (round, sub) = if idx % 3 == 2 { + (1 << WARPEDMODEL_PREC_BITS, 1 << prec_bits) + } else { + (0, 0) + }; + let mx = 1 << abs_bits; + let reference = (prev[rf][idx] >> prec_diff) - sub; + let value = decode_signed_subexp_with_ref(r, -mx, mx + 1, reference)?; + gm.gm_params[rf][idx] = (value << prec_diff) + round; + Ok(()) + }; + if ty >= ROTZOOM { + read(2, &mut gm)?; + read(3, &mut gm)?; + if ty == AFFINE { + read(4, &mut gm)?; + read(5, &mut gm)?; + } else { + gm.gm_params[rf][4] = -gm.gm_params[rf][3]; + gm.gm_params[rf][5] = gm.gm_params[rf][2]; + } + } + if ty >= TRANSLATION { + read(0, &mut gm)?; + read(1, &mut gm)?; + } + } + Ok(gm) +} + +/// `film_grain_params()` (5.9.30). After `load_grain_params()` this holds +/// the referenced frame's parameters with this frame's `grain_seed`, which +/// is what Vulkan expects the application to supply. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FilmGrainParams { + pub apply_grain: bool, + pub grain_seed: u16, + /// As coded on this frame (Vulkan ignores it). + pub update_grain: bool, + /// As coded on this frame when `update_grain` is 0 (Vulkan ignores it). + pub film_grain_params_ref_idx: u8, + pub num_y_points: u8, + pub point_y_value: [u8; 14], + pub point_y_scaling: [u8; 14], + pub chroma_scaling_from_luma: bool, + pub num_cb_points: u8, + pub point_cb_value: [u8; 10], + pub point_cb_scaling: [u8; 10], + pub num_cr_points: u8, + pub point_cr_value: [u8; 10], + pub point_cr_scaling: [u8; 10], + pub grain_scaling_minus_8: u8, + pub ar_coeff_lag: u8, + pub ar_coeffs_y_plus_128: [u8; 24], + pub ar_coeffs_cb_plus_128: [u8; 25], + pub ar_coeffs_cr_plus_128: [u8; 25], + pub ar_coeff_shift_minus_6: u8, + pub grain_scale_shift: u8, + pub cb_mult: u8, + pub cb_luma_mult: u8, + pub cb_offset: u16, + pub cr_mult: u8, + pub cr_luma_mult: u8, + pub cr_offset: u16, + pub overlap_flag: bool, + pub clip_to_restricted_range: bool, +} + +impl Default for FilmGrainParams { + /// `reset_grain_params()`: everything zero. + fn default() -> Self { + Self { + apply_grain: false, + grain_seed: 0, + update_grain: false, + film_grain_params_ref_idx: 0, + num_y_points: 0, + point_y_value: [0; 14], + point_y_scaling: [0; 14], + chroma_scaling_from_luma: false, + num_cb_points: 0, + point_cb_value: [0; 10], + point_cb_scaling: [0; 10], + num_cr_points: 0, + point_cr_value: [0; 10], + point_cr_scaling: [0; 10], + grain_scaling_minus_8: 0, + ar_coeff_lag: 0, + ar_coeffs_y_plus_128: [0; 24], + ar_coeffs_cb_plus_128: [0; 25], + ar_coeffs_cr_plus_128: [0; 25], + ar_coeff_shift_minus_6: 0, + grain_scale_shift: 0, + cb_mult: 0, + cb_luma_mult: 0, + cb_offset: 0, + cr_mult: 0, + cr_luma_mult: 0, + cr_offset: 0, + overlap_flag: false, + clip_to_restricted_range: false, + } + } +} + +fn read_points(r: &mut BitReader, max: u8, values: &mut [u8], scalings: &mut [u8]) -> Result { + let n = r.f(4)? as u8; + if n > max { + return Err(PixelForgeError::InvalidInput(format!( + "AV1: {n} film grain scaling points, at most {max} allowed" + ))); + } + for i in 0..n as usize { + values[i] = r.f(8)? as u8; + scalings[i] = r.f(8)? as u8; + } + Ok(n) +} + +/// What `film_grain_params()` needs to know about the frame. +pub(crate) struct GrainContext { + pub show_frame: bool, + pub showable_frame: bool, + pub is_inter_frame: bool, +} + +/// Parse `film_grain_params()`. `load` performs `load_grain_params(idx)`, +/// returning `None` when slot `idx` holds no valid frame. +pub(crate) fn parse_film_grain( + r: &mut BitReader, + seq: &SequenceHeader, + ctx: &GrainContext, + load: impl Fn(u8) -> Option, +) -> Result> { + if !seq.film_grain_params_present || (!ctx.show_frame && !ctx.showable_frame) { + return Ok(Ok(FilmGrainParams::default())); + } + let mut g = FilmGrainParams { + apply_grain: r.flag()?, + ..Default::default() + }; + if !g.apply_grain { + return Ok(Ok(FilmGrainParams::default())); + } + g.grain_seed = r.f(16)? as u16; + g.update_grain = if ctx.is_inter_frame { r.flag()? } else { true }; + if !g.update_grain { + let idx = r.f(3)? as u8; + let Some(mut loaded) = load(idx) else { + return Ok(Err(idx)); + }; + loaded.grain_seed = g.grain_seed; + loaded.update_grain = false; + loaded.film_grain_params_ref_idx = idx; + return Ok(Ok(loaded)); + } + g.num_y_points = read_points(r, 14, &mut g.point_y_value, &mut g.point_y_scaling)?; + let cc = &seq.color_config; + g.chroma_scaling_from_luma = if cc.mono_chrome { false } else { r.flag()? }; + if !(cc.mono_chrome + || g.chroma_scaling_from_luma + || (cc.subsampling_x && cc.subsampling_y && g.num_y_points == 0)) + { + g.num_cb_points = read_points(r, 10, &mut g.point_cb_value, &mut g.point_cb_scaling)?; + g.num_cr_points = read_points(r, 10, &mut g.point_cr_value, &mut g.point_cr_scaling)?; + } + g.grain_scaling_minus_8 = r.f(2)? as u8; + g.ar_coeff_lag = r.f(2)? as u8; + let num_pos_luma = 2 * g.ar_coeff_lag as usize * (g.ar_coeff_lag as usize + 1); + let num_pos_chroma = if g.num_y_points != 0 { + for i in 0..num_pos_luma { + g.ar_coeffs_y_plus_128[i] = r.f(8)? as u8; + } + num_pos_luma + 1 + } else { + num_pos_luma + }; + if g.chroma_scaling_from_luma || g.num_cb_points != 0 { + for i in 0..num_pos_chroma { + g.ar_coeffs_cb_plus_128[i] = r.f(8)? as u8; + } + } + if g.chroma_scaling_from_luma || g.num_cr_points != 0 { + for i in 0..num_pos_chroma { + g.ar_coeffs_cr_plus_128[i] = r.f(8)? as u8; + } + } + g.ar_coeff_shift_minus_6 = r.f(2)? as u8; + g.grain_scale_shift = r.f(2)? as u8; + if g.num_cb_points != 0 { + g.cb_mult = r.f(8)? as u8; + g.cb_luma_mult = r.f(8)? as u8; + g.cb_offset = r.f(9)? as u16; + } + if g.num_cr_points != 0 { + g.cr_mult = r.f(8)? as u8; + g.cr_luma_mult = r.f(8)? as u8; + g.cr_offset = r.f(9)? as u16; + } + g.overlap_flag = r.flag()?; + g.clip_to_restricted_range = r.flag()?; + Ok(Ok(g)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inverse_recenter_matches_the_spec_table() { + // v above 2r passes through; odd v steps below r, even v above it. + assert_eq!(inverse_recenter(2, 5), 5); + assert_eq!(inverse_recenter(2, 1), 1); + assert_eq!(inverse_recenter(2, 3), 0); + assert_eq!(inverse_recenter(2, 2), 3); + assert_eq!(inverse_recenter(2, 0), 2); + } + + #[test] + fn subexp_reads_the_reference_back_for_a_zero_code() { + // decode_subexp reading 0 (more_bits=0, then f(3)=000) recenters to + // the reference itself. + let mut r = BitReader::new(&[0x00]); + let v = decode_signed_subexp_with_ref(&mut r, -4096, 4097, 17).unwrap(); + assert_eq!(v, 17); + assert_eq!(r.position(), 4); + } + + #[test] + fn loop_restoration_sizes_convert_to_vulkan_form() { + let lr = LoopRestorationParams { + loop_restoration_size: [256, 128, 64], + ..Default::default() + }; + assert_eq!(lr.size_log2_minus_5(), [3, 2, 1]); + } + + #[test] + fn film_grain_reuse_loads_the_reference_with_a_new_seed() { + let seq = SequenceHeader { + film_grain_params_present: true, + ..Default::default() + }; + let ctx = GrainContext { + show_frame: true, + showable_frame: true, + is_inter_frame: true, + }; + // apply_grain 1, grain_seed 0xbeef, update_grain 0, ref idx 3. + let bits = [0b1101_1111, 0b0111_0111, 0b1001_1000]; + let saved = FilmGrainParams { + apply_grain: true, + grain_seed: 1, + update_grain: true, + num_y_points: 5, + ..Default::default() + }; + let mut r = BitReader::new(&bits); + let loaded = parse_film_grain(&mut r, &seq, &ctx, |idx| { + assert_eq!(idx, 3); + Some(saved.clone()) + }) + .unwrap() + .unwrap(); + assert_eq!(r.position(), 21); + assert_eq!(loaded.grain_seed, 0xbeef); + assert_eq!(loaded.num_y_points, 5); + assert!(!loaded.update_grain); + assert_eq!(loaded.film_grain_params_ref_idx, 3); + + // An empty slot comes back as the slot to report missing. + let mut r = BitReader::new(&bits); + let missing = parse_film_grain(&mut r, &seq, &ctx, |_| None).unwrap(); + assert_eq!(missing, Err(3)); + } + + #[test] + fn feature_masks_pack_per_segment() { + let mut s = SegmentationParams::default(); + s.feature_enabled[0][0] = true; + s.feature_enabled[0][5] = true; + s.feature_enabled[7][7] = true; + let masks = s.feature_enabled_masks(); + assert_eq!(masks[0], 0b0010_0001); + assert_eq!(masks[7], 0b1000_0000); + } +} diff --git a/src/decoder/av1/parser.rs b/src/decoder/av1/parser.rs new file mode 100644 index 00000000..3d049fb5 --- /dev/null +++ b/src/decoder/av1/parser.rs @@ -0,0 +1,328 @@ +//! The OBU-level decoding process: which OBUs make up a frame, and when the +//! reference state moves on (AV1 specification sections 7.4, 7.5 and 7.20). + +use super::bits::BitReader; +use super::consts::TOTAL_REFS_PER_FRAME; +use super::frame_header::{FrameHeader, FrameType, HeaderOutcome, parse_uncompressed_header}; +use super::obu::{Obu, ObuType, in_operating_point}; +use super::refs::RefState; +use super::sequence::{SequenceHeader, parse_sequence_header}; +use super::tile::{TileGroup, TileLocation, parse_tile_group}; +use crate::error::{PixelForgeError, Result}; + +/// One frame as the OBUs describe it: the parsed header and where its data +/// sits. +/// +/// All offsets are relative to the start of the buffer the OBUs were +/// iterated from (see [`Obu::offset`]). Handing the driver that whole buffer +/// makes them usable as they are; handing it a sub-range means subtracting +/// the sub-range's start from each. +#[derive(Debug, Clone)] +pub struct Av1Frame { + pub header: FrameHeader, + /// The OBU that carried the header: `Frame`, `FrameHeader` or + /// `RedundantFrameHeader` (when the original was lost). + pub header_obu_type: ObuType, + /// Offset of the first byte of that OBU (its `obu_header()`), which is + /// what Vulkan's `frameHeaderOffset` asks for: "the byte offset of the + /// AV1 frame header OBU". + pub frame_header_offset: usize, + /// Offset of the first byte of the header's payload, the start of + /// `uncompressed_header()`. + pub frame_header_payload_offset: usize, + /// Bytes of payload the header takes: `uncompressed_header()` plus its + /// byte alignment in a frame OBU, plus its trailing bits in a frame + /// header OBU. + pub frame_header_bytes: usize, + /// `temporal_id` and `spatial_id` of the header OBU. + pub temporal_id: u8, + pub spatial_id: u8, + /// The tile groups in order, which together cover every tile. Empty for + /// `show_existing_frame`. + pub tile_groups: Vec, +} + +impl Av1Frame { + /// Every tile of the frame, in tile order. + pub fn tiles(&self) -> impl Iterator + '_ { + self.tile_groups + .iter() + .flat_map(|tg| tg.tiles.iter().copied()) + } +} + +/// What one OBU produced. +#[derive(Debug)] +pub enum Av1Event { + /// A sequence header was parsed. `changed` is false when it repeats the + /// one in effect, which encoders do at every key frame. + SequenceHeader { changed: bool }, + /// A frame is complete: all of its tile groups have arrived, or it is a + /// `show_existing_frame`. The caller decodes (or shows) it and then calls + /// [`Av1Parser::commit_frame`] or [`Av1Parser::discard_frame`]. + Frame(Box), + /// The next frame refers to reference slots that hold nothing (bit `i` + /// for slot `i`), as after joining a stream mid-way or losing a frame. + /// It cannot be decoded; its remaining OBUs are skipped, and the + /// decoder needs a key frame. + MissingReferences(u8), +} + +/// A frame whose header has arrived but not all of its tiles. +#[derive(Debug)] +struct InProgress { + frame: Av1Frame, + refs: Box, + next_tile: usize, +} + +/// A complete frame waiting for the caller to commit or discard it. +#[derive(Debug)] +struct Pending { + header: FrameHeader, + refs: Box, +} + +/// Stateful AV1 parser: feed it OBUs in order, get frames out. +#[derive(Debug, Default)] +pub struct Av1Parser { + sequence: Option, + refs: RefState, + /// `OrderHints` as the last committed frame left it. + order_hints: [u8; TOTAL_REFS_PER_FRAME], + current: Option, + pending: Option, + /// Set after `MissingReferences`, until the next frame header. + skipping: bool, +} + +impl Av1Parser { + pub fn new() -> Self { + Self::default() + } + + /// The sequence header in effect, once one has been seen. + pub fn sequence_header(&self) -> Option<&SequenceHeader> { + self.sequence.as_ref() + } + + /// The reference slots as of the last committed frame. While a frame is + /// pending, this is the state its header was parsed against, which is + /// what its references must be looked up in. + pub fn refs(&self) -> &RefState { + &self.refs + } + + /// Forget every reference and any frame in flight, keeping the sequence + /// header. After this, only a key frame decodes. + pub fn reset(&mut self) { + self.refs = RefState::default(); + self.order_hints = [0; TOTAL_REFS_PER_FRAME]; + self.current = None; + self.pending = None; + self.skipping = false; + } + + /// Apply the reference frame update process for the pending frame, now + /// decoded into DPB slot `dpb_slot` (`None` if the caller does not track + /// it). For `show_existing_frame` of a key frame the shown slot's DPB + /// slot is used instead of `dpb_slot`. + pub fn commit_frame(&mut self, dpb_slot: Option) -> Result<()> { + let pending = self.pending.take().ok_or_else(|| { + PixelForgeError::InvalidInput("AV1: no frame waiting to be committed".to_string()) + })?; + let Some(seq) = &self.sequence else { + return Err(PixelForgeError::InvalidInput( + "AV1: frame without a sequence header".to_string(), + )); + }; + let header = &pending.header; + let mut refs = pending.refs; + refs.update(seq, header, dpb_slot); + self.refs = *refs; + if !header.show_existing_frame || header.frame_type == FrameType::Key { + self.order_hints = header.order_hints; + } + Ok(()) + } + + /// Drop the pending frame, as when it failed to decode. The slots it + /// would have refreshed no longer hold what the stream expects there, + /// so they become invalid, and frames that refer to them report missing + /// references until a key frame refreshes them. + pub fn discard_frame(&mut self) { + if let Some(pending) = self.pending.take() { + for (i, slot) in self.refs.slots.iter_mut().enumerate() { + if (pending.header.refresh_frame_flags >> i) & 1 != 0 { + slot.valid = false; + } + } + } + } + + /// Process one OBU. + pub fn parse_obu(&mut self, obu: &Obu) -> Result> { + match obu.header.obu_type { + ObuType::TemporalDelimiter => { + // SeenFrameHeader = 0. A frame still missing tiles here is + // broken; say so, but leave the parser ready for the next one. + self.skipping = false; + if self.current.take().is_some() { + return Err(PixelForgeError::InvalidInput( + "AV1: temporal unit ended before the frame's last tile group".to_string(), + )); + } + Ok(None) + } + ObuType::SequenceHeader => { + let seq = parse_sequence_header(obu.payload)?; + let changed = self.sequence.as_ref() != Some(&seq); + self.sequence = Some(seq); + Ok(Some(Av1Event::SequenceHeader { changed })) + } + ObuType::FrameHeader | ObuType::RedundantFrameHeader | ObuType::Frame => { + if !self.selected(obu)? { + return Ok(None); + } + self.frame_header(obu) + } + ObuType::TileGroup => { + if !self.selected(obu)? || self.skipping { + return Ok(None); + } + let Some(current) = self.current.as_mut() else { + return Err(PixelForgeError::InvalidInput( + "AV1: tile group without a frame header".to_string(), + )); + }; + let tg = parse_tile_group( + obu.payload, + obu.payload_offset(), + ¤t.frame.header.tile_info, + )?; + self.add_tile_group(tg) + } + ObuType::Metadata | ObuType::TileList | ObuType::Padding | ObuType::Reserved(_) => { + Ok(None) + } + } + } + + /// Whether the OBU is in operating point 0; frame data before any + /// sequence header is an error. + fn selected(&self, obu: &Obu) -> Result { + let seq = self.sequence.as_ref().ok_or_else(|| { + PixelForgeError::InvalidInput("AV1: frame data before a sequence header".to_string()) + })?; + Ok(in_operating_point(&obu.header, seq.operating_point_idc())) + } + + fn frame_header(&mut self, obu: &Obu) -> Result> { + let is_frame_obu = obu.header.obu_type == ObuType::Frame; + if self.current.is_some() { + if is_frame_obu { + self.current = None; + return Err(PixelForgeError::InvalidInput( + "AV1: frame OBU before the previous frame's last tile group".to_string(), + )); + } + // frame_header_copy(): a repeat of the header in effect. + return Ok(None); + } + if self.skipping && obu.header.obu_type == ObuType::RedundantFrameHeader { + return Ok(None); + } + if self.pending.is_some() { + return Err(PixelForgeError::InvalidInput( + "AV1: new frame before the previous one was committed or discarded".to_string(), + )); + } + let seq = self.sequence.as_ref().expect("checked by selected()"); + + let mut r = BitReader::new(obu.payload); + let outcome = + parse_uncompressed_header(&mut r, seq, &self.refs, &obu.header, self.order_hints)?; + let (header, refs) = match outcome { + HeaderOutcome::Parsed(header, refs) => (header, refs), + HeaderOutcome::MissingReferences(slots) => { + self.skipping = true; + return Ok(Some(Av1Event::MissingReferences(slots))); + } + }; + self.skipping = false; + + if is_frame_obu { + if header.show_existing_frame { + return Err(PixelForgeError::InvalidInput( + "AV1: frame OBU with show_existing_frame".to_string(), + )); + } + r.byte_alignment()?; + } else { + r.trailing_bits(obu.payload.len())?; + } + let header_bytes = r.byte_position(); + + let frame = Av1Frame { + header: *header, + header_obu_type: obu.header.obu_type, + frame_header_offset: obu.offset, + frame_header_payload_offset: obu.payload_offset(), + frame_header_bytes: header_bytes, + temporal_id: obu.header.temporal_id, + spatial_id: obu.header.spatial_id, + tile_groups: Vec::new(), + }; + if frame.header.show_existing_frame { + self.pending = Some(Pending { + header: frame.header.clone(), + refs, + }); + return Ok(Some(Av1Event::Frame(Box::new(frame)))); + } + self.current = Some(InProgress { + frame, + refs, + next_tile: 0, + }); + if !is_frame_obu { + return Ok(None); + } + let current = self.current.as_ref().expect("just set"); + let tg = parse_tile_group( + &obu.payload[header_bytes..], + obu.payload_offset() + header_bytes, + ¤t.frame.header.tile_info, + ); + match tg { + Ok(tg) => self.add_tile_group(tg), + Err(e) => { + self.current = None; + Err(e) + } + } + } + + fn add_tile_group(&mut self, tg: TileGroup) -> Result> { + let current = self.current.as_mut().expect("caller checked"); + if tg.tg_start as usize != current.next_tile { + let expected = current.next_tile; + self.current = None; + return Err(PixelForgeError::InvalidInput(format!( + "AV1: tile group starts at tile {}, expected {expected}", + tg.tg_start + ))); + } + current.next_tile = tg.tg_end as usize + 1; + current.frame.tile_groups.push(tg); + if current.next_tile < current.frame.header.tile_info.num_tiles() { + return Ok(None); + } + let done = self.current.take().expect("checked above"); + self.pending = Some(Pending { + header: done.frame.header.clone(), + refs: done.refs, + }); + Ok(Some(Av1Event::Frame(Box::new(done.frame)))) + } +} diff --git a/src/decoder/av1/refs.rs b/src/decoder/av1/refs.rs new file mode 100644 index 00000000..fcde1f94 --- /dev/null +++ b/src/decoder/av1/refs.rs @@ -0,0 +1,377 @@ +//! Reference frame state (AV1 specification sections 7.8, 7.20 and 7.21). +//! +//! AV1 keeps eight reference slots. Each slot remembers, besides the picture +//! itself, the frame's size, order hints and the coding-tool state that later +//! headers inherit from it (loop filter deltas, segmentation features, global +//! motion and film grain). The header parser reads this table; the reference +//! frame update process writes it once a frame is decoded. + +use super::consts::*; +use super::frame_header::{FrameHeader, FrameType}; +use super::params::FilmGrainParams; +use super::sequence::SequenceHeader; + +/// Everything one reference slot remembers. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RefSlot { + /// `RefValid`. + pub valid: bool, + /// `RefFrameId`. + pub frame_id: u32, + /// `RefFrameType`. + pub frame_type: FrameType, + /// `RefShowableFrame`: whether `show_existing_frame` may show it. + pub showable_frame: bool, + pub upscaled_width: u32, + pub frame_width: u32, + pub frame_height: u32, + pub render_width: u32, + pub render_height: u32, + pub mi_cols: u32, + pub mi_rows: u32, + pub bit_depth: u8, + pub subsampling_x: bool, + pub subsampling_y: bool, + /// `RefOrderHint`. + pub order_hint: u8, + /// `SavedOrderHints`, indexed by reference frame name. + pub saved_order_hints: [u8; TOTAL_REFS_PER_FRAME], + /// The frame's `RefFrameSignBias` as a bitmask indexed by reference + /// frame name, as Vulkan's reference info takes it. + pub ref_frame_sign_bias: u8, + /// Kept for Vulkan's reference info. + pub disable_frame_end_update_cdf: bool, + /// Kept for Vulkan's reference info. + pub segmentation_enabled: bool, + /// `SavedLoopFilterRefDeltas`. + pub loop_filter_ref_deltas: [i8; TOTAL_REFS_PER_FRAME], + /// `SavedLoopFilterModeDeltas`. + pub loop_filter_mode_deltas: [i8; 2], + /// `SavedFeatureEnabled`. + pub feature_enabled: [[bool; SEG_LVL_MAX]; MAX_SEGMENTS], + /// `SavedFeatureData`. + pub feature_data: [[i16; SEG_LVL_MAX]; MAX_SEGMENTS], + /// `SavedGmParams`. + pub gm_params: [[i32; 6]; TOTAL_REFS_PER_FRAME], + /// The saved film grain parameters (`load_grain_params()`). + pub film_grain: FilmGrainParams, + /// The Vulkan DPB slot holding this frame's picture, which the parser + /// never looks at: the decoder fills it in through + /// [`Av1Parser::commit_frame`](super::Av1Parser::commit_frame). + pub dpb_slot: Option, +} + +impl Default for RefSlot { + fn default() -> Self { + Self { + valid: false, + frame_id: 0, + frame_type: FrameType::Key, + showable_frame: false, + upscaled_width: 0, + frame_width: 0, + frame_height: 0, + render_width: 0, + render_height: 0, + mi_cols: 0, + mi_rows: 0, + bit_depth: 0, + subsampling_x: false, + subsampling_y: false, + order_hint: 0, + saved_order_hints: [0; TOTAL_REFS_PER_FRAME], + ref_frame_sign_bias: 0, + disable_frame_end_update_cdf: false, + segmentation_enabled: false, + loop_filter_ref_deltas: DEFAULT_LOOP_FILTER_REF_DELTAS, + loop_filter_mode_deltas: [0; 2], + feature_enabled: [[false; SEG_LVL_MAX]; MAX_SEGMENTS], + feature_data: [[0; SEG_LVL_MAX]; MAX_SEGMENTS], + gm_params: [DEFAULT_GM_PARAMS; TOTAL_REFS_PER_FRAME], + film_grain: FilmGrainParams::default(), + dpb_slot: None, + } + } +} + +/// The eight reference slots. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct RefState { + pub slots: [RefSlot; NUM_REF_FRAMES], +} + +impl RefState { + /// The reference frame update process (7.20): store the decoded frame + /// in every slot `refresh_frame_flags` selects, with `dpb_slot` as the + /// picture's location. + /// + /// For a key frame shown with `show_existing_frame`, the loading process + /// (7.21) makes the frame the current one first, so every slot becomes a + /// copy of the shown slot, DPB location included. + pub fn update(&mut self, seq: &SequenceHeader, header: &FrameHeader, dpb_slot: Option) { + if header.show_existing_frame { + if header.frame_type == FrameType::Key { + let mut shown = self.slots[header.frame_to_show_map_idx as usize].clone(); + // A key frame may be shown this way at most once. + shown.showable_frame = false; + for i in 0..NUM_REF_FRAMES { + if (header.refresh_frame_flags >> i) & 1 != 0 { + self.slots[i] = shown.clone(); + } + } + } + return; + } + + let size = &header.frame_size; + let slot = RefSlot { + valid: true, + frame_id: header.current_frame_id, + frame_type: header.frame_type, + showable_frame: header.showable_frame, + upscaled_width: size.upscaled_width, + frame_width: size.frame_width, + frame_height: size.frame_height, + render_width: size.render_width, + render_height: size.render_height, + mi_cols: size.mi_cols, + mi_rows: size.mi_rows, + bit_depth: seq.color_config.bit_depth, + subsampling_x: seq.color_config.subsampling_x, + subsampling_y: seq.color_config.subsampling_y, + order_hint: header.order_hint, + saved_order_hints: header.order_hints, + ref_frame_sign_bias: header.ref_frame_sign_bias_mask(), + disable_frame_end_update_cdf: header.disable_frame_end_update_cdf, + segmentation_enabled: header.segmentation.segmentation_enabled, + loop_filter_ref_deltas: header.loop_filter.loop_filter_ref_deltas, + loop_filter_mode_deltas: header.loop_filter.loop_filter_mode_deltas, + feature_enabled: header.segmentation.feature_enabled, + feature_data: header.segmentation.feature_data, + gm_params: header.global_motion.gm_params, + film_grain: header.film_grain.clone(), + dpb_slot, + }; + for i in 0..NUM_REF_FRAMES { + if (header.refresh_frame_flags >> i) & 1 != 0 { + self.slots[i] = slot.clone(); + } + } + } + + /// Whether any slot still refers to DPB slot `dpb_slot`, which tells the + /// decoder when a picture can be reused. + pub fn dpb_slot_in_use(&self, dpb_slot: u8) -> bool { + self.slots.iter().any(|s| s.dpb_slot == Some(dpb_slot)) + } +} + +/// `get_relative_dist(a, b)` (7.12.3). +pub fn relative_dist(seq: &SequenceHeader, a: u8, b: u8) -> i32 { + if !seq.enable_order_hint || seq.order_hint_bits == 0 { + return 0; + } + let diff = a as i32 - b as i32; + let m = 1 << (seq.order_hint_bits - 1); + (diff & (m - 1)) - (diff & m) +} + +/// The set_frame_refs process (7.8): derive all seven `ref_frame_idx` from +/// the two coded with `frame_refs_short_signaling`. +pub fn set_frame_refs( + seq: &SequenceHeader, + refs: &RefState, + order_hint: u8, + last_frame_idx: u8, + gold_frame_idx: u8, +) -> [u8; REFS_PER_FRAME] { + let mut ref_frame_idx = [-1i32; REFS_PER_FRAME]; + ref_frame_idx[0] = last_frame_idx as i32; + ref_frame_idx[GOLDEN_FRAME - LAST_FRAME] = gold_frame_idx as i32; + let mut used = [false; NUM_REF_FRAMES]; + used[last_frame_idx as usize] = true; + used[gold_frame_idx as usize] = true; + + let cur_frame_hint = 1i32 << (seq.order_hint_bits.max(1) - 1); + let shifted: [i32; NUM_REF_FRAMES] = std::array::from_fn(|i| { + cur_frame_hint + relative_dist(seq, refs.slots[i].order_hint, order_hint) + }); + + // Latest or earliest unused backward (future) reference, or latest + // forward (past) one. + let find = |used: &[bool; NUM_REF_FRAMES], backward: bool, latest: bool| -> Option { + let mut best: Option<(usize, i32)> = None; + for (i, &hint) in shifted.iter().enumerate() { + let side = if backward { + hint >= cur_frame_hint + } else { + hint < cur_frame_hint + }; + if used[i] || !side { + continue; + } + let better = match best { + None => true, + Some((_, b)) if latest => hint >= b, + Some((_, b)) => hint < b, + }; + if better { + best = Some((i, hint)); + } + } + best.map(|(i, _)| i) + }; + + if let Some(r) = find(&used, true, true) { + ref_frame_idx[ALTREF_FRAME - LAST_FRAME] = r as i32; + used[r] = true; + } + if let Some(r) = find(&used, true, false) { + ref_frame_idx[BWDREF_FRAME - LAST_FRAME] = r as i32; + used[r] = true; + } + if let Some(r) = find(&used, true, false) { + ref_frame_idx[ALTREF2_FRAME - LAST_FRAME] = r as i32; + used[r] = true; + } + const REF_FRAME_LIST: [usize; REFS_PER_FRAME - 2] = [ + LAST2_FRAME, + LAST3_FRAME, + BWDREF_FRAME, + ALTREF2_FRAME, + ALTREF_FRAME, + ]; + for ref_frame in REF_FRAME_LIST { + if ref_frame_idx[ref_frame - LAST_FRAME] < 0 + && let Some(r) = find(&used, false, true) + { + ref_frame_idx[ref_frame - LAST_FRAME] = r as i32; + used[r] = true; + } + } + + // Whatever is left gets the earliest frame of all. + let mut earliest: Option<(usize, i32)> = None; + for (i, &hint) in shifted.iter().enumerate() { + if earliest.is_none_or(|(_, e)| hint < e) { + earliest = Some((i, hint)); + } + } + let fallback = earliest.map_or(0, |(i, _)| i as i32); + ref_frame_idx.map(|idx| if idx < 0 { fallback } else { idx } as u8) +} + +/// The frame id check of `mark_ref_frames(idLen)` (5.9.4): invalidate the +/// slots whose frame id cannot precede `current_frame_id`. +pub fn mark_ref_frames(seq: &SequenceHeader, refs: &mut RefState, current_frame_id: u32) { + let id_len = seq.frame_id_length(); + let diff_len = seq.delta_frame_id_length_minus_2 as u32 + 2; + for slot in refs.slots.iter_mut() { + let id = slot.frame_id; + let stale = if current_frame_id > (1 << diff_len) { + id > current_frame_id || id < current_frame_id - (1 << diff_len) + } else { + id > current_frame_id && id < (1 << id_len) + current_frame_id - (1 << diff_len) + }; + if stale { + slot.valid = false; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn seq_with_order_hints(bits: u8) -> SequenceHeader { + SequenceHeader { + enable_order_hint: true, + order_hint_bits: bits, + ..Default::default() + } + } + + #[test] + fn relative_dist_wraps() { + let seq = seq_with_order_hints(7); + assert_eq!(relative_dist(&seq, 5, 3), 2); + assert_eq!(relative_dist(&seq, 3, 5), -2); + // 127 is one before 0 with 7-bit hints. + assert_eq!(relative_dist(&seq, 127, 0), -1); + assert_eq!(relative_dist(&seq, 0, 127), 1); + assert_eq!(relative_dist(&SequenceHeader::default(), 9, 1), 0); + } + + #[test] + fn set_frame_refs_orders_past_and_future() { + // Current frame 10; slots hold 2, 4, 6, 8 (past) and 12, 14, 16 and + // 9 (one more past frame). + let seq = seq_with_order_hints(7); + let mut refs = RefState::default(); + for (slot, hint) in [2u8, 4, 6, 8, 12, 14, 16, 9].iter().enumerate() { + refs.slots[slot].order_hint = *hint; + } + // LAST is slot 3 (8), GOLDEN slot 0 (2). + let idx = set_frame_refs(&seq, &refs, 10, 3, 0); + assert_eq!(idx[0], 3, "LAST as coded"); + assert_eq!(idx[GOLDEN_FRAME - LAST_FRAME], 0, "GOLDEN as coded"); + assert_eq!( + idx[ALTREF_FRAME - LAST_FRAME], + 6, + "ALTREF: latest future (16)" + ); + assert_eq!( + idx[BWDREF_FRAME - LAST_FRAME], + 4, + "BWDREF: earliest future (12)" + ); + assert_eq!( + idx[ALTREF2_FRAME - LAST_FRAME], + 5, + "ALTREF2: next future (14)" + ); + assert_eq!( + idx[LAST2_FRAME - LAST_FRAME], + 7, + "LAST2: latest past left (9)" + ); + assert_eq!(idx[LAST3_FRAME - LAST_FRAME], 2, "LAST3: next past (6)"); + } + + #[test] + fn set_frame_refs_fills_gaps_with_the_earliest_frame() { + // Every slot in the past and only three distinct ones: the rest of + // the list falls back to the earliest frame. + let seq = seq_with_order_hints(7); + let mut refs = RefState::default(); + for (slot, hint) in [5u8, 3, 1, 1, 1, 1, 1, 1].iter().enumerate() { + refs.slots[slot].order_hint = *hint; + } + let idx = set_frame_refs(&seq, &refs, 6, 0, 1); + assert_eq!(idx[0], 0); + assert_eq!(idx[GOLDEN_FRAME - LAST_FRAME], 1); + // No future frames: BWDREF/ALTREF2/ALTREF take the remaining past + // frames in latest-first order, then the earliest frame (slot 2). + assert!(idx.iter().all(|&i| i < 8)); + assert_eq!(idx[LAST2_FRAME - LAST_FRAME], 7); + } + + #[test] + fn mark_ref_frames_drops_ids_from_the_future() { + let seq = SequenceHeader { + frame_id_numbers_present_flag: true, + delta_frame_id_length_minus_2: 2, // diffLen 4 + additional_frame_id_length_minus_1: 3, // idLen 8 + ..Default::default() + }; + let mut refs = RefState::default(); + for (i, slot) in refs.slots.iter_mut().enumerate() { + slot.valid = true; + slot.frame_id = [30, 20, 14, 31, 40, 25, 15, 29][i]; + } + // Current id 30: valid ids are 14..=30. + mark_ref_frames(&seq, &mut refs, 30); + let valid: Vec = refs.slots.iter().map(|s| s.valid).collect(); + assert_eq!(valid, [true, true, true, false, false, true, true, true]); + } +} diff --git a/src/decoder/av1/sequence.rs b/src/decoder/av1/sequence.rs new file mode 100644 index 00000000..c7032f49 --- /dev/null +++ b/src/decoder/av1/sequence.rs @@ -0,0 +1,355 @@ +//! Sequence header OBU (AV1 specification section 5.5). +//! +//! Every syntax element is kept, including those Vulkan does not take, so a +//! change in any of them can be detected by comparing two headers. + +use super::bits::BitReader; +use super::consts::{SELECT_INTEGER_MV, SELECT_SCREEN_CONTENT_TOOLS}; +use crate::error::{PixelForgeError, Result}; + +/// `timing_info()` (section 5.5.3). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct TimingInfo { + pub num_units_in_display_tick: u32, + pub time_scale: u32, + pub equal_picture_interval: bool, + /// Zero unless `equal_picture_interval`. + pub num_ticks_per_picture_minus_1: u32, +} + +/// `decoder_model_info()` (section 5.5.4). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct DecoderModelInfo { + pub buffer_delay_length_minus_1: u8, + pub num_units_in_decoding_tick: u32, + pub buffer_removal_time_length_minus_1: u8, + pub frame_presentation_time_length_minus_1: u8, +} + +/// One entry of the operating point loop. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct OperatingPoint { + pub operating_point_idc: u16, + pub seq_level_idx: u8, + pub seq_tier: u8, + pub decoder_model_present_for_this_op: bool, + /// `operating_parameters_info()`, when the decoder model is present. + pub decoder_buffer_delay: u32, + pub encoder_buffer_delay: u32, + pub low_delay_mode_flag: bool, + pub initial_display_delay_present_for_this_op: bool, + pub initial_display_delay_minus_1: u8, +} + +/// `color_config()` (section 5.5.2), with the derived `BitDepth`. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct ColorConfig { + pub high_bitdepth: bool, + pub twelve_bit: bool, + /// `BitDepth`: 8, 10 or 12. + pub bit_depth: u8, + pub mono_chrome: bool, + pub color_description_present_flag: bool, + pub color_primaries: u8, + pub transfer_characteristics: u8, + pub matrix_coefficients: u8, + pub color_range: bool, + pub subsampling_x: bool, + pub subsampling_y: bool, + pub chroma_sample_position: u8, + pub separate_uv_delta_q: bool, +} + +impl ColorConfig { + /// `NumPlanes`. + pub fn num_planes(&self) -> usize { + if self.mono_chrome { 1 } else { 3 } + } +} + +/// Parsed sequence header OBU. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct SequenceHeader { + pub seq_profile: u8, + pub still_picture: bool, + pub reduced_still_picture_header: bool, + pub timing_info: Option, + pub decoder_model_info: Option, + pub initial_display_delay_present_flag: bool, + /// At least one entry; the parser decodes operating point 0. + pub operating_points: Vec, + pub frame_width_bits_minus_1: u8, + pub frame_height_bits_minus_1: u8, + pub max_frame_width_minus_1: u16, + pub max_frame_height_minus_1: u16, + pub frame_id_numbers_present_flag: bool, + pub delta_frame_id_length_minus_2: u8, + pub additional_frame_id_length_minus_1: u8, + pub use_128x128_superblock: bool, + pub enable_filter_intra: bool, + pub enable_intra_edge_filter: bool, + pub enable_interintra_compound: bool, + pub enable_masked_compound: bool, + pub enable_warped_motion: bool, + pub enable_dual_filter: bool, + pub enable_order_hint: bool, + pub enable_jnt_comp: bool, + pub enable_ref_frame_mvs: bool, + pub seq_choose_screen_content_tools: bool, + /// 0, 1, or `SELECT_SCREEN_CONTENT_TOOLS` (2). + pub seq_force_screen_content_tools: u8, + pub seq_choose_integer_mv: bool, + /// 0, 1, or `SELECT_INTEGER_MV` (2). + pub seq_force_integer_mv: u8, + /// `OrderHintBits`: 0 without order hints, else 1..=8. + pub order_hint_bits: u8, + pub enable_superres: bool, + pub enable_cdef: bool, + pub enable_restoration: bool, + pub color_config: ColorConfig, + pub film_grain_params_present: bool, +} + +impl SequenceHeader { + /// `OperatingPointIdc` of operating point 0, the one this parser decodes. + pub fn operating_point_idc(&self) -> u16 { + self.operating_points + .first() + .map_or(0, |op| op.operating_point_idc) + } + + /// `order_hint_bits_minus_1` as Vulkan takes it (0 when absent). + pub fn order_hint_bits_minus_1(&self) -> u8 { + self.order_hint_bits.saturating_sub(1) + } + + /// `idLen`, the width of frame ids, when they are present. + pub fn frame_id_length(&self) -> u32 { + self.additional_frame_id_length_minus_1 as u32 + + self.delta_frame_id_length_minus_2 as u32 + + 3 + } + + /// `equal_picture_interval`, false without timing info. + pub fn equal_picture_interval(&self) -> bool { + self.timing_info + .as_ref() + .is_some_and(|t| t.equal_picture_interval) + } + + pub fn max_frame_width(&self) -> u32 { + self.max_frame_width_minus_1 as u32 + 1 + } + + pub fn max_frame_height(&self) -> u32 { + self.max_frame_height_minus_1 as u32 + 1 + } +} + +/// Parse a sequence header OBU payload, including its trailing bits. +pub fn parse_sequence_header(payload: &[u8]) -> Result { + let mut r = BitReader::new(payload); + let mut seq = SequenceHeader { + seq_profile: r.f(3)? as u8, + still_picture: r.flag()?, + reduced_still_picture_header: r.flag()?, + ..Default::default() + }; + if seq.seq_profile > 2 { + return Err(PixelForgeError::InvalidInput(format!( + "AV1: reserved seq_profile {}", + seq.seq_profile + ))); + } + + if seq.reduced_still_picture_header { + seq.operating_points.push(OperatingPoint { + seq_level_idx: r.f(5)? as u8, + ..Default::default() + }); + } else { + if r.flag()? { + let mut timing = TimingInfo { + num_units_in_display_tick: r.f(32)?, + time_scale: r.f(32)?, + equal_picture_interval: r.flag()?, + num_ticks_per_picture_minus_1: 0, + }; + if timing.equal_picture_interval { + timing.num_ticks_per_picture_minus_1 = r.uvlc()?; + } + seq.timing_info = Some(timing); + if r.flag()? { + seq.decoder_model_info = Some(DecoderModelInfo { + buffer_delay_length_minus_1: r.f(5)? as u8, + num_units_in_decoding_tick: r.f(32)?, + buffer_removal_time_length_minus_1: r.f(5)? as u8, + frame_presentation_time_length_minus_1: r.f(5)? as u8, + }); + } + } + seq.initial_display_delay_present_flag = r.flag()?; + let count = r.f(5)? + 1; + for _ in 0..count { + let mut op = OperatingPoint { + operating_point_idc: r.f(12)? as u16, + seq_level_idx: r.f(5)? as u8, + ..Default::default() + }; + if op.seq_level_idx > 7 { + op.seq_tier = r.f(1)? as u8; + } + if let Some(model) = &seq.decoder_model_info { + op.decoder_model_present_for_this_op = r.flag()?; + if op.decoder_model_present_for_this_op { + let n = model.buffer_delay_length_minus_1 as u32 + 1; + op.decoder_buffer_delay = r.f(n)?; + op.encoder_buffer_delay = r.f(n)?; + op.low_delay_mode_flag = r.flag()?; + } + } + if seq.initial_display_delay_present_flag { + op.initial_display_delay_present_for_this_op = r.flag()?; + if op.initial_display_delay_present_for_this_op { + op.initial_display_delay_minus_1 = r.f(4)? as u8; + } + } + seq.operating_points.push(op); + } + } + + seq.frame_width_bits_minus_1 = r.f(4)? as u8; + seq.frame_height_bits_minus_1 = r.f(4)? as u8; + seq.max_frame_width_minus_1 = r.f(seq.frame_width_bits_minus_1 as u32 + 1)? as u16; + seq.max_frame_height_minus_1 = r.f(seq.frame_height_bits_minus_1 as u32 + 1)? as u16; + if !seq.reduced_still_picture_header { + seq.frame_id_numbers_present_flag = r.flag()?; + } + if seq.frame_id_numbers_present_flag { + seq.delta_frame_id_length_minus_2 = r.f(4)? as u8; + seq.additional_frame_id_length_minus_1 = r.f(3)? as u8; + if seq.frame_id_length() > 16 { + // The spec caps idLen at 16 bits. + return Err(PixelForgeError::InvalidInput( + "AV1: frame id length exceeds 16 bits".to_string(), + )); + } + } + seq.use_128x128_superblock = r.flag()?; + seq.enable_filter_intra = r.flag()?; + seq.enable_intra_edge_filter = r.flag()?; + + if seq.reduced_still_picture_header { + seq.seq_force_screen_content_tools = SELECT_SCREEN_CONTENT_TOOLS; + seq.seq_force_integer_mv = SELECT_INTEGER_MV; + } else { + seq.enable_interintra_compound = r.flag()?; + seq.enable_masked_compound = r.flag()?; + seq.enable_warped_motion = r.flag()?; + seq.enable_dual_filter = r.flag()?; + seq.enable_order_hint = r.flag()?; + if seq.enable_order_hint { + seq.enable_jnt_comp = r.flag()?; + seq.enable_ref_frame_mvs = r.flag()?; + } + seq.seq_choose_screen_content_tools = r.flag()?; + seq.seq_force_screen_content_tools = if seq.seq_choose_screen_content_tools { + SELECT_SCREEN_CONTENT_TOOLS + } else { + r.f(1)? as u8 + }; + seq.seq_force_integer_mv = if seq.seq_force_screen_content_tools > 0 { + seq.seq_choose_integer_mv = r.flag()?; + if seq.seq_choose_integer_mv { + SELECT_INTEGER_MV + } else { + r.f(1)? as u8 + } + } else { + SELECT_INTEGER_MV + }; + if seq.enable_order_hint { + seq.order_hint_bits = r.f(3)? as u8 + 1; + } + } + + seq.enable_superres = r.flag()?; + seq.enable_cdef = r.flag()?; + seq.enable_restoration = r.flag()?; + seq.color_config = parse_color_config(&mut r, seq.seq_profile)?; + seq.film_grain_params_present = r.flag()?; + r.trailing_bits(payload.len())?; + Ok(seq) +} + +/// Color primaries, transfer and matrix values the parser branches on. +const CP_BT_709: u8 = 1; +const CP_UNSPECIFIED: u8 = 2; +const TC_UNSPECIFIED: u8 = 2; +const TC_SRGB: u8 = 13; +const MC_IDENTITY: u8 = 0; +const MC_UNSPECIFIED: u8 = 2; + +fn parse_color_config(r: &mut BitReader, seq_profile: u8) -> Result { + let mut cc = ColorConfig { + high_bitdepth: r.flag()?, + ..Default::default() + }; + cc.bit_depth = if seq_profile == 2 && cc.high_bitdepth { + cc.twelve_bit = r.flag()?; + if cc.twelve_bit { 12 } else { 10 } + } else if cc.high_bitdepth { + 10 + } else { + 8 + }; + cc.mono_chrome = if seq_profile == 1 { false } else { r.flag()? }; + cc.color_description_present_flag = r.flag()?; + if cc.color_description_present_flag { + cc.color_primaries = r.f(8)? as u8; + cc.transfer_characteristics = r.f(8)? as u8; + cc.matrix_coefficients = r.f(8)? as u8; + } else { + cc.color_primaries = CP_UNSPECIFIED; + cc.transfer_characteristics = TC_UNSPECIFIED; + cc.matrix_coefficients = MC_UNSPECIFIED; + } + + if cc.mono_chrome { + cc.color_range = r.flag()?; + cc.subsampling_x = true; + cc.subsampling_y = true; + // CSP_UNKNOWN, and no separate_uv_delta_q to read. + return Ok(cc); + } + if cc.color_primaries == CP_BT_709 + && cc.transfer_characteristics == TC_SRGB + && cc.matrix_coefficients == MC_IDENTITY + { + cc.color_range = true; + } else { + cc.color_range = r.flag()?; + match seq_profile { + 0 => { + cc.subsampling_x = true; + cc.subsampling_y = true; + } + 1 => {} + _ => { + if cc.bit_depth == 12 { + cc.subsampling_x = r.flag()?; + if cc.subsampling_x { + cc.subsampling_y = r.flag()?; + } + } else { + cc.subsampling_x = true; + } + } + } + if cc.subsampling_x && cc.subsampling_y { + cc.chroma_sample_position = r.f(2)? as u8; + } + } + cc.separate_uv_delta_q = r.flag()?; + Ok(cc) +} diff --git a/src/decoder/av1/std_info.rs b/src/decoder/av1/std_info.rs new file mode 100644 index 00000000..a4abc766 --- /dev/null +++ b/src/decoder/av1/std_info.rs @@ -0,0 +1,351 @@ +//! Conversion of parsed headers to the Vulkan Video Std structures for AV1. +//! +//! The Std structures point to each other. Each builder here owns every +//! structure it points to, boxed so the pointers stay valid when the builder +//! moves; the structure it hands out is valid for as long as the builder +//! lives. + +use ash::vk::native::*; + +use super::consts::{NUM_REF_FRAMES, REFS_PER_FRAME}; +use super::frame_header::FrameHeader; +use super::params::{FilmGrainParams, GlobalMotionParams}; +use super::refs::RefSlot; +use super::sequence::SequenceHeader; + +fn zeroed() -> T { + // SAFETY: only used for the plain-data Std structures, for which all + // zero bits are a valid value (null pointers, zero numbers and flags). + unsafe { std::mem::zeroed() } +} + +/// `StdVideoAV1SequenceHeader` and the structures it points to. +pub struct StdSequenceHeader { + header: StdVideoAV1SequenceHeader, + _color: Box, + _timing: Option>, +} + +impl StdSequenceHeader { + pub fn new(seq: &SequenceHeader) -> Self { + let cc = &seq.color_config; + let mut color: Box = Box::new(zeroed()); + color.flags.set_mono_chrome(cc.mono_chrome as u32); + color.flags.set_color_range(cc.color_range as u32); + color + .flags + .set_separate_uv_delta_q(cc.separate_uv_delta_q as u32); + color + .flags + .set_color_description_present_flag(cc.color_description_present_flag as u32); + color.BitDepth = cc.bit_depth; + color.subsampling_x = cc.subsampling_x as u8; + color.subsampling_y = cc.subsampling_y as u8; + color.color_primaries = cc.color_primaries as u32; + color.transfer_characteristics = cc.transfer_characteristics as u32; + color.matrix_coefficients = cc.matrix_coefficients as u32; + color.chroma_sample_position = cc.chroma_sample_position as u32; + + let timing = seq.timing_info.as_ref().map(|t| { + let mut timing: Box = Box::new(zeroed()); + timing + .flags + .set_equal_picture_interval(t.equal_picture_interval as u32); + timing.num_units_in_display_tick = t.num_units_in_display_tick; + timing.time_scale = t.time_scale; + timing.num_ticks_per_picture_minus_1 = t.num_ticks_per_picture_minus_1; + timing + }); + + let mut header: StdVideoAV1SequenceHeader = zeroed(); + let f = &mut header.flags; + f.set_still_picture(seq.still_picture as u32); + f.set_reduced_still_picture_header(seq.reduced_still_picture_header as u32); + f.set_use_128x128_superblock(seq.use_128x128_superblock as u32); + f.set_enable_filter_intra(seq.enable_filter_intra as u32); + f.set_enable_intra_edge_filter(seq.enable_intra_edge_filter as u32); + f.set_enable_interintra_compound(seq.enable_interintra_compound as u32); + f.set_enable_masked_compound(seq.enable_masked_compound as u32); + f.set_enable_warped_motion(seq.enable_warped_motion as u32); + f.set_enable_dual_filter(seq.enable_dual_filter as u32); + f.set_enable_order_hint(seq.enable_order_hint as u32); + f.set_enable_jnt_comp(seq.enable_jnt_comp as u32); + f.set_enable_ref_frame_mvs(seq.enable_ref_frame_mvs as u32); + f.set_frame_id_numbers_present_flag(seq.frame_id_numbers_present_flag as u32); + f.set_enable_superres(seq.enable_superres as u32); + f.set_enable_cdef(seq.enable_cdef as u32); + f.set_enable_restoration(seq.enable_restoration as u32); + f.set_film_grain_params_present(seq.film_grain_params_present as u32); + f.set_timing_info_present_flag(seq.timing_info.is_some() as u32); + f.set_initial_display_delay_present_flag(seq.initial_display_delay_present_flag as u32); + header.seq_profile = seq.seq_profile as u32; + header.frame_width_bits_minus_1 = seq.frame_width_bits_minus_1; + header.frame_height_bits_minus_1 = seq.frame_height_bits_minus_1; + header.max_frame_width_minus_1 = seq.max_frame_width_minus_1; + header.max_frame_height_minus_1 = seq.max_frame_height_minus_1; + header.delta_frame_id_length_minus_2 = seq.delta_frame_id_length_minus_2; + header.additional_frame_id_length_minus_1 = seq.additional_frame_id_length_minus_1; + header.order_hint_bits_minus_1 = seq.order_hint_bits_minus_1(); + header.seq_force_integer_mv = seq.seq_force_integer_mv; + header.seq_force_screen_content_tools = seq.seq_force_screen_content_tools; + header.pColorConfig = &*color; + header.pTimingInfo = timing.as_deref().map_or(std::ptr::null(), |t| t); + Self { + header, + _color: color, + _timing: timing, + } + } + + pub fn get(&self) -> &StdVideoAV1SequenceHeader { + &self.header + } +} + +fn std_global_motion(gm: &GlobalMotionParams) -> StdVideoAV1GlobalMotion { + StdVideoAV1GlobalMotion { + GmType: gm.gm_type, + gm_params: gm.gm_params, + } +} + +fn std_film_grain(g: &FilmGrainParams) -> StdVideoAV1FilmGrain { + let mut s: StdVideoAV1FilmGrain = zeroed(); + s.flags + .set_chroma_scaling_from_luma(g.chroma_scaling_from_luma as u32); + s.flags.set_overlap_flag(g.overlap_flag as u32); + s.flags + .set_clip_to_restricted_range(g.clip_to_restricted_range as u32); + s.flags.set_update_grain(g.update_grain as u32); + s.grain_scaling_minus_8 = g.grain_scaling_minus_8; + s.ar_coeff_lag = g.ar_coeff_lag; + s.ar_coeff_shift_minus_6 = g.ar_coeff_shift_minus_6; + s.grain_scale_shift = g.grain_scale_shift; + s.grain_seed = g.grain_seed; + s.film_grain_params_ref_idx = g.film_grain_params_ref_idx; + s.num_y_points = g.num_y_points; + s.point_y_value = g.point_y_value; + s.point_y_scaling = g.point_y_scaling; + s.num_cb_points = g.num_cb_points; + s.point_cb_value = g.point_cb_value; + s.point_cb_scaling = g.point_cb_scaling; + s.num_cr_points = g.num_cr_points; + s.point_cr_value = g.point_cr_value; + s.point_cr_scaling = g.point_cr_scaling; + // The Std header declares these int8_t although they hold 0..=255. + s.ar_coeffs_y_plus_128 = g.ar_coeffs_y_plus_128.map(|v| v as i8); + s.ar_coeffs_cb_plus_128 = g.ar_coeffs_cb_plus_128.map(|v| v as i8); + s.ar_coeffs_cr_plus_128 = g.ar_coeffs_cr_plus_128.map(|v| v as i8); + s.cb_mult = g.cb_mult; + s.cb_luma_mult = g.cb_luma_mult; + s.cb_offset = g.cb_offset; + s.cr_mult = g.cr_mult; + s.cr_luma_mult = g.cr_luma_mult; + s.cr_offset = g.cr_offset; + s +} + +/// The arrays a `StdVideoAV1TileInfo` points to. +struct TileArrays { + mi_col_starts: Box<[u16]>, + mi_row_starts: Box<[u16]>, + width_in_sbs_minus_1: Box<[u16]>, + height_in_sbs_minus_1: Box<[u16]>, +} + +/// `StdVideoDecodeAV1PictureInfo` and the structures it points to. +pub struct StdPictureInfo { + info: StdVideoDecodeAV1PictureInfo, + _tile_info: Box, + _tile_arrays: TileArrays, + _quantization: Box, + _segmentation: Box, + _loop_filter: Box, + _cdef: Box, + _loop_restoration: Box, + _global_motion: Box, + _film_grain: Box, +} + +impl StdPictureInfo { + /// Build the picture info for a frame (not a `show_existing_frame`, + /// which is not decoded). + pub fn new(h: &FrameHeader) -> Self { + let size = &h.frame_size; + let ti = &h.tile_info; + let tile_arrays = TileArrays { + // Vulkan takes TileCols / TileRows entries, without the final + // MiCols / MiRows the spec's arrays end with. + mi_col_starts: ti.mi_col_starts[..ti.tile_cols as usize].into(), + mi_row_starts: ti.mi_row_starts[..ti.tile_rows as usize].into(), + width_in_sbs_minus_1: ti.width_in_sbs_minus_1.clone().into(), + height_in_sbs_minus_1: ti.height_in_sbs_minus_1.clone().into(), + }; + let mut tile_info: Box = Box::new(zeroed()); + tile_info + .flags + .set_uniform_tile_spacing_flag(ti.uniform_tile_spacing_flag as u32); + tile_info.TileCols = ti.tile_cols; + tile_info.TileRows = ti.tile_rows; + tile_info.context_update_tile_id = ti.context_update_tile_id; + tile_info.tile_size_bytes_minus_1 = ti.tile_size_bytes_minus_1; + tile_info.pMiColStarts = tile_arrays.mi_col_starts.as_ptr(); + tile_info.pMiRowStarts = tile_arrays.mi_row_starts.as_ptr(); + tile_info.pWidthInSbsMinus1 = tile_arrays.width_in_sbs_minus_1.as_ptr(); + tile_info.pHeightInSbsMinus1 = tile_arrays.height_in_sbs_minus_1.as_ptr(); + + let q = &h.quantization; + let mut quantization: Box = Box::new(zeroed()); + quantization.flags.set_using_qmatrix(q.using_qmatrix as u32); + quantization.flags.set_diff_uv_delta(q.diff_uv_delta as u32); + quantization.base_q_idx = q.base_q_idx; + quantization.DeltaQYDc = q.delta_q_y_dc; + quantization.DeltaQUDc = q.delta_q_u_dc; + quantization.DeltaQUAc = q.delta_q_u_ac; + quantization.DeltaQVDc = q.delta_q_v_dc; + quantization.DeltaQVAc = q.delta_q_v_ac; + quantization.qm_y = q.qm_y; + quantization.qm_u = q.qm_u; + quantization.qm_v = q.qm_v; + + let segmentation = Box::new(StdVideoAV1Segmentation { + FeatureEnabled: h.segmentation.feature_enabled_masks(), + FeatureData: h.segmentation.feature_data, + }); + + let lf = &h.loop_filter; + let mut loop_filter: Box = Box::new(zeroed()); + loop_filter + .flags + .set_loop_filter_delta_enabled(lf.loop_filter_delta_enabled as u32); + loop_filter + .flags + .set_loop_filter_delta_update(lf.loop_filter_delta_update as u32); + loop_filter.loop_filter_level = lf.loop_filter_level; + loop_filter.loop_filter_sharpness = lf.loop_filter_sharpness; + loop_filter.update_ref_delta = lf.update_ref_delta; + loop_filter.loop_filter_ref_deltas = lf.loop_filter_ref_deltas; + loop_filter.update_mode_delta = lf.update_mode_delta; + loop_filter.loop_filter_mode_deltas = lf.loop_filter_mode_deltas; + + let c = &h.cdef; + let cdef = Box::new(StdVideoAV1CDEF { + cdef_damping_minus_3: c.cdef_damping_minus_3, + cdef_bits: c.cdef_bits, + cdef_y_pri_strength: c.cdef_y_pri_strength, + cdef_y_sec_strength: c.cdef_y_sec_strength, + cdef_uv_pri_strength: c.cdef_uv_pri_strength, + cdef_uv_sec_strength: c.cdef_uv_sec_strength, + }); + + let lr = &h.loop_restoration; + let loop_restoration = Box::new(StdVideoAV1LoopRestoration { + FrameRestorationType: lr.frame_restoration_type.map(|t| t as u32), + LoopRestorationSize: lr.size_log2_minus_5(), + }); + let global_motion = Box::new(std_global_motion(&h.global_motion)); + let film_grain = Box::new(std_film_grain(&h.film_grain)); + + let mut info: StdVideoDecodeAV1PictureInfo = zeroed(); + let f = &mut info.flags; + f.set_error_resilient_mode(h.error_resilient_mode as u32); + f.set_disable_cdf_update(h.disable_cdf_update as u32); + f.set_use_superres(size.use_superres as u32); + f.set_render_and_frame_size_different(size.render_and_frame_size_different as u32); + f.set_allow_screen_content_tools(h.allow_screen_content_tools as u32); + f.set_is_filter_switchable(h.is_filter_switchable as u32); + f.set_force_integer_mv(h.force_integer_mv as u32); + f.set_frame_size_override_flag(h.frame_size_override_flag as u32); + f.set_buffer_removal_time_present_flag(h.buffer_removal_time_present_flag as u32); + f.set_allow_intrabc(h.allow_intrabc as u32); + f.set_frame_refs_short_signaling(h.frame_refs_short_signaling as u32); + f.set_allow_high_precision_mv(h.allow_high_precision_mv as u32); + f.set_is_motion_mode_switchable(h.is_motion_mode_switchable as u32); + f.set_use_ref_frame_mvs(h.use_ref_frame_mvs as u32); + f.set_disable_frame_end_update_cdf(h.disable_frame_end_update_cdf as u32); + f.set_allow_warped_motion(h.allow_warped_motion as u32); + f.set_reduced_tx_set(h.reduced_tx_set as u32); + f.set_reference_select(h.reference_select as u32); + f.set_skip_mode_present(h.skip_mode_present as u32); + f.set_delta_q_present(h.delta.delta_q_present as u32); + f.set_delta_lf_present(h.delta.delta_lf_present as u32); + f.set_delta_lf_multi(h.delta.delta_lf_multi as u32); + f.set_segmentation_enabled(h.segmentation.segmentation_enabled as u32); + f.set_segmentation_update_map(h.segmentation.segmentation_update_map as u32); + f.set_segmentation_temporal_update(h.segmentation.segmentation_temporal_update as u32); + f.set_segmentation_update_data(h.segmentation.segmentation_update_data as u32); + f.set_UsesLr(lr.uses_lr as u32); + f.set_usesChromaLr(lr.uses_chroma_lr as u32); + f.set_apply_grain(h.film_grain.apply_grain as u32); + info.frame_type = h.frame_type as u32; + info.current_frame_id = h.current_frame_id; + info.OrderHint = h.order_hint; + info.primary_ref_frame = h.primary_ref_frame; + info.refresh_frame_flags = h.refresh_frame_flags; + info.interpolation_filter = h.interpolation_filter as u32; + info.TxMode = h.tx_mode as u32; + info.delta_q_res = h.delta.delta_q_res; + info.delta_lf_res = h.delta.delta_lf_res; + info.SkipModeFrame = h.skip_mode_frame; + info.coded_denom = size.coded_denom; + info.OrderHints = h.order_hints; + // Section 5.9.2 indexes expectedFrameId by i, for LAST_FRAME + i. + info.expectedFrameId[..REFS_PER_FRAME].copy_from_slice(&h.expected_frame_id); + info.pTileInfo = &*tile_info; + info.pQuantization = &*quantization; + info.pSegmentation = &*segmentation; + info.pLoopFilter = &*loop_filter; + info.pCDEF = &*cdef; + info.pLoopRestoration = &*loop_restoration; + info.pGlobalMotion = &*global_motion; + info.pFilmGrain = &*film_grain; + + Self { + info, + _tile_info: tile_info, + _tile_arrays: tile_arrays, + _quantization: quantization, + _segmentation: segmentation, + _loop_filter: loop_filter, + _cdef: cdef, + _loop_restoration: loop_restoration, + _global_motion: global_motion, + _film_grain: film_grain, + } + } + + pub fn get(&self) -> &StdVideoDecodeAV1PictureInfo { + &self.info + } +} + +/// `StdVideoDecodeAV1ReferenceInfo` for a reference slot (or, after the +/// update, for the frame just decoded). +pub fn std_reference_info(slot: &RefSlot) -> StdVideoDecodeAV1ReferenceInfo { + let mut info: StdVideoDecodeAV1ReferenceInfo = zeroed(); + info.flags + .set_disable_frame_end_update_cdf(slot.disable_frame_end_update_cdf as u32); + info.flags + .set_segmentation_enabled(slot.segmentation_enabled as u32); + info.frame_type = slot.frame_type as u8; + info.RefFrameSignBias = slot.ref_frame_sign_bias; + info.OrderHint = slot.order_hint; + info.SavedOrderHints = slot.saved_order_hints; + info +} + +/// `VkVideoDecodeAV1PictureInfoKHR::referenceNameSlotIndices`: the DPB slot +/// of each of LAST_FRAME..=ALTREF_FRAME, or -1. Intra frames use none. +pub fn reference_name_slot_indices( + header: &FrameHeader, + slots: &[RefSlot; NUM_REF_FRAMES], +) -> [i32; REFS_PER_FRAME] { + if header.frame_is_intra() { + return [-1; REFS_PER_FRAME]; + } + header.ref_frame_idx.map(|idx| { + slots[idx as usize] + .dpb_slot + .map_or(-1, |dpb_slot| dpb_slot as i32) + }) +} diff --git a/src/decoder/av1/stream_tests.rs b/src/decoder/av1/stream_tests.rs new file mode 100644 index 00000000..e9134969 --- /dev/null +++ b/src/decoder/av1/stream_tests.rs @@ -0,0 +1,621 @@ +//! Parser tests against real encoder output. +//! +//! The fixtures in `tests/data/av1` are 320x240 (`aom_mono`: 176x144) +//! streams of 12 to 15 frames in the low-overhead OBU format, from libaom +//! (through ffmpeg's `libaom-av1` and `aomenc --obu`) and SVT-AV1. Each +//! `.expected` holds what ffmpeg's `trace_headers` bitstream filter +//! printed for that stream, reduced to one `F` line per frame header and one +//! `T` line per tile group, each a list of `syntax_element=value`. The +//! script that reduced them, `tests/data/av1/extract_trace.py`, also lists +//! the encoder settings; `header_bits` is the position after the header's last +//! syntax element minus that of its first. +//! +//! | stream | what it covers | +//! |-------------------|--------------------------------------------------------| +//! | `aom_altref` | hidden alt-refs, `show_existing_frame`, skip mode | +//! | `aom_tiles` | 2x2 uniform tiles, two-byte tile sizes | +//! | `aom_tilegroups` | frame header OBUs with two tile groups each, 128x128 | +//! | | superblocks, quantizer matrices, segmentation, delta | +//! | | lf, loop restoration, decoder model and removal times | +//! | `aom_10bit` | 10-bit Main profile | +//! | `aom_grain` | film grain (libaom denoiser) | +//! | `svt_grain` | SVT-AV1 with film grain, `update_grain = 0` reuse | +//! | `aom_errres` | error resilient frames, frame ids, `ref_order_hint`, | +//! | | S-frames | +//! | `aom_superres` | superres | +//! | `aom_resize` | frame size override, `frame_size_with_refs`, render | +//! | | size | +//! | `aom_mono` | monochrome, loop restoration | +//! | `aom_annexb` | Annex B framing, which must be rejected | +//! +//! No encoder at hand produces `frame_refs_short_signaling`, non-uniform +//! tile spacing or redundant frame headers; `refs.rs` and `tile.rs` test +//! those paths on synthetic input. + +use super::frame_header::FrameHeader; +use super::obu::{ObuType, iter_obus, temporal_unit_starts}; +use super::parser::{Av1Event, Av1Frame, Av1Parser}; +use super::sequence::SequenceHeader; +use super::std_info::{ + StdPictureInfo, StdSequenceHeader, reference_name_slot_indices, std_reference_info, +}; + +macro_rules! fixture { + ($name:literal) => { + ( + $name, + include_bytes!(concat!("../../../tests/data/av1/", $name, ".obu")).as_slice(), + include_str!(concat!("../../../tests/data/av1/", $name, ".expected")), + ) + }; +} + +const FIXTURES: [(&str, &[u8], &str); 10] = [ + fixture!("aom_altref"), + fixture!("aom_tiles"), + fixture!("aom_tilegroups"), + fixture!("aom_10bit"), + fixture!("aom_grain"), + fixture!("svt_grain"), + fixture!("aom_errres"), + fixture!("aom_superres"), + fixture!("aom_resize"), + fixture!("aom_mono"), +]; + +const ANNEX_B: &[u8] = include_bytes!("../../../tests/data/av1/aom_annexb.obu"); + +fn fixture(name: &str) -> &'static [u8] { + FIXTURES.iter().find(|f| f.0 == name).unwrap().1 +} + +/// Everything parsing a whole stream produced. +struct Parsed { + sequence: SequenceHeader, + frames: Vec, + /// `(payload_offset, end)` of every tile group carrying OBU. + tile_group_obus: Vec<(usize, usize)>, + /// Frames shown, by `show_frame` or `show_existing_frame`. + shown: usize, +} + +/// Parse a stream, committing every frame, and check what holds for every +/// stream: every OBU parses and every frame is complete. +fn parse_stream(name: &str, data: &[u8]) -> Parsed { + let mut parser = Av1Parser::new(); + let mut frames = Vec::new(); + let mut tile_group_obus = Vec::new(); + let mut shown = 0; + let mut next_dpb_slot = 0u8; + for obu in iter_obus(data) { + let obu = obu.unwrap_or_else(|e| panic!("{name}: OBU framing: {e}")); + if matches!(obu.header.obu_type, ObuType::TileGroup | ObuType::Frame) { + tile_group_obus.push(( + obu.payload_offset(), + obu.payload_offset() + obu.payload.len(), + )); + } + let event = parser + .parse_obu(&obu) + .unwrap_or_else(|e| panic!("{name}: OBU at byte {}: {e}", obu.offset)); + match event { + Some(Av1Event::Frame(frame)) => { + let h = &frame.header; + if h.show_frame || h.show_existing_frame { + shown += 1; + } + if !h.show_existing_frame { + check_vulkan_conversion(&parser, h); + } + // A stand-in for the DPB slot the decoder would pick. + parser.commit_frame(Some(next_dpb_slot)).unwrap(); + next_dpb_slot = (next_dpb_slot + 1) % 16; + frames.push(*frame); + } + Some(Av1Event::MissingReferences(slots)) => { + panic!("{name}: missing references {slots:#010b}") + } + Some(Av1Event::SequenceHeader { .. }) | None => {} + } + } + Parsed { + sequence: parser + .sequence_header() + .expect("stream has a sequence header") + .clone(), + frames, + tile_group_obus, + shown, + } +} + +/// Build the Vulkan structures for a frame about to be decoded, as the +/// decoder will, and spot-check that they carry the parsed values. +fn check_vulkan_conversion(parser: &Av1Parser, h: &FrameHeader) { + let seq = StdSequenceHeader::new(parser.sequence_header().unwrap()); + let std_seq = seq.get(); + // SAFETY: the pointer targets are owned by `seq`, which is alive. + let color = unsafe { &*std_seq.pColorConfig }; + assert_eq!( + color.BitDepth, + parser.sequence_header().unwrap().color_config.bit_depth + ); + + let pic = StdPictureInfo::new(h); + let info = pic.get(); + assert_eq!(info.OrderHint, h.order_hint); + assert_eq!(info.refresh_frame_flags, h.refresh_frame_flags); + // SAFETY: as above, owned by `pic`. + let tile_info = unsafe { &*info.pTileInfo }; + let quant = unsafe { &*info.pQuantization }; + assert_eq!(quant.base_q_idx, h.quantization.base_q_idx); + assert_eq!(tile_info.TileCols, h.tile_info.tile_cols); + let cols = + unsafe { std::slice::from_raw_parts(tile_info.pMiColStarts, tile_info.TileCols as usize) }; + assert_eq!(cols, &h.tile_info.mi_col_starts[..cols.len()]); + + let indices = reference_name_slot_indices(h, &parser.refs().slots); + if !h.frame_is_intra() { + for (i, &idx) in h.ref_frame_idx.iter().enumerate() { + let slot = &parser.refs().slots[idx as usize]; + assert!(slot.valid); + assert_eq!(indices[i], slot.dpb_slot.unwrap() as i32); + assert_eq!(std_reference_info(slot).OrderHint, h.order_hints[i + 1]); + } + } else { + assert!(indices.iter().all(|&i| i == -1)); + } +} + +/// The `syntax_element=value` pairs of one line. +type Elements = Vec<(String, i64)>; + +/// The `F` (frame header) and `T` (tile group) lines of an `.expected` file. +fn parse_expected(text: &str) -> (Vec, Vec) { + let mut frames = Vec::new(); + let mut tile_groups = Vec::new(); + for line in text.lines() { + let mut words = line.split_whitespace(); + let kind = words.next().unwrap(); + let pairs: Elements = words + .map(|w| { + let (k, v) = w.split_once('=').unwrap(); + (k.to_string(), v.parse().unwrap()) + }) + .collect(); + match kind { + "F" => frames.push(pairs), + "T" => tile_groups.push(pairs), + other => panic!("unknown line kind {other}"), + } + } + (frames, tile_groups) +} + +/// The parsed value of the syntax element `trace_headers` calls `key`. +/// `return_coded` is the traced value, returned where the parser keeps a +/// derived value that cannot be compared with it. +fn value_of(f: &Av1Frame, key: &str, return_coded: i64) -> i64 { + let h = &f.header; + let (name, index) = match key.split_once('[') { + Some((name, rest)) => ( + name, + Some(rest.trim_end_matches(']').parse::().unwrap()), + ), + None => (key, None), + }; + let b = |v: bool| v as i64; + let lr = &h.loop_restoration; + match (name, index) { + ("obu", None) => match f.header_obu_type { + ObuType::FrameHeader => 3, + ObuType::Frame => 6, + ObuType::RedundantFrameHeader => 7, + other => panic!("header in {other:?}"), + }, + ("show_existing_frame", None) => b(h.show_existing_frame), + ("frame_to_show_map_idx", None) => h.frame_to_show_map_idx as i64, + ("frame_type", None) => h.frame_type as i64, + ("show_frame", None) => b(h.show_frame), + ("showable_frame", None) => b(h.showable_frame), + ("error_resilient_mode", None) => b(h.error_resilient_mode), + ("disable_cdf_update", None) => b(h.disable_cdf_update), + ("allow_screen_content_tools", None) => b(h.allow_screen_content_tools), + // Intra frames code force_integer_mv but always use 1, which is the + // value kept; only an inter frame's coded value is comparable. + ("force_integer_mv", None) if h.frame_is_intra() => return_coded, + ("force_integer_mv", None) => b(h.force_integer_mv), + ("current_frame_id", None) => h.current_frame_id as i64, + ("frame_size_override_flag", None) => b(h.frame_size_override_flag), + ("order_hint", None) => h.order_hint as i64, + ("primary_ref_frame", None) => h.primary_ref_frame as i64, + ("refresh_frame_flags", None) => h.refresh_frame_flags as i64, + ("ref_frame_idx", Some(i)) => h.ref_frame_idx[i] as i64, + ("frame_width_minus_1", None) => h.frame_size.upscaled_width as i64 - 1, + ("frame_height_minus_1", None) => h.frame_size.frame_height as i64 - 1, + ("render_width_minus_1", None) => h.frame_size.render_width as i64 - 1, + ("render_height_minus_1", None) => h.frame_size.render_height as i64 - 1, + ("use_superres", None) => b(h.frame_size.use_superres), + ("coded_denom", None) => h.frame_size.coded_denom as i64, + ("allow_intrabc", None) => b(h.allow_intrabc), + ("allow_high_precision_mv", None) => b(h.allow_high_precision_mv), + ("is_filter_switchable", None) => b(h.is_filter_switchable), + ("interpolation_filter", None) => h.interpolation_filter as i64, + ("use_ref_frame_mvs", None) => b(h.use_ref_frame_mvs), + ("disable_frame_end_update_cdf", None) => b(h.disable_frame_end_update_cdf), + ("tile_cols_log2", None) => h.tile_info.tile_cols_log2 as i64, + ("tile_rows_log2", None) => h.tile_info.tile_rows_log2 as i64, + ("context_update_tile_id", None) => h.tile_info.context_update_tile_id as i64, + ("tile_size_bytes_minus1", None) => h.tile_info.tile_size_bytes_minus_1 as i64, + ("base_q_idx", None) => h.quantization.base_q_idx as i64, + ("using_qmatrix", None) => b(h.quantization.using_qmatrix), + ("qm_y", None) => h.quantization.qm_y as i64, + ("qm_u", None) => h.quantization.qm_u as i64, + ("qm_v", None) => h.quantization.qm_v as i64, + ("segmentation_enabled", None) => b(h.segmentation.segmentation_enabled), + ("segmentation_update_map", None) => b(h.segmentation.segmentation_update_map), + ("segmentation_temporal_update", None) => b(h.segmentation.segmentation_temporal_update), + ("segmentation_update_data", None) => b(h.segmentation.segmentation_update_data), + ("delta_q_present", None) => b(h.delta.delta_q_present), + ("delta_q_res", None) => h.delta.delta_q_res as i64, + ("delta_lf_present", None) => b(h.delta.delta_lf_present), + ("delta_lf_res", None) => h.delta.delta_lf_res as i64, + ("delta_lf_multi", None) => b(h.delta.delta_lf_multi), + ("loop_filter_level", Some(i)) => h.loop_filter.loop_filter_level[i] as i64, + ("loop_filter_sharpness", None) => h.loop_filter.loop_filter_sharpness as i64, + ("loop_filter_delta_enabled", None) => b(h.loop_filter.loop_filter_delta_enabled), + ("loop_filter_delta_update", None) => b(h.loop_filter.loop_filter_delta_update), + ("cdef_damping_minus_3", None) => h.cdef.cdef_damping_minus_3 as i64, + ("cdef_bits", None) => h.cdef.cdef_bits as i64, + ("cdef_y_pri_strength", Some(i)) => h.cdef.cdef_y_pri_strength[i] as i64, + ("cdef_y_sec_strength", Some(i)) => h.cdef.cdef_y_sec_strength[i] as i64, + // trace_headers prints the coded lr_type, before the remapping. + ("lr_type", Some(i)) => [0, 2, 3, 1][lr.frame_restoration_type[i] as usize], + // cbs_av1 reads lr_unit_shift as one increment and prints the sum. + ("lr_unit_shift", None) => lr.lr_unit_shift as i64, + ("lr_uv_shift", None) => lr.lr_uv_shift as i64, + ("tx_mode", None) => h.tx_mode as i64, + ("reference_select", None) => b(h.reference_select), + ("skip_mode_present", None) => b(h.skip_mode_present), + ("allow_warped_motion", None) => b(h.allow_warped_motion), + ("reduced_tx_set", None) => b(h.reduced_tx_set), + ("apply_grain", None) => b(h.film_grain.apply_grain), + ("grain_seed", None) => h.film_grain.grain_seed as i64, + ("update_grain", None) => b(h.film_grain.update_grain), + ("film_grain_params_ref_idx", None) => h.film_grain.film_grain_params_ref_idx as i64, + ("num_y_points", None) => h.film_grain.num_y_points as i64, + ("num_cb_points", None) => h.film_grain.num_cb_points as i64, + ("num_cr_points", None) => h.film_grain.num_cr_points as i64, + ("header_bits", None) => h.header_bits as i64, + _ => panic!("no mapping for trace element {key}"), + } +} + +/// Every syntax element `trace_headers` printed matches the parse, frame by +/// frame and tile group by tile group, including each header's bit length. +#[test] +fn headers_match_trace_headers() { + for (name, data, expected) in FIXTURES { + let parsed = parse_stream(name, data); + let (frames, tile_groups) = parse_expected(expected); + assert_eq!(parsed.frames.len(), frames.len(), "{name}: frame count"); + for (n, (frame, pairs)) in parsed.frames.iter().zip(&frames).enumerate() { + for (key, want) in pairs { + let got = value_of(frame, key, *want); + assert_eq!(got, *want, "{name}: frame {n}: {key}"); + } + // skip_mode_present is coded exactly when skip mode is allowed, + // so its presence checks the SkipModeFrame derivation. + let skip_coded = pairs.iter().any(|(k, _)| k == "skip_mode_present"); + let h = &frame.header; + if !h.show_existing_frame { + assert_eq!( + h.skip_mode_frame[0] != 0, + skip_coded, + "{name}: frame {n}: skipModeAllowed" + ); + } + } + let parsed_groups: Vec<_> = parsed.frames.iter().flat_map(|f| &f.tile_groups).collect(); + assert_eq!( + parsed_groups.len(), + tile_groups.len(), + "{name}: tile groups" + ); + for (n, (tg, pairs)) in parsed_groups.iter().zip(&tile_groups).enumerate() { + for (key, want) in pairs { + let got = match key.as_str() { + "obu" => continue, + "tile_start_and_end_present_flag" => tg.tile_start_and_end_present_flag as i64, + "tg_start" => tg.tg_start as i64, + "tg_end" => tg.tg_end as i64, + other => panic!("no mapping for {other}"), + }; + assert_eq!(got, *want, "{name}: tile group {n}: {key}"); + } + } + } +} + +/// Tiles cover every tile group exactly: each tile starts right after the +/// previous one and its size field, and the last ends at the end of the OBU. +/// The frame header offset points at the OBU that carried the header. +#[test] +fn tiles_and_header_offsets_cover_the_obus() { + for (name, data, _) in FIXTURES { + let parsed = parse_stream(name, data); + let mut obus = parsed.tile_group_obus.iter(); + for (n, frame) in parsed.frames.iter().enumerate() { + let h = &frame.header; + let first = data[frame.frame_header_offset]; + let obu_type = (first >> 3) & 0x0f; + assert!( + matches!(obu_type, 3 | 6), + "{name}: frame {n}: header OBU type" + ); + assert_eq!( + frame.frame_header_bytes, + h.header_bits.div_ceil(8) + + (frame.header_obu_type == ObuType::FrameHeader && h.header_bits % 8 == 0) + as usize, + "{name}: frame {n}: header bytes vs bits" + ); + if h.show_existing_frame { + assert!(frame.tile_groups.is_empty()); + continue; + } + assert_eq!( + frame.tiles().count(), + h.tile_info.num_tiles(), + "{name}: frame {n}" + ); + let size_bytes = h.tile_info.tile_size_bytes() as usize; + for tg in &frame.tile_groups { + let &(_, obu_end) = obus.next().expect("a tile group OBU per tile group"); + // Every tile but the last is preceded by its size field. + for (k, pair) in tg.tiles.windows(2).enumerate() { + let field = if k + 2 < tg.tiles.len() { + size_bytes + } else { + 0 + }; + assert_eq!(pair[0].offset + pair[0].size + field, pair[1].offset); + } + let last = tg.tiles.last().unwrap(); + assert_eq!( + last.offset + last.size, + obu_end, + "{name}: frame {n}: last tile" + ); + assert!(tg.tiles.iter().all(|t| t.size > 0)); + } + } + assert!(obus.next().is_none(), "{name}: tile group OBUs left over"); + } +} + +/// Each temporal unit shows exactly one frame, and the temporal unit +/// boundaries split the stream into units that parse on their own. +#[test] +fn temporal_units_show_one_frame_each() { + for (name, data, _) in FIXTURES { + let parsed = parse_stream(name, data); + let starts = temporal_unit_starts(data).unwrap(); + assert_eq!( + starts[0], 0, + "{name}: stream starts with a temporal delimiter" + ); + assert_eq!( + starts.len(), + parsed.shown, + "{name}: one shown frame per temporal unit" + ); + + // Feeding the units one at a time gives the same frames. + let mut parser = Av1Parser::new(); + let mut count = 0; + let mut bounds = starts.clone(); + bounds.push(data.len()); + for w in bounds.windows(2) { + for obu in iter_obus(&data[w[0]..w[1]]) { + if let Some(Av1Event::Frame(_)) = parser.parse_obu(&obu.unwrap()).unwrap() { + parser.commit_frame(None).unwrap(); + count += 1; + } + } + } + assert_eq!(count, parsed.frames.len(), "{name}: frames per unit"); + + // A byte stream cut mid-OBU still yields the complete units. + let cut = &data[..data.len() - 3]; + assert_eq!(temporal_unit_starts(cut).unwrap(), starts, "{name}"); + } +} + +/// The features each fixture exists for are really in it. +#[test] +fn fixtures_cover_their_features() { + let altref = parse_stream("aom_altref", fixture("aom_altref")); + assert!( + altref + .frames + .iter() + .any(|f| !f.header.show_frame && !f.header.show_existing_frame) + ); + assert!(altref.frames.iter().any(|f| f.header.show_existing_frame)); + assert_eq!(altref.shown, 15); + + let tiles = parse_stream("aom_tiles", fixture("aom_tiles")); + assert!(tiles.frames.iter().all(|f| f.header.show_existing_frame + || (f.header.tile_info.tile_cols, f.header.tile_info.tile_rows) == (2, 2))); + + let groups = parse_stream("aom_tilegroups", fixture("aom_tilegroups")); + assert!(groups.sequence.use_128x128_superblock); + assert!(groups.sequence.decoder_model_info.is_some()); + assert!(groups.frames.iter().all(|f| f.tile_groups.len() == 2)); + assert!( + groups + .frames + .iter() + .any(|f| f.header.segmentation.segmentation_enabled) + ); + assert!( + groups + .frames + .iter() + .any(|f| f.header.quantization.using_qmatrix) + ); + assert!( + groups + .frames + .iter() + .any(|f| f.header.loop_restoration.uses_lr) + ); + + let ten = parse_stream("aom_10bit", fixture("aom_10bit")); + assert_eq!(ten.sequence.seq_profile, 0); + assert_eq!(ten.sequence.color_config.bit_depth, 10); + + for name in ["aom_grain", "svt_grain"] { + let grain = parse_stream(name, fixture(name)); + assert!(grain.sequence.film_grain_params_present); + assert!(grain.frames.iter().any(|f| f.header.film_grain.apply_grain)); + } + let errres = parse_stream("aom_errres", fixture("aom_errres")); + assert!(errres.sequence.frame_id_numbers_present_flag); + assert!( + errres + .frames + .iter() + .any(|f| f.header.frame_type == super::frame_header::FrameType::Switch) + ); + assert!( + errres + .frames + .iter() + .filter(|f| !f.header.frame_is_intra()) + .all(|f| f.header.error_resilient_mode) + ); + + let superres = parse_stream("aom_superres", fixture("aom_superres")); + assert!( + superres + .frames + .iter() + .any(|f| f.header.frame_size.use_superres + && f.header.frame_size.frame_width < f.header.frame_size.upscaled_width) + ); + + let resize = parse_stream("aom_resize", fixture("aom_resize")); + assert!( + resize + .frames + .iter() + .any(|f| f.header.frame_size.frame_width != 320) + ); + assert!( + resize + .frames + .iter() + .all(|f| f.header.frame_size.render_width == 320) + ); + assert!( + resize + .frames + .iter() + .any(|f| f.header.frame_size.found_ref.is_some()) + ); + + let mono = parse_stream("aom_mono", fixture("aom_mono")); + assert!(mono.sequence.color_config.mono_chrome); +} + +/// Annex B input is recognised and rejected, not misparsed. +#[test] +fn rejects_annex_b() { + let err = iter_obus(ANNEX_B).next().unwrap().unwrap_err(); + assert!(err.to_string().contains("Annex B"), "{err}"); + assert!(temporal_unit_starts(ANNEX_B).is_err()); +} + +/// Starting after the key frame, or losing a frame, reports missing +/// references instead of failing, and the next key frame recovers. +#[test] +fn missing_references_wait_for_a_key_frame() { + let data = fixture("aom_altref"); + let starts = temporal_unit_starts(data).unwrap(); + + // Skip the first temporal unit's frames, keep its sequence header. + let mut parser = Av1Parser::new(); + let mut missing = 0; + let mut frames = 0; + for obu in iter_obus(data) { + let obu = obu.unwrap(); + if obu.offset < starts[1] && obu.header.obu_type != ObuType::SequenceHeader { + continue; + } + match parser.parse_obu(&obu).unwrap() { + Some(Av1Event::MissingReferences(slots)) => { + assert_ne!(slots, 0); + missing += 1; + } + Some(Av1Event::Frame(_)) => { + parser.commit_frame(None).unwrap(); + frames += 1; + } + _ => {} + } + } + assert!(missing > 0, "frames after the lost key frame are flagged"); + assert_eq!( + frames, 0, + "and none of them decode (there is one key frame)" + ); + + // Discarding a frame leaves the references it would have refreshed + // empty; the stream continues once the slots are reset by a key frame. + let mut parser = Av1Parser::new(); + let mut seen = 0; + let mut flagged = false; + for obu in iter_obus(data) { + match parser.parse_obu(&obu.unwrap()).unwrap() { + Some(Av1Event::Frame(_)) => { + seen += 1; + if seen == 2 { + parser.discard_frame(); + } else { + parser.commit_frame(None).unwrap(); + } + } + Some(Av1Event::MissingReferences(_)) => flagged = true, + _ => {} + } + } + assert!(flagged, "a frame needing the discarded one is flagged"); +} + +/// Corrupt input never panics: flip bits throughout every stream and parse +/// everything, ignoring the errors. +#[test] +fn corrupt_streams_do_not_panic() { + for (_, data, _) in FIXTURES { + for step in [7usize, 13, 29, 61] { + let mut corrupt = data.to_vec(); + let mut i = step; + while i < corrupt.len() { + corrupt[i] ^= 1 << (i % 8); + i += step * 17; + } + let mut parser = Av1Parser::new(); + for obu in iter_obus(&corrupt) { + let Ok(obu) = obu else { break }; + if let Ok(Some(Av1Event::Frame(_))) = parser.parse_obu(&obu) { + parser.commit_frame(None).unwrap(); + } + } + for len in (0..corrupt.len()).step_by(step * 101) { + let _ = temporal_unit_starts(&corrupt[..len]); + } + } + } +} diff --git a/src/decoder/av1/tile.rs b/src/decoder/av1/tile.rs new file mode 100644 index 00000000..ede4d87a --- /dev/null +++ b/src/decoder/av1/tile.rs @@ -0,0 +1,335 @@ +//! Tile layout and tile group OBUs (AV1 specification sections 5.9.15 and +//! 5.11.1). + +use super::bits::BitReader; +use super::consts::{MAX_TILE_AREA, MAX_TILE_COLS, MAX_TILE_ROWS, MAX_TILE_WIDTH}; +use crate::error::{PixelForgeError, Result}; + +/// `tile_info()`, with the derived tile grid. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct TileInfo { + pub uniform_tile_spacing_flag: bool, + pub tile_cols_log2: u8, + pub tile_rows_log2: u8, + pub tile_cols: u8, + pub tile_rows: u8, + /// `MiColStarts`, `tile_cols + 1` entries (the last is `MiCols`). + pub mi_col_starts: Vec, + /// `MiRowStarts`, `tile_rows + 1` entries (the last is `MiRows`). + pub mi_row_starts: Vec, + /// `width_in_sbs_minus_1` per tile column. Coded for non-uniform + /// spacing; derived from the grid for uniform spacing, which is what + /// Vulkan expects either way. + pub width_in_sbs_minus_1: Vec, + /// `height_in_sbs_minus_1` per tile row, like `width_in_sbs_minus_1`. + pub height_in_sbs_minus_1: Vec, + pub context_update_tile_id: u16, + /// `TileSizeBytes - 1`; 3 (four-byte sizes) when not coded, which only + /// happens with a single tile, where no size is ever read. + pub tile_size_bytes_minus_1: u8, +} + +impl TileInfo { + /// `NumTiles`. + pub fn num_tiles(&self) -> usize { + self.tile_cols as usize * self.tile_rows as usize + } + + /// `TileSizeBytes`. + pub fn tile_size_bytes(&self) -> u32 { + self.tile_size_bytes_minus_1 as u32 + 1 + } +} + +/// `tile_log2(blkSize, target)`. +fn tile_log2(blk_size: u32, target: u32) -> u32 { + let mut k = 0; + while (blk_size << k) < target { + k += 1; + } + k +} + +/// Superblock sizes per tile from the superblock-unit starts. +fn sizes_minus_1(starts_sb: &[u32]) -> Vec { + starts_sb + .windows(2) + .map(|w| (w[1] - w[0] - 1) as u16) + .collect() +} + +/// Parse `tile_info()` for a frame of `mi_cols` by `mi_rows` mode-info units. +pub(crate) fn parse_tile_info( + r: &mut BitReader, + use_128x128_superblock: bool, + mi_cols: u32, + mi_rows: u32, +) -> Result { + if mi_cols == 0 || mi_rows == 0 { + return Err(PixelForgeError::InvalidInput( + "AV1: frame has zero size".to_string(), + )); + } + let (sb_cols, sb_rows, sb_shift) = if use_128x128_superblock { + ((mi_cols + 31) >> 5, (mi_rows + 31) >> 5, 5) + } else { + ((mi_cols + 15) >> 4, (mi_rows + 15) >> 4, 4) + }; + let sb_size = sb_shift + 2; + let max_tile_width_sb = MAX_TILE_WIDTH >> sb_size; + let mut max_tile_area_sb = MAX_TILE_AREA >> (2 * sb_size); + let min_log2_tile_cols = tile_log2(max_tile_width_sb, sb_cols); + let max_log2_tile_cols = tile_log2(1, sb_cols.min(MAX_TILE_COLS)); + let max_log2_tile_rows = tile_log2(1, sb_rows.min(MAX_TILE_ROWS)); + let min_log2_tiles = min_log2_tile_cols.max(tile_log2(max_tile_area_sb, sb_rows * sb_cols)); + + let mut info = TileInfo { + uniform_tile_spacing_flag: r.flag()?, + tile_size_bytes_minus_1: 3, + ..Default::default() + }; + // Tile starts in superblocks, ending with the frame size in superblocks. + let mut col_starts_sb = Vec::new(); + let mut row_starts_sb = Vec::new(); + let (tile_cols_log2, tile_rows_log2); + if info.uniform_tile_spacing_flag { + let mut cols_log2 = min_log2_tile_cols; + while cols_log2 < max_log2_tile_cols { + if !r.flag()? { + break; + } + cols_log2 += 1; + } + let tile_width_sb = (sb_cols + (1 << cols_log2) - 1) >> cols_log2; + col_starts_sb.extend((0..sb_cols).step_by(tile_width_sb as usize)); + + let min_log2_tile_rows = min_log2_tiles.saturating_sub(cols_log2); + let mut rows_log2 = min_log2_tile_rows; + while rows_log2 < max_log2_tile_rows { + if !r.flag()? { + break; + } + rows_log2 += 1; + } + let tile_height_sb = (sb_rows + (1 << rows_log2) - 1) >> rows_log2; + row_starts_sb.extend((0..sb_rows).step_by(tile_height_sb as usize)); + tile_cols_log2 = cols_log2; + tile_rows_log2 = rows_log2; + } else { + let mut widest_tile_sb = 0; + let mut start_sb = 0; + while start_sb < sb_cols { + if col_starts_sb.len() as u32 >= MAX_TILE_COLS { + return Err(PixelForgeError::InvalidInput( + "AV1: more than 64 tile columns".to_string(), + )); + } + col_starts_sb.push(start_sb); + let max_width = (sb_cols - start_sb).min(max_tile_width_sb); + let size_sb = r.ns(max_width)? + 1; + widest_tile_sb = widest_tile_sb.max(size_sb); + start_sb += size_sb; + } + tile_cols_log2 = tile_log2(1, col_starts_sb.len() as u32); + + if min_log2_tiles > 0 { + max_tile_area_sb = (sb_rows * sb_cols) >> (min_log2_tiles + 1); + } else { + max_tile_area_sb = sb_rows * sb_cols; + } + let max_tile_height_sb = (max_tile_area_sb / widest_tile_sb).max(1); + let mut start_sb = 0; + while start_sb < sb_rows { + if row_starts_sb.len() as u32 >= MAX_TILE_ROWS { + return Err(PixelForgeError::InvalidInput( + "AV1: more than 64 tile rows".to_string(), + )); + } + row_starts_sb.push(start_sb); + let max_height = (sb_rows - start_sb).min(max_tile_height_sb); + start_sb += r.ns(max_height)? + 1; + } + tile_rows_log2 = tile_log2(1, row_starts_sb.len() as u32); + } + + if col_starts_sb.len() as u32 > MAX_TILE_COLS || row_starts_sb.len() as u32 > MAX_TILE_ROWS { + return Err(PixelForgeError::InvalidInput( + "AV1: tile grid exceeds 64 columns or rows".to_string(), + )); + } + info.tile_cols = col_starts_sb.len() as u8; + info.tile_rows = row_starts_sb.len() as u8; + info.tile_cols_log2 = tile_cols_log2 as u8; + info.tile_rows_log2 = tile_rows_log2 as u8; + info.mi_col_starts = col_starts_sb + .iter() + .map(|&s| (s << sb_shift) as u16) + .chain([mi_cols as u16]) + .collect(); + info.mi_row_starts = row_starts_sb + .iter() + .map(|&s| (s << sb_shift) as u16) + .chain([mi_rows as u16]) + .collect(); + col_starts_sb.push(sb_cols); + row_starts_sb.push(sb_rows); + info.width_in_sbs_minus_1 = sizes_minus_1(&col_starts_sb); + info.height_in_sbs_minus_1 = sizes_minus_1(&row_starts_sb); + + if tile_cols_log2 > 0 || tile_rows_log2 > 0 { + info.context_update_tile_id = r.f(tile_cols_log2 + tile_rows_log2)? as u16; + if info.context_update_tile_id as usize >= info.num_tiles() { + return Err(PixelForgeError::InvalidInput( + "AV1: context_update_tile_id out of range".to_string(), + )); + } + info.tile_size_bytes_minus_1 = r.f(2)? as u8; + } + Ok(info) +} + +/// Where one tile's data sits. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct TileLocation { + /// Byte offset of the first byte of tile data (after its + /// `tile_size_minus_1` field, if any), relative to the start of the + /// buffer the OBU iterator was given. + pub offset: usize, + /// Size of the tile data in bytes. + pub size: usize, +} + +/// One parsed tile group: the range of tiles it covers and where each one is. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TileGroup { + pub tile_start_and_end_present_flag: bool, + pub tg_start: u16, + pub tg_end: u16, + pub tiles: Vec, +} + +/// Parse `tile_group_obu(sz)` from `data`, which starts where the tile group +/// does: at the payload of a tile group OBU, or after the byte-aligned frame +/// header in a frame OBU. `base_offset` is the offset of `data[0]` in the +/// buffer tile locations are reported against. +pub(crate) fn parse_tile_group( + data: &[u8], + base_offset: usize, + info: &TileInfo, +) -> Result { + let num_tiles = info.num_tiles(); + if num_tiles == 0 { + return Err(PixelForgeError::InvalidInput( + "AV1: tile group for a frame with no tiles".to_string(), + )); + } + let mut r = BitReader::new(data); + let mut tg = TileGroup { + tile_start_and_end_present_flag: false, + tg_start: 0, + tg_end: (num_tiles - 1) as u16, + tiles: Vec::new(), + }; + if num_tiles > 1 { + tg.tile_start_and_end_present_flag = r.flag()?; + } + if tg.tile_start_and_end_present_flag { + let tile_bits = info.tile_cols_log2 as u32 + info.tile_rows_log2 as u32; + tg.tg_start = r.f(tile_bits)? as u16; + tg.tg_end = r.f(tile_bits)? as u16; + if tg.tg_start > tg.tg_end || tg.tg_end as usize >= num_tiles { + return Err(PixelForgeError::InvalidInput(format!( + "AV1: tile group covers tiles {}..={} of {num_tiles}", + tg.tg_start, tg.tg_end + ))); + } + } + r.byte_alignment()?; + let mut pos = r.byte_position(); + let size_bytes = info.tile_size_bytes() as usize; + for tile_num in tg.tg_start..=tg.tg_end { + let size = if tile_num == tg.tg_end { + data.len() - pos + } else { + if data.len() - pos < size_bytes { + return Err(PixelForgeError::InvalidInput( + "AV1: tile group ends inside a tile size".to_string(), + )); + } + let mut sr = BitReader::new(&data[pos..pos + size_bytes]); + let size = sr.le(size_bytes as u32)? as usize + 1; + pos += size_bytes; + if size > data.len() - pos { + return Err(PixelForgeError::InvalidInput(format!( + "AV1: tile {tile_num} of {size} bytes overruns its tile group" + ))); + } + size + }; + if size == 0 { + return Err(PixelForgeError::InvalidInput(format!( + "AV1: tile {tile_num} is empty" + ))); + } + tg.tiles.push(TileLocation { + offset: base_offset + pos, + size, + }); + pos += size; + } + Ok(tg) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uniform_grid_for_320x240() { + // 320x240 is 80x60 mode-info units: 5x4 superblocks of 64x64. + // One increment of each log2: 2x2 tiles of 3 and 2 superblocks. + // Then context_update_tile_id (2 bits) and tile_size_bytes_minus_1. + // Bits: 1 1 0 | 1 0 | 11 | 01, then padding. + let bits = [0b1101_0110, 0b1000_0000]; + let mut r = BitReader::new(&bits); + let info = parse_tile_info(&mut r, false, 80, 60).unwrap(); + assert_eq!((info.tile_cols, info.tile_rows), (2, 2)); + assert_eq!(info.mi_col_starts, vec![0, 48, 80]); + assert_eq!(info.mi_row_starts, vec![0, 32, 60]); + assert_eq!(info.width_in_sbs_minus_1, vec![2, 1]); + assert_eq!(info.height_in_sbs_minus_1, vec![1, 1]); + assert_eq!(info.context_update_tile_id, 3); + assert_eq!(info.tile_size_bytes(), 2); + } + + #[test] + fn tile_group_sizes_and_offsets() { + let info = TileInfo { + tile_cols: 2, + tile_rows: 1, + tile_cols_log2: 1, + tile_size_bytes_minus_1: 1, + ..Default::default() + }; + // No start/end, aligned; tile 0 is 3 bytes (size field 2), tile 1 + // takes the remaining 2 bytes. + let data = [0x00, 0x02, 0x00, 1, 2, 3, 4, 5]; + let tg = parse_tile_group(&data, 100, &info).unwrap(); + assert_eq!( + tg.tiles, + vec![ + TileLocation { + offset: 103, + size: 3 + }, + TileLocation { + offset: 106, + size: 2 + } + ] + ); + // A tile size that overruns the OBU is an error, not a panic. + let bad = [0x00, 0x20, 0x00, 1]; + assert!(parse_tile_group(&bad, 0, &info).is_err()); + } +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index f1df6edd..c8c6f99e 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -40,6 +40,9 @@ /// Annex B start-code framing, shared by H.264 and H.265. pub(crate) mod annexb; +/// AV1 OBU parsing and reference state (host side; not wired up yet). +#[allow(dead_code, unused_imports)] +pub(crate) mod av1; pub(crate) mod bitreader; /// The Vulkan video session and the per-decoder state every codec shares. pub(crate) mod common; diff --git a/tests/data/av1/aom_10bit.expected b/tests/data/av1/aom_10bit.expected new file mode 100644 index 00000000..c2079d2c --- /dev/null +++ b/tests/data/av1/aom_10bit.expected @@ -0,0 +1,35 @@ +F obu=6 show_existing_frame=0 frame_type=0 show_frame=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=0 allow_intrabc=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=99 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=6 loop_filter_level[1]=6 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=11 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 tx_mode=2 reduced_tx_set=0 header_bits=97 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=13 primary_ref_frame=7 refresh_frame_flags=2 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=101 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=6 loop_filter_level[1]=6 loop_filter_level[2]=8 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=146 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=6 primary_ref_frame=7 refresh_frame_flags=4 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=1 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=105 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=6 loop_filter_level[1]=6 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=147 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=3 primary_ref_frame=4 refresh_frame_flags=8 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=0 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=111 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=2 loop_filter_level[1]=2 loop_filter_level[2]=8 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=135 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=1 primary_ref_frame=7 refresh_frame_flags=16 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=127 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=10 loop_filter_level[1]=10 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=131 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=2 primary_ref_frame=0 refresh_frame_flags=32 ref_frame_idx[0]=4 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=6 loop_filter_level[1]=6 loop_filter_level[2]=4 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=143 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=3 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=4 primary_ref_frame=1 refresh_frame_flags=64 ref_frame_idx[0]=3 ref_frame_idx[1]=5 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=0 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=125 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=6 loop_filter_level[1]=6 loop_filter_level[2]=8 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=131 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=5 primary_ref_frame=0 refresh_frame_flags=128 ref_frame_idx[0]=6 ref_frame_idx[1]=3 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=4 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=6 loop_filter_level[1]=6 loop_filter_level[2]=4 loop_filter_level[3]=18 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=143 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=2 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=9 primary_ref_frame=4 refresh_frame_flags=16 ref_frame_idx[0]=2 ref_frame_idx[1]=7 ref_frame_idx[2]=6 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=5 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=120 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=6 loop_filter_level[1]=6 loop_filter_level[2]=12 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=135 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=7 primary_ref_frame=1 refresh_frame_flags=32 ref_frame_idx[0]=2 ref_frame_idx[1]=7 ref_frame_idx[2]=6 ref_frame_idx[3]=0 ref_frame_idx[4]=4 ref_frame_idx[5]=3 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=126 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=10 loop_filter_level[1]=10 loop_filter_level[2]=8 loop_filter_level[3]=18 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=131 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=8 primary_ref_frame=0 refresh_frame_flags=8 ref_frame_idx[0]=5 ref_frame_idx[1]=2 ref_frame_idx[2]=7 ref_frame_idx[3]=0 ref_frame_idx[4]=4 ref_frame_idx[5]=6 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=6 loop_filter_level[1]=6 loop_filter_level[2]=8 loop_filter_level[3]=6 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=11 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=143 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=4 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=11 primary_ref_frame=0 refresh_frame_flags=64 ref_frame_idx[0]=4 ref_frame_idx[1]=3 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=7 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=120 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=10 loop_filter_level[1]=10 loop_filter_level[2]=4 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=135 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=10 primary_ref_frame=1 refresh_frame_flags=128 ref_frame_idx[0]=4 ref_frame_idx[1]=3 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=6 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=10 loop_filter_level[1]=10 loop_filter_level[2]=12 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=143 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=6 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=12 primary_ref_frame=1 refresh_frame_flags=4 ref_frame_idx[0]=6 ref_frame_idx[1]=7 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=5 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=127 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=6 loop_filter_level[1]=6 loop_filter_level[2]=8 loop_filter_level[3]=18 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=131 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=1 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=14 primary_ref_frame=6 refresh_frame_flags=32 ref_frame_idx[0]=2 ref_frame_idx[1]=6 ref_frame_idx[2]=7 ref_frame_idx[3]=1 ref_frame_idx[4]=4 ref_frame_idx[5]=3 ref_frame_idx[6]=0 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=10 loop_filter_level[1]=10 loop_filter_level[2]=12 loop_filter_level[3]=18 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=142 +T obu=6 diff --git a/tests/data/av1/aom_10bit.obu b/tests/data/av1/aom_10bit.obu new file mode 100644 index 0000000000000000000000000000000000000000..872c259996b0795ddb417ccb2eb21adbe5363507 GIT binary patch literal 14648 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 literal 0 HcmV?d00001 diff --git a/tests/data/av1/aom_errres.expected b/tests/data/av1/aom_errres.expected new file mode 100644 index 00000000..5c8f6b6c --- /dev/null +++ b/tests/data/av1/aom_errres.expected @@ -0,0 +1,42 @@ +F obu=6 show_existing_frame=0 frame_type=0 show_frame=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=55 frame_size_override_flag=0 order_hint=0 allow_intrabc=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=33 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=2 loop_filter_level[3]=4 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reduced_tx_set=0 header_bits=106 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=3 show_frame=0 showable_frame=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=56 order_hint=14 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 frame_width_minus_1=319 frame_height_minus_1=239 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=56 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=6 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=1 cdef_y_pri_strength[0]=11 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 reduced_tx_set=0 header_bits=323 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=57 frame_size_override_flag=0 order_hint=7 refresh_frame_flags=2 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=74 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=10 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 reduced_tx_set=0 header_bits=313 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=58 frame_size_override_flag=0 order_hint=3 refresh_frame_flags=4 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=1 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=83 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=2 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 reduced_tx_set=0 header_bits=313 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=59 frame_size_override_flag=0 order_hint=1 refresh_frame_flags=8 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=1 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=91 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=10 loop_filter_level[3]=12 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 reduced_tx_set=0 header_bits=312 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=60 frame_size_override_flag=0 order_hint=2 refresh_frame_flags=16 ref_frame_idx[0]=3 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=1 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=91 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=2 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=301 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=61 frame_size_override_flag=0 order_hint=3 refresh_frame_flags=0 ref_frame_idx[0]=4 ref_frame_idx[1]=3 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=1 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=91 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=6 loop_filter_level[3]=12 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 reduced_tx_set=0 header_bits=312 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=62 frame_size_override_flag=0 order_hint=5 refresh_frame_flags=32 ref_frame_idx[0]=2 ref_frame_idx[1]=4 ref_frame_idx[2]=3 ref_frame_idx[3]=0 ref_frame_idx[4]=1 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=87 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=6 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=302 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=63 frame_size_override_flag=0 order_hint=4 refresh_frame_flags=64 ref_frame_idx[0]=2 ref_frame_idx[1]=4 ref_frame_idx[2]=3 ref_frame_idx[3]=0 ref_frame_idx[4]=5 ref_frame_idx[5]=1 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=92 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=2 loop_filter_level[3]=12 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=301 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=64 frame_size_override_flag=0 order_hint=5 refresh_frame_flags=0 ref_frame_idx[0]=6 ref_frame_idx[1]=2 ref_frame_idx[2]=4 ref_frame_idx[3]=3 ref_frame_idx[4]=5 ref_frame_idx[5]=1 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=93 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=6 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 reduced_tx_set=0 header_bits=300 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=65 frame_size_override_flag=0 order_hint=6 refresh_frame_flags=128 ref_frame_idx[0]=5 ref_frame_idx[1]=6 ref_frame_idx[2]=2 ref_frame_idx[3]=4 ref_frame_idx[4]=1 ref_frame_idx[5]=3 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=93 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=9 loop_filter_level[1]=9 loop_filter_level[2]=10 loop_filter_level[3]=12 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=299 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=66 frame_size_override_flag=0 order_hint=7 refresh_frame_flags=0 ref_frame_idx[0]=7 ref_frame_idx[1]=5 ref_frame_idx[2]=6 ref_frame_idx[3]=2 ref_frame_idx[4]=1 ref_frame_idx[5]=4 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=94 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=10 loop_filter_level[3]=12 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 reduced_tx_set=0 header_bits=312 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=67 frame_size_override_flag=0 order_hint=10 refresh_frame_flags=8 ref_frame_idx[0]=1 ref_frame_idx[1]=7 ref_frame_idx[2]=5 ref_frame_idx[3]=6 ref_frame_idx[4]=2 ref_frame_idx[5]=4 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=84 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=6 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=302 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=68 frame_size_override_flag=0 order_hint=8 refresh_frame_flags=16 ref_frame_idx[0]=1 ref_frame_idx[1]=7 ref_frame_idx[2]=5 ref_frame_idx[3]=6 ref_frame_idx[4]=3 ref_frame_idx[5]=2 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=95 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=6 loop_filter_level[3]=12 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=301 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=69 frame_size_override_flag=0 order_hint=9 refresh_frame_flags=4 ref_frame_idx[0]=4 ref_frame_idx[1]=1 ref_frame_idx[2]=7 ref_frame_idx[3]=5 ref_frame_idx[4]=3 ref_frame_idx[5]=6 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=96 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=10 loop_filter_level[3]=12 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=301 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=70 frame_size_override_flag=0 order_hint=10 refresh_frame_flags=0 ref_frame_idx[0]=2 ref_frame_idx[1]=4 ref_frame_idx[2]=1 ref_frame_idx[3]=7 ref_frame_idx[4]=3 ref_frame_idx[5]=5 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=97 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=9 loop_filter_level[1]=9 loop_filter_level[2]=10 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 reduced_tx_set=0 header_bits=300 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=71 frame_size_override_flag=0 order_hint=12 refresh_frame_flags=64 ref_frame_idx[0]=3 ref_frame_idx[1]=2 ref_frame_idx[2]=4 ref_frame_idx[3]=1 ref_frame_idx[4]=7 ref_frame_idx[5]=5 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=90 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=10 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=312 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=72 frame_size_override_flag=0 order_hint=11 refresh_frame_flags=32 ref_frame_idx[0]=3 ref_frame_idx[1]=2 ref_frame_idx[2]=4 ref_frame_idx[3]=1 ref_frame_idx[4]=6 ref_frame_idx[5]=7 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=98 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=6 loop_filter_level[3]=12 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=301 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=73 frame_size_override_flag=0 order_hint=12 refresh_frame_flags=0 ref_frame_idx[0]=5 ref_frame_idx[1]=3 ref_frame_idx[2]=2 ref_frame_idx[3]=4 ref_frame_idx[4]=6 ref_frame_idx[5]=1 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=99 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=6 loop_filter_level[3]=12 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=301 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=74 frame_size_override_flag=0 order_hint=13 refresh_frame_flags=128 ref_frame_idx[0]=6 ref_frame_idx[1]=5 ref_frame_idx[2]=3 ref_frame_idx[3]=2 ref_frame_idx[4]=4 ref_frame_idx[5]=1 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=99 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=1 loop_filter_level[1]=1 loop_filter_level[2]=6 loop_filter_level[3]=8 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 reduced_tx_set=0 header_bits=301 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 current_frame_id=75 frame_size_override_flag=0 order_hint=14 refresh_frame_flags=0 ref_frame_idx[0]=7 ref_frame_idx[1]=6 ref_frame_idx[2]=5 ref_frame_idx[3]=3 ref_frame_idx[4]=2 ref_frame_idx[5]=4 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=100 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=6 loop_filter_level[3]=4 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=1 reference_select=0 reduced_tx_set=0 header_bits=300 +T obu=6 diff --git a/tests/data/av1/aom_errres.obu b/tests/data/av1/aom_errres.obu new file mode 100644 index 0000000000000000000000000000000000000000..032219b51ffe85fc2a14c176ea743fe484d3be6f GIT binary patch literal 31841 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 T@`?M#xUjcvQJZ8o<3r1#$6e?9YJ=A1Qa&AWZ} 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

x&<=%=#NV?ElVCm1+uYvihtqSXdgP65U9|P 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 literal 0 HcmV?d00001 diff --git a/tests/data/av1/aom_mono.expected b/tests/data/av1/aom_mono.expected new file mode 100644 index 00000000..6ef36b80 --- /dev/null +++ b/tests/data/av1/aom_mono.expected @@ -0,0 +1,28 @@ +F obu=6 show_existing_frame=0 frame_type=0 show_frame=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=0 allow_intrabc=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=5 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 lr_type[0]=0 tx_mode=2 reduced_tx_set=0 header_bits=73 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=11 primary_ref_frame=7 refresh_frame_flags=2 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=40 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=114 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=5 primary_ref_frame=7 refresh_frame_flags=4 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=1 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=53 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=114 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=2 primary_ref_frame=4 refresh_frame_flags=8 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=0 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=59 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=113 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=1 primary_ref_frame=7 refresh_frame_flags=16 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=65 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=119 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=3 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=3 primary_ref_frame=1 refresh_frame_flags=32 ref_frame_idx[0]=3 ref_frame_idx[1]=4 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=0 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=61 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=119 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=4 primary_ref_frame=0 refresh_frame_flags=64 ref_frame_idx[0]=5 ref_frame_idx[1]=3 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=0 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=61 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=1 cdef_y_pri_strength[0]=11 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 lr_type[0]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=119 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=2 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=8 primary_ref_frame=4 refresh_frame_flags=128 ref_frame_idx[0]=2 ref_frame_idx[1]=6 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=4 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=58 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=114 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=6 primary_ref_frame=1 refresh_frame_flags=16 ref_frame_idx[0]=2 ref_frame_idx[1]=6 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=7 ref_frame_idx[5]=3 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=61 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=11 cdef_y_sec_strength[0]=2 lr_type[0]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=113 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=7 primary_ref_frame=0 refresh_frame_flags=8 ref_frame_idx[0]=4 ref_frame_idx[1]=2 ref_frame_idx[2]=6 ref_frame_idx[3]=0 ref_frame_idx[4]=7 ref_frame_idx[5]=5 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=60 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=113 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=7 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=9 primary_ref_frame=1 refresh_frame_flags=32 ref_frame_idx[0]=7 ref_frame_idx[1]=3 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=6 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=60 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=113 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=10 primary_ref_frame=0 refresh_frame_flags=64 ref_frame_idx[0]=5 ref_frame_idx[1]=7 ref_frame_idx[2]=3 ref_frame_idx[3]=0 ref_frame_idx[4]=4 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=60 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=113 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=1 header_bits=4 diff --git a/tests/data/av1/aom_mono.obu b/tests/data/av1/aom_mono.obu new file mode 100644 index 0000000000000000000000000000000000000000..2d5c203cf94ae2193e73dc56d98d979792668b5a GIT binary patch literal 7476 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 literal 0 HcmV?d00001 diff --git a/tests/data/av1/aom_resize.expected b/tests/data/av1/aom_resize.expected new file mode 100644 index 00000000..26adff2c --- /dev/null +++ b/tests/data/av1/aom_resize.expected @@ -0,0 +1,37 @@ +F obu=6 show_existing_frame=0 frame_type=0 show_frame=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=0 frame_width_minus_1=159 frame_height_minus_1=119 render_width_minus_1=319 render_height_minus_1=239 allow_intrabc=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=8 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reduced_tx_set=0 header_bits=125 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=0 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=14 primary_ref_frame=7 refresh_frame_flags=2 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 frame_width_minus_1=212 frame_height_minus_1=159 render_width_minus_1=319 render_height_minus_1=239 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=54 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=4 loop_filter_level[3]=6 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=3 lr_unit_shift=1 lr_uv_shift=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=206 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=7 primary_ref_frame=7 refresh_frame_flags=4 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=1 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=43 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=4 loop_filter_level[3]=6 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=3 lr_type[1]=0 lr_type[2]=0 lr_unit_shift=1 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=144 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=3 primary_ref_frame=4 refresh_frame_flags=8 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=0 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=67 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=142 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=1 primary_ref_frame=7 refresh_frame_flags=16 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=91 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=8 loop_filter_level[1]=8 loop_filter_level[2]=12 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=141 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=2 primary_ref_frame=0 refresh_frame_flags=32 ref_frame_idx[0]=4 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=91 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=8 loop_filter_level[1]=8 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=137 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=3 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=5 primary_ref_frame=0 refresh_frame_flags=64 ref_frame_idx[0]=3 ref_frame_idx[1]=5 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=0 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=79 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=12 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=138 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=4 primary_ref_frame=1 refresh_frame_flags=128 ref_frame_idx[0]=3 ref_frame_idx[1]=5 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=6 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=66 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=12 loop_filter_level[3]=6 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=147 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=6 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=6 primary_ref_frame=1 refresh_frame_flags=16 ref_frame_idx[0]=6 ref_frame_idx[1]=7 ref_frame_idx[2]=3 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=5 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=63 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=4 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=149 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=2 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=10 primary_ref_frame=2 refresh_frame_flags=32 ref_frame_idx[0]=2 ref_frame_idx[1]=4 ref_frame_idx[2]=6 ref_frame_idx[3]=0 ref_frame_idx[4]=7 ref_frame_idx[5]=3 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=62 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=8 loop_filter_level[1]=8 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=0 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=150 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=8 primary_ref_frame=1 refresh_frame_flags=8 ref_frame_idx[0]=2 ref_frame_idx[1]=4 ref_frame_idx[2]=6 ref_frame_idx[3]=0 ref_frame_idx[4]=5 ref_frame_idx[5]=7 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=91 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=8 loop_filter_level[3]=18 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=137 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=9 primary_ref_frame=0 refresh_frame_flags=128 ref_frame_idx[0]=3 ref_frame_idx[1]=2 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=5 ref_frame_idx[5]=6 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=93 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=8 loop_filter_level[1]=8 loop_filter_level[2]=8 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=149 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=5 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=12 primary_ref_frame=0 refresh_frame_flags=64 ref_frame_idx[0]=5 ref_frame_idx[1]=7 ref_frame_idx[2]=3 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=4 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=79 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=8 loop_filter_level[1]=8 loop_filter_level[2]=12 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=0 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=150 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=11 primary_ref_frame=1 refresh_frame_flags=16 ref_frame_idx[0]=5 ref_frame_idx[1]=7 ref_frame_idx[2]=3 ref_frame_idx[3]=0 ref_frame_idx[4]=6 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=95 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=8 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=135 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=6 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=13 primary_ref_frame=1 refresh_frame_flags=4 ref_frame_idx[0]=6 ref_frame_idx[1]=4 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=7 ref_frame_idx[5]=3 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=96 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=135 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=1 order_hint=14 primary_ref_frame=0 refresh_frame_flags=0 ref_frame_idx[0]=2 ref_frame_idx[1]=6 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=5 ref_frame_idx[5]=7 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=97 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=8 loop_filter_level[1]=8 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=1 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=136 +T obu=6 diff --git a/tests/data/av1/aom_resize.obu b/tests/data/av1/aom_resize.obu new file mode 100644 index 0000000000000000000000000000000000000000..20f25a3c078578a0ba2e82fa7a9a875de56faf75 GIT binary patch literal 17301 zcmV)0K+eAs0168L000C$|Ge7yfIu>|85IDZcYvROcaR_e01yBW5Zr~v%alzMk|RF! z*;R@gZDwH2l9>sBmM=~@!%%s(}greYRz-U(_99xzP>#J_)o#CG}t)WcN2^P)jbz>DYagiom_I> zFsBi-b_!k6Bm=iVHrUvlIC^e$o|7YKISVx<6j!AdIDbGAw{O5**SkAX60$$8prZ{= zFWukQK1n?ER2E)97L^kIt;f};IAV24s1lshW6Dry-Y}v{{(3R`L^}==>YGG6)!Oi~ zGD~tH82;t`tsgX3{1}gig`DNSo!U~JZ{cZ=zy#A@|IyQyTY6l+Agbmcc^Dw-DnJYK zn0xf_r2tajOI7Qzj+LwdLr9*}aj# zwxs`o4^2@NuSP-^Sjc!e$0cdtBa?PdLZGj{)cu4TWCuE?x^pV3Sk#zS_|%^?dq-~$ zt*tlObqFu^&qNj|jubg6iM&ebtFc#JF|F?eWzp1Ad{3(64UAT5FJ2XLqTlw9W0lXP zYlF}Z&{L`fYYXe@?yCD6b zm}hBF-*8TGG@~KzJ_KW8zC#FdUNJHn^Y$bE`-mHW0douS9RB>CC*wo-qEv@MVjU|O z{{0;<(13&7_Rx`bnE;?4I`%=Xl3^24o5%|5?eyN2I0QQiCEVVJdOJEg3p$mP*GB65 z&e3Ib2BB6$ud&zO$!@y3$`J&?T(m=7`GX)Ol?-IyHNWP3TZ6E9;YF=>0?0z66v6{y zUI(bWU>lxKFIophymh+lx+}h;7mu!z8+qv_mADfjy?Yc5zKnyOhD2cFq$*qQd&-KJ z+4z^iuY-9`Mo`6VXI3A4L8cxJyAZ`T$WZPY?Hq7&7h|c)iBaUpj|hQwr^bNYW&t1u zU(%2a)-#Cy#g2u^yI|r`q$w~A&PJ|FCXW100OnHxbVqH{pHW<1Y{zRk^m%m715UiC!~OI4d%NujH*$61tGB)P@hN zjZYr$v~7{9KJHL*HW>!0|M_vO(ObB4UukK&moD0R)C!M2iWnoZFUGT7GXX z)7K;fA>&z-!QcQ!k}IiRL-Lp86f8@nMK>QK2c!T%<<0qFo%dXxJ=e8WBu^p zilJu5n+nlJhyQJofmgyKg*KywTfISeOcp{JP{tfx^dQplFHSzC)d3@|v5-QTOfc
{t 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 literal 0 HcmV?d00001 diff --git a/tests/data/av1/aom_tilegroups.expected b/tests/data/av1/aom_tilegroups.expected new file mode 100644 index 00000000..50057a66 --- /dev/null +++ b/tests/data/av1/aom_tilegroups.expected @@ -0,0 +1,36 @@ +F obu=3 show_existing_frame=0 frame_type=0 show_frame=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=0 allow_intrabc=0 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=0 tile_size_bytes_minus1=3 base_q_idx=33 using_qmatrix=1 qm_y=5 qm_u=5 segmentation_enabled=1 delta_q_present=1 delta_q_res=2 delta_lf_present=1 delta_lf_res=1 delta_lf_multi=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=0 cdef_bits=2 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=0 cdef_y_pri_strength[2]=11 cdef_y_sec_strength[2]=0 cdef_y_pri_strength[3]=0 cdef_y_sec_strength[3]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=3 lr_unit_shift=1 lr_uv_shift=0 tx_mode=2 reduced_tx_set=0 header_bits=290 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=1 primary_ref_frame=7 refresh_frame_flags=2 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=93 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 delta_q_present=1 delta_q_res=2 delta_lf_present=1 delta_lf_res=1 delta_lf_multi=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=4 loop_filter_level[3]=5 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=11 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=252 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=2 primary_ref_frame=0 refresh_frame_flags=4 ref_frame_idx[0]=1 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=93 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 segmentation_update_map=0 segmentation_update_data=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=8 loop_filter_level[3]=13 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=184 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=3 primary_ref_frame=0 refresh_frame_flags=8 ref_frame_idx[0]=2 ref_frame_idx[1]=1 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=93 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 segmentation_update_map=0 segmentation_update_data=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=8 loop_filter_level[3]=9 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=172 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=4 primary_ref_frame=0 refresh_frame_flags=16 ref_frame_idx[0]=3 ref_frame_idx[1]=2 ref_frame_idx[2]=1 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=94 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 segmentation_update_map=0 segmentation_update_data=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=8 loop_filter_level[3]=13 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=172 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=5 primary_ref_frame=0 refresh_frame_flags=32 ref_frame_idx[0]=4 ref_frame_idx[1]=3 ref_frame_idx[2]=2 ref_frame_idx[3]=0 ref_frame_idx[4]=1 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=95 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 segmentation_update_map=0 segmentation_update_data=0 delta_q_present=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=8 loop_filter_level[3]=9 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=184 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=6 primary_ref_frame=0 refresh_frame_flags=64 ref_frame_idx[0]=5 ref_frame_idx[1]=4 ref_frame_idx[2]=3 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=1 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=96 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 segmentation_update_map=0 segmentation_update_data=0 delta_q_present=0 loop_filter_level[0]=8 loop_filter_level[1]=8 loop_filter_level[2]=8 loop_filter_level[3]=13 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=184 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=7 primary_ref_frame=0 refresh_frame_flags=128 ref_frame_idx[0]=6 ref_frame_idx[1]=5 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=97 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 segmentation_update_map=0 segmentation_update_data=0 delta_q_present=1 delta_q_res=2 delta_lf_present=1 delta_lf_res=1 delta_lf_multi=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=4 loop_filter_level[3]=9 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=190 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=8 primary_ref_frame=7 refresh_frame_flags=2 ref_frame_idx[0]=7 ref_frame_idx[1]=6 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=4 ref_frame_idx[5]=3 ref_frame_idx[6]=2 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=98 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 delta_q_present=1 delta_q_res=2 delta_lf_present=1 delta_lf_res=1 delta_lf_multi=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=4 loop_filter_level[3]=9 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=252 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=9 primary_ref_frame=0 refresh_frame_flags=4 ref_frame_idx[0]=7 ref_frame_idx[1]=6 ref_frame_idx[2]=5 ref_frame_idx[3]=1 ref_frame_idx[4]=4 ref_frame_idx[5]=3 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=99 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 segmentation_update_map=0 segmentation_update_data=0 delta_q_present=1 delta_q_res=2 delta_lf_present=1 delta_lf_res=1 delta_lf_multi=0 loop_filter_level[0]=8 loop_filter_level[1]=8 loop_filter_level[2]=4 loop_filter_level[3]=13 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=190 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=10 primary_ref_frame=0 refresh_frame_flags=8 ref_frame_idx[0]=2 ref_frame_idx[1]=7 ref_frame_idx[2]=6 ref_frame_idx[3]=1 ref_frame_idx[4]=5 ref_frame_idx[5]=4 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=100 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 segmentation_update_map=0 segmentation_update_data=0 delta_q_present=1 delta_q_res=2 delta_lf_present=1 delta_lf_res=1 delta_lf_multi=0 loop_filter_level[0]=4 loop_filter_level[1]=4 loop_filter_level[2]=8 loop_filter_level[3]=9 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=178 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 +F obu=3 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=11 primary_ref_frame=0 refresh_frame_flags=16 ref_frame_idx[0]=3 ref_frame_idx[1]=2 ref_frame_idx[2]=7 ref_frame_idx[3]=1 ref_frame_idx[4]=6 ref_frame_idx[5]=5 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=2 tile_size_bytes_minus1=3 base_q_idx=101 using_qmatrix=1 qm_y=6 qm_u=6 segmentation_enabled=1 segmentation_update_map=0 segmentation_update_data=0 delta_q_present=1 delta_q_res=2 delta_lf_present=1 delta_lf_res=1 delta_lf_multi=0 loop_filter_level[0]=8 loop_filter_level[1]=8 loop_filter_level[2]=8 loop_filter_level[3]=9 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 lr_type[0]=0 lr_type[1]=0 lr_type[2]=0 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=178 +T obu=4 tile_start_and_end_present_flag=1 tg_start=0 tg_end=1 +T obu=4 tile_start_and_end_present_flag=1 tg_start=2 tg_end=3 diff --git a/tests/data/av1/aom_tilegroups.obu b/tests/data/av1/aom_tilegroups.obu new file mode 100644 index 0000000000000000000000000000000000000000..c51257f23c15152821626cb2833b03a6e6883cb4 GIT binary patch literal 17845 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 O;jV$zug1aXtY_)Gen5Hv literal 0 HcmV?d00001 diff --git a/tests/data/av1/aom_tiles.expected b/tests/data/av1/aom_tiles.expected new file mode 100644 index 00000000..3e69aac7 --- /dev/null +++ b/tests/data/av1/aom_tiles.expected @@ -0,0 +1,36 @@ +F obu=6 show_existing_frame=0 frame_type=0 show_frame=1 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=0 allow_intrabc=0 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=99 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reduced_tx_set=0 header_bits=103 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=0 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=13 primary_ref_frame=7 refresh_frame_flags=2 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=101 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=8 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=152 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=6 primary_ref_frame=7 refresh_frame_flags=4 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=1 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=106 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=12 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=153 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=3 primary_ref_frame=4 refresh_frame_flags=8 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=0 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=111 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=3 loop_filter_level[1]=3 loop_filter_level[2]=4 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=141 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=1 primary_ref_frame=7 refresh_frame_flags=16 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=127 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=4 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=137 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=2 primary_ref_frame=0 refresh_frame_flags=32 ref_frame_idx[0]=4 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=3 loop_filter_level[1]=3 loop_filter_level[2]=8 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=149 +T obu=6 tile_start_and_end_present_flag=0 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=3 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=4 primary_ref_frame=1 refresh_frame_flags=64 ref_frame_idx[0]=3 ref_frame_idx[1]=5 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=0 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=125 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=137 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=5 primary_ref_frame=0 refresh_frame_flags=128 ref_frame_idx[0]=6 ref_frame_idx[1]=3 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=4 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=4 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=149 +T obu=6 tile_start_and_end_present_flag=0 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=2 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=9 primary_ref_frame=4 refresh_frame_flags=16 ref_frame_idx[0]=2 ref_frame_idx[1]=7 ref_frame_idx[2]=6 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=5 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=120 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=4 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=141 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=7 primary_ref_frame=1 refresh_frame_flags=32 ref_frame_idx[0]=2 ref_frame_idx[1]=7 ref_frame_idx[2]=6 ref_frame_idx[3]=0 ref_frame_idx[4]=4 ref_frame_idx[5]=3 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=126 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=12 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=137 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=8 primary_ref_frame=0 refresh_frame_flags=8 ref_frame_idx[0]=5 ref_frame_idx[1]=2 ref_frame_idx[2]=7 ref_frame_idx[3]=0 ref_frame_idx[4]=4 ref_frame_idx[5]=6 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=149 +T obu=6 tile_start_and_end_present_flag=0 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=4 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=11 primary_ref_frame=0 refresh_frame_flags=64 ref_frame_idx[0]=4 ref_frame_idx[1]=3 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=2 ref_frame_idx[5]=7 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=120 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=2 delta_lf_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=141 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=10 primary_ref_frame=1 refresh_frame_flags=128 ref_frame_idx[0]=4 ref_frame_idx[1]=3 ref_frame_idx[2]=5 ref_frame_idx[3]=0 ref_frame_idx[4]=6 ref_frame_idx[5]=2 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=3 loop_filter_level[1]=3 loop_filter_level[2]=8 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=11 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=0 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=149 +T obu=6 tile_start_and_end_present_flag=0 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=6 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=12 primary_ref_frame=1 refresh_frame_flags=4 ref_frame_idx[0]=6 ref_frame_idx[1]=7 ref_frame_idx[2]=4 ref_frame_idx[3]=0 ref_frame_idx[4]=3 ref_frame_idx[5]=5 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=127 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=3 loop_filter_level[1]=3 loop_filter_level[2]=8 loop_filter_level[3]=14 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 tx_mode=2 reference_select=1 skip_mode_present=1 allow_warped_motion=1 reduced_tx_set=0 header_bits=137 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=13 primary_ref_frame=0 refresh_frame_flags=0 ref_frame_idx[0]=2 ref_frame_idx[1]=6 ref_frame_idx[2]=7 ref_frame_idx[3]=0 ref_frame_idx[4]=4 ref_frame_idx[5]=3 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=0 tile_size_bytes_minus1=0 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=3 loop_filter_level[1]=3 loop_filter_level[2]=4 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 tx_mode=1 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=136 +T obu=6 tile_start_and_end_present_flag=0 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=1 force_integer_mv=0 frame_size_override_flag=0 order_hint=14 primary_ref_frame=6 refresh_frame_flags=32 ref_frame_idx[0]=2 ref_frame_idx[1]=6 ref_frame_idx[2]=7 ref_frame_idx[3]=1 ref_frame_idx[4]=4 ref_frame_idx[5]=3 ref_frame_idx[6]=0 allow_high_precision_mv=0 is_filter_switchable=0 interpolation_filter=0 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=1 tile_rows_log2=1 context_update_tile_id=3 tile_size_bytes_minus1=1 base_q_idx=128 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=11 loop_filter_level[1]=11 loop_filter_level[2]=8 loop_filter_level[3]=10 loop_filter_sharpness=0 loop_filter_delta_enabled=1 loop_filter_delta_update=0 cdef_damping_minus_3=2 cdef_bits=1 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=11 cdef_y_sec_strength[1]=2 tx_mode=2 reference_select=0 allow_warped_motion=1 reduced_tx_set=0 header_bits=148 +T obu=6 tile_start_and_end_present_flag=0 diff --git a/tests/data/av1/aom_tiles.obu b/tests/data/av1/aom_tiles.obu new file mode 100644 index 0000000000000000000000000000000000000000..262ae8470206138be4dc29612af3d21d5d1332c9 GIT binary patch literal 15932 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 Z+`CFIlk>_w&(p?Uo>% literal 0 HcmV?d00001 diff --git a/tests/data/av1/extract_trace.py b/tests/data/av1/extract_trace.py new file mode 100644 index 00000000..f5e647c7 --- /dev/null +++ b/tests/data/av1/extract_trace.py @@ -0,0 +1,129 @@ +"""Reduce ffmpeg trace_headers output for an AV1 stream to the per-frame +expectations the parser tests check (src/decoder/av1/stream_tests.rs). + +One line per frame header ("F ...") and one per tile group ("T ..."). + +Usage: + ffmpeg -i x.obu -c copy -bsf:v trace_headers -f null - 2> x.trace + python3 extract_trace.py x.trace > x.expected + +The fixtures were made from testsrc2 (320x240, 15 frames; aom_mono 176x144) +with ffmpeg n9.0, libaom and SVT-AV1 4.2: + aom_altref: ffmpeg -c:v libaom-av1 -cpu-used 8 -lag-in-frames 16 -auto-alt-ref 1 -f obu + aom_tiles: ffmpeg -c:v libaom-av1 -cpu-used 8 -tiles 2x2 -row-mt 0 -f obu + aom_10bit: ffmpeg -c:v libaom-av1 -cpu-used 8 -pix_fmt yuv420p10le -f obu + aom_grain: ffmpeg -c:v libaom-av1 -cpu-used 8 -denoise-noise-level 10 -f obu + svt_grain: ffmpeg -c:v libsvtav1 -preset 10 -svtav1-params film-grain=8 -f obu + aom_errres: aomenc --cpu-used=8 --limit=15 --obu --error-resilient=1 --sframe-dist=5 --sframe-mode=1 + aom_superres: aomenc --cpu-used=8 --limit=15 --obu --superres-mode=1 --superres-denominator=12 --superres-kf-denominator=10 + aom_resize: aomenc --cpu-used=8 --limit=15 --obu --resize-mode=1 --resize-denominator=12 --resize-kf-denominator=16 + aom_tilegroups: aomenc --cpu-used=8 --limit=12 --obu --tile-columns=1 --tile-rows=1 --num-tile-groups=2 + --sb-size=128 --enable-qm=1 --aq-mode=1 --delta-lf-mode=1 --deltaq-mode=3 + --lag-in-frames=0 --timing-info=model + aom_mono: aomenc --cpu-used=8 --limit=12 --obu --monochrome --enable-restoration=1 + aom_annexb: aomenc --cpu-used=8 --limit=3 --obu --annexb=1 +""" + + +import re +import sys + +KEYS = { + "show_existing_frame", "frame_to_show_map_idx", "frame_type", "show_frame", + "showable_frame", "error_resilient_mode", "disable_cdf_update", + "allow_screen_content_tools", "force_integer_mv", "current_frame_id", + "frame_size_override_flag", "order_hint", "primary_ref_frame", + "refresh_frame_flags", "use_superres", "coded_denom", "allow_intrabc", + "allow_high_precision_mv", "interpolation_filter", "is_filter_switchable", + "use_ref_frame_mvs", "disable_frame_end_update_cdf", "tile_cols_log2", + "tile_rows_log2", "context_update_tile_id", "tile_size_bytes_minus1", + "base_q_idx", "using_qmatrix", "qm_y", "qm_u", "qm_v", "segmentation_enabled", + "segmentation_update_map", "segmentation_temporal_update", + "segmentation_update_data", "delta_q_present", "delta_q_res", + "delta_lf_present", "delta_lf_res", "delta_lf_multi", "loop_filter_sharpness", + "loop_filter_delta_enabled", "loop_filter_delta_update", "cdef_bits", + "cdef_damping_minus_3", "tx_mode", "reference_select", "skip_mode_present", + "allow_warped_motion", "reduced_tx_set", "apply_grain", "grain_seed", + "update_grain", "film_grain_params_ref_idx", "num_y_points", "num_cb_points", + "num_cr_points", "frame_width_minus_1", "frame_height_minus_1", + "render_width_minus_1", "render_height_minus_1", "lr_unit_shift", + "lr_uv_shift", +} +INDEXED = {"ref_frame_idx", "loop_filter_level", "lr_type", + "cdef_y_pri_strength", "cdef_y_sec_strength"} + +LINE = re.compile(r"^\[trace_headers @ 0x[0-9a-f]+\] (.*)$") +ELEM = re.compile(r"^(\d+)\s+(\S+)\s+([01]+) = (-?\d+)$") + + +def main(path): + out = [] + section = None + obu_type = None + obu_size = None + frame = None + tg = None + + def flush(): + nonlocal frame, tg + if frame is not None: + start, end, kv = frame + kv.append(("header_bits", end - start)) + out.append("F obu=%d " % frame_obu + " ".join("%s=%s" % p for p in kv)) + frame = None + if tg is not None: + out.append("T obu=%d " % tg_obu + " ".join("%s=%s" % p for p in tg)) + tg = None + + frame_obu = tg_obu = 0 + for raw in open(path, errors="replace"): + m = LINE.match(raw.rstrip("\n")) + if not m: + continue + body = m.group(1).strip() + e = ELEM.match(body) + if not e: + if body in ("Frame Header", "Tile Group", "Redundant Frame Header"): + if body == "Tile Group": + flush_frame_only = frame is not None + if flush_frame_only: + start, end, kv = frame + kv.append(("header_bits", end - start)) + out.append("F obu=%d " % frame_obu + " ".join("%s=%s" % p for p in kv)) + frame = None + tg = [] + tg_obu = obu_type + elif body == "Frame Header": + frame = [None, None, []] + frame_obu = obu_type + section = body + elif body == "OBU header": + flush() + section = "OBU header" + else: + section = body + continue + pos, name, bits, value = int(e.group(1)), e.group(2), e.group(3), e.group(4) + if section == "OBU header": + if name == "obu_type": + obu_type = int(value) + if name == "obu_size": + obu_size = int(value) + continue + if section == "Frame Header" and frame is not None: + if name in ("zero_bit", "trailing_one_bit", "trailing_zero_bit"): + continue + if frame[0] is None: + frame[0] = pos + frame[1] = pos + len(bits) + base = name.split("[")[0] + if name in KEYS or base in INDEXED: + frame[2].append((name, value)) + elif section == "Tile Group" and tg is not None: + if name in ("tile_start_and_end_present_flag", "tg_start", "tg_end"): + tg.append((name, value)) + flush() + print("\n".join(out)) + + +main(sys.argv[1]) diff --git a/tests/data/av1/svt_grain.expected b/tests/data/av1/svt_grain.expected new file mode 100644 index 00000000..02d96b63 --- /dev/null +++ b/tests/data/av1/svt_grain.expected @@ -0,0 +1,37 @@ +F obu=6 show_existing_frame=0 frame_type=0 show_frame=1 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=0 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=69 using_qmatrix=0 segmentation_enabled=0 delta_q_present=1 delta_q_res=0 delta_lf_present=0 loop_filter_level[0]=3 loop_filter_level[1]=3 loop_filter_level[2]=1 loop_filter_level[3]=1 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=1 cdef_bits=2 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=0 cdef_y_pri_strength[2]=0 cdef_y_sec_strength[2]=0 cdef_y_pri_strength[3]=15 cdef_y_sec_strength[3]=0 tx_mode=1 reduced_tx_set=0 apply_grain=1 grain_seed=7391 num_y_points=8 num_cb_points=10 num_cr_points=10 header_bits=1248 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=8 primary_ref_frame=0 refresh_frame_flags=2 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=91 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=2 loop_filter_level[3]=2 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=1 cdef_bits=1 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 cdef_y_pri_strength[1]=0 cdef_y_sec_strength[1]=2 tx_mode=1 reference_select=1 allow_warped_motion=1 reduced_tx_set=0 apply_grain=1 grain_seed=34439 update_grain=1 num_y_points=10 num_cb_points=8 num_cr_points=10 header_bits=1270 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=4 primary_ref_frame=0 refresh_frame_flags=16 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=1 ref_frame_idx[5]=1 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=111 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=20915 update_grain=1 num_y_points=8 num_cb_points=9 num_cr_points=10 header_bits=1231 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=2 primary_ref_frame=4 refresh_frame_flags=32 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=4 ref_frame_idx[5]=4 ref_frame_idx[6]=4 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=124 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=14153 update_grain=1 num_y_points=9 num_cb_points=10 num_cr_points=10 header_bits=1263 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=1 primary_ref_frame=4 refresh_frame_flags=0 ref_frame_idx[0]=0 ref_frame_idx[1]=0 ref_frame_idx[2]=0 ref_frame_idx[3]=0 ref_frame_idx[4]=5 ref_frame_idx[5]=5 ref_frame_idx[6]=5 allow_high_precision_mv=0 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=140 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=2 cdef_bits=0 cdef_y_pri_strength[0]=7 cdef_y_sec_strength[0]=3 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=10772 update_grain=1 num_y_points=11 num_cb_points=10 num_cr_points=10 header_bits=1294 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=5 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=3 primary_ref_frame=0 refresh_frame_flags=0 ref_frame_idx[0]=5 ref_frame_idx[1]=5 ref_frame_idx[2]=5 ref_frame_idx[3]=5 ref_frame_idx[4]=4 ref_frame_idx[5]=4 ref_frame_idx[6]=4 allow_high_precision_mv=0 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=140 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=17534 update_grain=1 num_y_points=7 num_cb_points=10 num_cr_points=10 header_bits=1230 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=4 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=6 primary_ref_frame=0 refresh_frame_flags=32 ref_frame_idx[0]=4 ref_frame_idx[1]=4 ref_frame_idx[2]=4 ref_frame_idx[3]=4 ref_frame_idx[4]=1 ref_frame_idx[5]=1 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=124 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=27677 update_grain=1 num_y_points=10 num_cb_points=10 num_cr_points=10 header_bits=1279 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=5 primary_ref_frame=4 refresh_frame_flags=0 ref_frame_idx[0]=4 ref_frame_idx[1]=4 ref_frame_idx[2]=4 ref_frame_idx[3]=4 ref_frame_idx[4]=5 ref_frame_idx[5]=5 ref_frame_idx[6]=5 allow_high_precision_mv=0 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=140 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=0 cdef_y_sec_strength[0]=0 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=24296 update_grain=1 num_y_points=11 num_cb_points=9 num_cr_points=10 header_bits=1278 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=5 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=7 primary_ref_frame=0 refresh_frame_flags=0 ref_frame_idx[0]=5 ref_frame_idx[1]=5 ref_frame_idx[2]=5 ref_frame_idx[3]=5 ref_frame_idx[4]=1 ref_frame_idx[5]=1 ref_frame_idx[6]=1 allow_high_precision_mv=0 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=140 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=2 cdef_bits=0 cdef_y_pri_strength[0]=8 cdef_y_sec_strength[0]=0 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=31058 update_grain=1 num_y_points=10 num_cb_points=10 num_cr_points=10 header_bits=1278 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=1 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=12 primary_ref_frame=0 refresh_frame_flags=4 ref_frame_idx[0]=1 ref_frame_idx[1]=1 ref_frame_idx[2]=1 ref_frame_idx[3]=1 ref_frame_idx[4]=1 ref_frame_idx[5]=1 ref_frame_idx[6]=1 allow_high_precision_mv=1 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=109 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=5 loop_filter_level[1]=5 loop_filter_level[2]=2 loop_filter_level[3]=2 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=1 cdef_bits=0 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 tx_mode=1 reference_select=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=47963 update_grain=1 num_y_points=9 num_cb_points=10 num_cr_points=10 header_bits=1274 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=10 primary_ref_frame=0 refresh_frame_flags=8 ref_frame_idx[0]=1 ref_frame_idx[1]=1 ref_frame_idx[2]=1 ref_frame_idx[3]=1 ref_frame_idx[4]=2 ref_frame_idx[5]=2 ref_frame_idx[6]=2 allow_high_precision_mv=0 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=132 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=2 cdef_bits=0 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=41201 update_grain=1 num_y_points=7 num_cb_points=8 num_cr_points=9 header_bits=1183 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=9 primary_ref_frame=4 refresh_frame_flags=0 ref_frame_idx[0]=1 ref_frame_idx[1]=1 ref_frame_idx[2]=1 ref_frame_idx[3]=1 ref_frame_idx[4]=3 ref_frame_idx[5]=3 ref_frame_idx[6]=3 allow_high_precision_mv=0 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=140 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=2 cdef_bits=0 cdef_y_pri_strength[0]=8 cdef_y_sec_strength[0]=0 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=37820 update_grain=1 num_y_points=7 num_cb_points=10 num_cr_points=10 header_bits=1230 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=3 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=11 primary_ref_frame=0 refresh_frame_flags=0 ref_frame_idx[0]=3 ref_frame_idx[1]=3 ref_frame_idx[2]=3 ref_frame_idx[3]=3 ref_frame_idx[4]=2 ref_frame_idx[5]=2 ref_frame_idx[6]=2 allow_high_precision_mv=0 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=140 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=2 cdef_bits=0 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=44582 update_grain=1 num_y_points=10 num_cb_points=10 num_cr_points=10 header_bits=1278 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=2 header_bits=4 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=0 showable_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=14 primary_ref_frame=0 refresh_frame_flags=1 ref_frame_idx[0]=2 ref_frame_idx[1]=2 ref_frame_idx[2]=2 ref_frame_idx[3]=2 ref_frame_idx[4]=2 ref_frame_idx[5]=2 ref_frame_idx[6]=2 allow_high_precision_mv=0 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=137 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=7 loop_filter_level[1]=7 loop_filter_level[2]=3 loop_filter_level[3]=3 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=2 cdef_bits=0 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 tx_mode=1 reference_select=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=54725 update_grain=1 num_y_points=10 num_cb_points=9 num_cr_points=10 header_bits=1274 +T obu=6 +F obu=6 show_existing_frame=0 frame_type=1 show_frame=1 error_resilient_mode=0 disable_cdf_update=0 allow_screen_content_tools=0 frame_size_override_flag=0 order_hint=13 primary_ref_frame=0 refresh_frame_flags=0 ref_frame_idx[0]=2 ref_frame_idx[1]=2 ref_frame_idx[2]=2 ref_frame_idx[3]=2 ref_frame_idx[4]=0 ref_frame_idx[5]=0 ref_frame_idx[6]=0 allow_high_precision_mv=0 is_filter_switchable=1 use_ref_frame_mvs=1 disable_frame_end_update_cdf=0 tile_cols_log2=0 tile_rows_log2=0 base_q_idx=140 using_qmatrix=0 segmentation_enabled=0 delta_q_present=0 loop_filter_level[0]=0 loop_filter_level[1]=0 loop_filter_sharpness=0 loop_filter_delta_enabled=0 cdef_damping_minus_3=2 cdef_bits=0 cdef_y_pri_strength[0]=15 cdef_y_sec_strength[0]=2 tx_mode=1 reference_select=1 skip_mode_present=1 allow_warped_motion=0 reduced_tx_set=0 apply_grain=1 grain_seed=51344 update_grain=1 num_y_points=9 num_cb_points=10 num_cr_points=10 header_bits=1262 +T obu=6 +F obu=3 show_existing_frame=1 frame_to_show_map_idx=0 header_bits=4 diff --git a/tests/data/av1/svt_grain.obu b/tests/data/av1/svt_grain.obu new file mode 100644 index 0000000000000000000000000000000000000000..8bce74bec57f015b211a66b17739e5d309c3711d GIT binary patch literal 28734 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 D5vji~ literal 0 HcmV?d00001 From 3d3a8565114448b6aa62c96b8aa21049f260d8a9 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 12:23:33 +0100 Subject: [PATCH 12/17] feat(decoder): add the H.265 parser and reference bookkeeping --- src/decoder/h265/dpb.rs | 440 ++++++++++++++++++ src/decoder/h265/dpb_tests.rs | 665 +++++++++++++++++++++++++++ src/decoder/h265/mod.rs | 29 ++ src/decoder/h265/parser.rs | 742 +++++++++++++++++++++++++++++++ src/decoder/h265/parser_tests.rs | 701 +++++++++++++++++++++++++++++ src/decoder/h265/pps.rs | 251 +++++++++++ src/decoder/h265/rps.rs | 263 +++++++++++ src/decoder/h265/scaling.rs | 162 +++++++ src/decoder/h265/slice.rs | 268 +++++++++++ src/decoder/h265/sps.rs | 464 +++++++++++++++++++ src/decoder/h265/syntax_tests.rs | 229 ++++++++++ src/decoder/mod.rs | 3 + tests/data/closedgop.265 | Bin 0 -> 10845 bytes tests/data/cra_start.265 | Bin 0 -> 1222 bytes tests/data/main10.265 | Bin 0 -> 7586 bytes tests/data/nvenc.265 | Bin 0 -> 6781 bytes tests/data/opengop.265 | Bin 0 -> 11033 bytes tests/data/scaling.265 | Bin 0 -> 4138 bytes tests/data/slices.265 | Bin 0 -> 8572 bytes tests/data/temporal.265 | Bin 0 -> 4397 bytes 20 files changed, 4217 insertions(+) create mode 100644 src/decoder/h265/dpb.rs create mode 100644 src/decoder/h265/dpb_tests.rs create mode 100644 src/decoder/h265/mod.rs create mode 100644 src/decoder/h265/parser.rs create mode 100644 src/decoder/h265/parser_tests.rs create mode 100644 src/decoder/h265/pps.rs create mode 100644 src/decoder/h265/rps.rs create mode 100644 src/decoder/h265/scaling.rs create mode 100644 src/decoder/h265/slice.rs create mode 100644 src/decoder/h265/sps.rs create mode 100644 src/decoder/h265/syntax_tests.rs create mode 100644 tests/data/closedgop.265 create mode 100644 tests/data/cra_start.265 create mode 100644 tests/data/main10.265 create mode 100644 tests/data/nvenc.265 create mode 100644 tests/data/opengop.265 create mode 100644 tests/data/scaling.265 create mode 100644 tests/data/slices.265 create mode 100644 tests/data/temporal.265 diff --git a/src/decoder/h265/dpb.rs b/src/decoder/h265/dpb.rs new file mode 100644 index 00000000..c5440965 --- /dev/null +++ b/src/decoder/h265/dpb.rs @@ -0,0 +1,440 @@ +//! Decode-side H.265 picture order count and reference picture set handling. +//! +//! H.265 has no sliding window and no marking commands: every slice header +//! names the complete set of pictures to keep (the reference picture set), and +//! anything not named is dropped *before* the current picture is decoded. This +//! module derives the picture order count (clause 8.3.1), resolves the +//! reference picture set against the pictures held (clause 8.3.2), and keeps +//! the DPB slots the way the H.264 decoder's `DecodeDpb` does, so the session +//! code can drive both alike. +//! +//! Random access follows `NoRaslOutputFlag`: an IDR or BLA picture, the first +//! picture of the stream and the first after an end of sequence all start a +//! new coded video sequence, and the RASL pictures that lead such a CRA are +//! skipped, since they reference pictures the decoder never saw. +//! `HandleCraAsBlaFlag` is always 0, and unavailable reference pictures +//! (clause 8.3.3) are never generated: the only pictures that would need them +//! are those RASL pictures, and they are not decoded. + +use std::sync::Arc; + +use super::parser::{NalHeader, NalType, invalid}; +use super::slice::SliceHeader; +use super::sps::Sps; +use crate::decoder::frames::SlotPins; +use crate::error::Result; + +/// DPB slots this module can address: `MaxDpbSize` (16) pictures, which +/// includes the current one, plus one so a picture held for output never +/// blocks the next decode. +pub(crate) const MAX_DPB_SLOTS: usize = 17; + +/// Entries in each of Vulkan's `RefPicSet*` arrays +/// (`STD_VIDEO_DECODE_H265_REF_PIC_SET_LIST_SIZE`). +pub(crate) const VULKAN_REF_PIC_SET_LIST_SIZE: usize = 8; + +/// The value Vulkan's `RefPicSet*` arrays use for "no reference picture" +/// (`STD_VIDEO_H265_NO_REFERENCE_PICTURE`). +pub(crate) const NO_REFERENCE_PICTURE: u8 = 0xFF; + +/// A picture currently held in the DPB as a reference. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct RefPicture { + /// DPB slot index this picture lives in. + pub slot: u8, + /// `PicOrderCntVal`. + pub poc: i32, + /// Marked "used for long-term reference"; otherwise short-term. + pub long_term: bool, +} + +/// One entry of a reference picture set list: the POC the slice header asks +/// for, and the slot of the picture that has it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct RpsEntry { + pub poc: i32, + /// `None` is the standard's "no reference picture". + pub slot: Option, +} + +/// The five lists of clause 8.3.2, resolved to DPB slots. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct RefPicSet { + /// `RefPicSetStCurrBefore`: short-term, used by the current picture, + /// preceding it in output order. + pub st_curr_before: Vec, + /// `RefPicSetStCurrAfter`: short-term, used, following in output order. + pub st_curr_after: Vec, + /// `RefPicSetStFoll`: short-term, kept for later pictures only. + pub st_foll: Vec, + /// `RefPicSetLtCurr`: long-term, used by the current picture. + pub lt_curr: Vec, + /// `RefPicSetLtFoll`: long-term, kept for later pictures only. + pub lt_foll: Vec, +} + +impl RefPicSet { + /// Whether a picture the current one predicts from is not in the DPB. + /// + /// Missing entries in the two `Foll` lists do not matter to the current + /// picture and are not counted (the standard ignores them too). + pub fn missing_current(&self) -> bool { + self.st_curr_before + .iter() + .chain(&self.st_curr_after) + .chain(&self.lt_curr) + .any(|e| e.slot.is_none()) + } + + /// A list in the form Vulkan's `RefPicSetStCurrBefore`, `RefPicSetStCurrAfter` + /// and `RefPicSetLtCurr` take: slot indices, padded with + /// [`NO_REFERENCE_PICTURE`]. + pub fn vulkan_list(list: &[RpsEntry]) -> Result<[u8; VULKAN_REF_PIC_SET_LIST_SIZE]> { + if list.len() > VULKAN_REF_PIC_SET_LIST_SIZE { + return Err(invalid( + "more than 8 pictures in one reference picture set list", + )); + } + let mut out = [NO_REFERENCE_PICTURE; VULKAN_REF_PIC_SET_LIST_SIZE]; + for (dst, entry) in out.iter_mut().zip(list) { + *dst = entry.slot.unwrap_or(NO_REFERENCE_PICTURE); + } + Ok(out) + } +} + +/// Why a picture is not to be decoded. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SkipReason { + /// No IRAP picture has started a coded video sequence yet (the stream + /// starts mid-sequence, or after an end of sequence). The caller should + /// ask for a keyframe. + AwaitingIrap, + /// A RASL picture led by a CRA that started decoding: it references + /// pictures from before the CRA, which the decoder never had. Skipping it + /// is normal and not an error. + RaslWithoutReferences, +} + +/// Per-picture state derived from the bitstream. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct PictureState { + /// `PicOrderCntVal`. + pub poc: i32, + pub nal_type: NalType, + pub temporal_id: u8, + /// `NoRaslOutputFlag`. Meaningful for IRAP pictures only. + pub no_rasl_output_flag: bool, + /// An IRAP picture with `NoRaslOutputFlag`: decoding can start here, and + /// nothing after it in decoding order references anything before it. + pub is_keyframe: bool, + /// `PicOutputFlag`: whether the picture is to be output at all. + pub pic_output_flag: bool, + /// `NoOutputOfPriorPicsFlag` (clause C.5.2.2), set only on a keyframe. + /// + /// On a keyframe every picture still waiting for output is either output + /// first (false) or discarded (true). It is inferred to be true for a CRA, + /// so a caller that flushes its output at an end of sequence NAL unit, + /// before the CRA that follows it arrives, loses nothing. + pub no_output_of_prior_pics: bool, + /// Whether the decoded picture is kept as a reference afterwards. False + /// only for a sub-layer non-reference picture of the highest sub-layer, + /// which nothing can reference. + pub is_reference: bool, + /// The reference picture set, resolved to slots. Empty for IDR pictures. + pub rps: RefPicSet, + /// Whether a picture the current one predicts from is missing (see + /// [`RefPicSet::missing_current`]). Decoding it anyway produces corrupt + /// output; the caller would normally skip it and ask for a keyframe. + pub missing_reference: bool, +} + +/// What [`DecodeDpb::begin_picture`] decided. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum PictureDecision { + Decode(PictureState), + Skip(SkipReason), +} + +/// Decode-side DPB: POC state, the reference pictures held and their slots. +pub(crate) struct DecodeDpb { + /// Slots reserved by decoded frames the caller still holds. Never handed + /// out for a new picture, however the reference rules mark them. + pins: Arc, + /// Reference pictures, in decoding order. + refs: Vec, + /// Which slots are occupied (by a reference or the in-flight picture). + slot_used: [bool; MAX_DPB_SLOTS], + /// Number of slots the session was created with. + slot_count: usize, + /// The next picture is the first of the stream or follows an end of + /// sequence, so only an IRAP can start decoding. + at_sequence_start: bool, + /// `NoRaslOutputFlag` of the last IRAP, which decides whether the RASL + /// pictures associated with it are decodable. + irap_no_rasl_output: bool, + /// `PicOrderCntVal` of `prevTid0Pic` (clause 8.3.1). + prev_tid0_poc: i32, +} + +impl DecodeDpb { + pub fn new(slot_count: usize, pins: Arc) -> Self { + Self { + pins, + refs: Vec::new(), + slot_used: [false; MAX_DPB_SLOTS], + slot_count: slot_count.min(MAX_DPB_SLOTS), + at_sequence_start: true, + irap_no_rasl_output: true, + prev_tid0_poc: 0, + } + } + + /// Current reference pictures, in the order they were decoded. + /// + /// After [`Self::begin_picture`] these are exactly the pictures in the + /// current reference picture set, `Foll` lists included; all of them must + /// stay active in the Vulkan session for later pictures. + pub fn references(&self) -> &[RefPicture] { + &self.refs + } + + /// Note an end of sequence NAL unit: the next picture must be an IRAP, and + /// starts a new coded video sequence with `NoRaslOutputFlag` = 1. + /// + /// Also the way to resynchronise after an error: the next CRA is then + /// treated as a random access point and its RASL pictures are skipped. + pub fn end_of_sequence(&mut self) { + self.at_sequence_start = true; + } + + /// Derive POC and the reference picture set for the picture about to be + /// decoded, and drop every reference the set does not name. + /// + /// `slice` is the header of the picture's first slice segment. Must be + /// called once per picture, before [`Self::try_allocate_slot`]; a skipped + /// picture changes no state, and needs no slot. + pub fn begin_picture( + &mut self, + nal: &NalHeader, + slice: &SliceHeader, + sps: &Sps, + ) -> Result { + let nal_type = nal.nal_type; + if !nal_type.is_vcl() || slice.dependent_slice_segment_flag { + return Err(invalid("begin_picture needs an independent slice segment")); + } + + let irap = nal_type.is_irap(); + if !irap && self.at_sequence_start { + return Ok(PictureDecision::Skip(SkipReason::AwaitingIrap)); + } + // HandleCraAsBlaFlag is 0, so a CRA starts a new sequence only where + // it is the first picture (clause 8.1.3). + let no_rasl_output = + irap && (nal_type.is_idr() || nal_type.is_bla() || self.at_sequence_start); + if irap { + self.irap_no_rasl_output = no_rasl_output; + } + if nal_type.is_rasl() && self.irap_no_rasl_output { + return Ok(PictureDecision::Skip(SkipReason::RaslWithoutReferences)); + } + self.at_sequence_start = false; + + let poc = self.derive_poc(nal, slice, sps, irap && no_rasl_output); + + if irap && no_rasl_output { + // A new coded video sequence: nothing before it is referenced. + self.drop_refs(|_| true); + } + let rps = if nal_type.is_idr() { + RefPicSet::default() + } else { + self.apply_rps(poc, slice, sps)? + }; + let missing_reference = rps.missing_current(); + + let keyframe = irap && no_rasl_output; + Ok(PictureDecision::Decode(PictureState { + poc, + nal_type, + temporal_id: nal.temporal_id, + no_rasl_output_flag: no_rasl_output, + is_keyframe: keyframe, + pic_output_flag: slice.pic_output_flag, + no_output_of_prior_pics: keyframe + && (nal_type == NalType::Cra || slice.no_output_of_prior_pics_flag), + is_reference: !(nal_type.is_sub_layer_non_reference() + && nal.temporal_id as usize >= sps.highest_tid()), + rps, + missing_reference, + })) + } + + /// Clause 8.3.1. Updates `prevTid0Pic` when the current picture qualifies. + fn derive_poc( + &mut self, + nal: &NalHeader, + slice: &SliceHeader, + sps: &Sps, + new_sequence: bool, + ) -> i32 { + let max_lsb = sps.max_pic_order_cnt_lsb() as i32; + let lsb = slice.slice_pic_order_cnt_lsb as i32; + let msb = if new_sequence { + 0 + } else { + let prev_lsb = self.prev_tid0_poc & (max_lsb - 1); + let prev_msb = self.prev_tid0_poc - prev_lsb; + if lsb < prev_lsb && prev_lsb - lsb >= max_lsb / 2 { + prev_msb + max_lsb + } else if lsb > prev_lsb && lsb - prev_lsb > max_lsb / 2 { + prev_msb - max_lsb + } else { + prev_msb + } + }; + let poc = msb + lsb; + + // prevTid0Pic: TemporalId 0 and not a RASL, RADL or sub-layer + // non-reference picture. + let t = nal.nal_type; + if nal.temporal_id == 0 && !t.is_rasl() && !t.is_radl() && !t.is_sub_layer_non_reference() { + self.prev_tid0_poc = poc; + } + poc + } + + /// Clause 8.3.2 for a non-IDR picture: build the five lists, mark the + /// long-term pictures, and drop every reference left out. + fn apply_rps(&mut self, poc: i32, slice: &SliceHeader, sps: &Sps) -> Result { + let st = &slice + .short_term_ref_pic_set(sps) + .ok_or_else(|| invalid("non-IDR picture without a short-term reference picture set"))? + .rps; + let max_lsb = sps.max_pic_order_cnt_lsb() as i32; + let mut keep = vec![false; self.refs.len()]; + let mut set = RefPicSet::default(); + + // Long-term pictures first: they may be found among the short-term + // ones, and are then no longer candidates for the short-term lists. + for lt in &slice.long_term_refs { + let mut poc_lt = lt.poc_lsb_lt as i32; + if lt.delta_poc_msb_present_flag { + poc_lt += poc + - lt.delta_poc_msb_cycle_lt_accumulated as i32 * max_lsb + - (poc & (max_lsb - 1)); + } + let found = self.refs.iter().position(|r| { + if lt.delta_poc_msb_present_flag { + r.poc == poc_lt + } else { + r.poc & (max_lsb - 1) == poc_lt + } + }); + let slot = found.map(|i| { + keep[i] = true; + self.refs[i].long_term = true; + self.refs[i].slot + }); + let entry = RpsEntry { poc: poc_lt, slot }; + if lt.used_by_curr_pic_lt { + set.lt_curr.push(entry); + } else { + set.lt_foll.push(entry); + } + } + + let short_term = |target: i32, keep: &mut [bool]| { + let found = self + .refs + .iter() + .position(|r| !r.long_term && r.poc == target); + let slot = found.map(|i| { + keep[i] = true; + self.refs[i].slot + }); + RpsEntry { poc: target, slot } + }; + for (delta, used) in st.delta_poc_s0.iter().zip(&st.used_s0) { + let entry = short_term(poc + delta, &mut keep); + if *used { + set.st_curr_before.push(entry); + } else { + set.st_foll.push(entry); + } + } + for (delta, used) in st.delta_poc_s1.iter().zip(&st.used_s1) { + let entry = short_term(poc + delta, &mut keep); + if *used { + set.st_curr_after.push(entry); + } else { + set.st_foll.push(entry); + } + } + + let mut index = 0; + self.drop_refs(|_| { + let drop_it = !keep[index]; + index += 1; + drop_it + }); + Ok(set) + } + + /// Drop every reference matching `pred`, freeing its slot. + fn drop_refs(&mut self, mut pred: impl FnMut(&RefPicture) -> bool) { + let slot_used = &mut self.slot_used; + self.refs.retain(|r| { + let drop_it = pred(r); + if drop_it { + slot_used[r.slot as usize] = false; + } + !drop_it + }); + } + + /// Reserve a DPB slot for the current picture, if one is available. + /// + /// Slots pinned by a handed-out frame are skipped: the caller is still + /// reading them. `None` means every slot is either a reference or pinned. + pub fn try_allocate_slot(&mut self) -> Option { + for slot in 0..self.slot_count { + if !self.slot_used[slot] && !self.pins.is_pinned(slot as u8) { + self.slot_used[slot] = true; + return Some(slot as u8); + } + } + None + } + + /// Whether any slot is held by a frame the caller has not dropped, which + /// means waiting could free one. + pub fn has_pinned_slots(&self) -> bool { + self.pins.any_pinned() + } + + /// Release a slot allocated for a picture that will not be kept. + pub fn release_slot(&mut self, slot: u8) { + self.slot_used[slot as usize] = false; + } + + /// Record the just-decoded picture. It becomes a short-term reference + /// (clause 8.3.2, last paragraph) unless nothing can ever reference it, in + /// which case its slot is released. + pub fn end_picture(&mut self, slot: u8, state: &PictureState) { + if !state.is_reference { + self.release_slot(slot); + return; + } + self.refs.push(RefPicture { + slot, + poc: state.poc, + long_term: false, + }); + } +} + +#[cfg(test)] +#[path = "dpb_tests.rs"] +mod tests; diff --git a/src/decoder/h265/dpb_tests.rs b/src/decoder/h265/dpb_tests.rs new file mode 100644 index 00000000..f330e665 --- /dev/null +++ b/src/decoder/h265/dpb_tests.rs @@ -0,0 +1,665 @@ +//! DPB tests on synthetic slice headers, for the cases the fixtures do not +//! reach: POC wrap-around with `prevTid0Pic`, long-term pictures, missing +//! references, BLA and end of sequence. + +use super::*; +use crate::decoder::h265::parser::DecPicBufMgr; +use crate::decoder::h265::rps::{StRefPicSet, StRps}; +use crate::decoder::h265::slice::LongTermRef; + +/// MaxPicOrderCntLsb = 16, a 6-picture DPB, `sub_layers` temporal layers. +fn sps(sub_layers: u8) -> Sps { + let mgr = DecPicBufMgr { + max_dec_pic_buffering_minus1: [5; 7], + ..Default::default() + }; + Sps { + log2_max_pic_order_cnt_lsb_minus4: 0, + sps_max_sub_layers_minus1: sub_layers - 1, + dec_pic_buf_mgr: mgr, + ..Default::default() + } +} + +fn nal(nal_type: NalType, temporal_id: u8) -> NalHeader { + NalHeader { + nal_type, + layer_id: 0, + temporal_id, + } +} + +/// A slice header with an inline short-term set of (delta, used) pairs. +fn slice(lsb: u32, s0: &[(i32, bool)], s1: &[(i32, bool)]) -> SliceHeader { + SliceHeader { + first_slice_segment_in_pic_flag: true, + pic_output_flag: true, + slice_pic_order_cnt_lsb: lsb, + st_ref_pic_set: Some(StRefPicSet { + rps: StRps { + delta_poc_s0: s0.iter().map(|e| e.0).collect(), + used_s0: s0.iter().map(|e| e.1).collect(), + delta_poc_s1: s1.iter().map(|e| e.0).collect(), + used_s1: s1.iter().map(|e| e.1).collect(), + }, + ..Default::default() + }), + ..Default::default() + } +} + +/// Begin a picture that must be decoded, allocate its slot and finish it. +fn decode( + dpb: &mut DecodeDpb, + nal: NalHeader, + slice: &SliceHeader, + sps: &Sps, +) -> (PictureState, u8) { + let state = match dpb.begin_picture(&nal, slice, sps).unwrap() { + PictureDecision::Decode(state) => state, + PictureDecision::Skip(reason) => panic!("unexpected skip: {reason:?}"), + }; + let slot = dpb.try_allocate_slot().expect("free slot"); + dpb.end_picture(slot, &state); + (state, slot) +} + +fn skip_reason(dpb: &mut DecodeDpb, nal: NalHeader, slice: &SliceHeader, sps: &Sps) -> SkipReason { + match dpb.begin_picture(&nal, slice, sps).unwrap() { + PictureDecision::Skip(reason) => reason, + PictureDecision::Decode(state) => panic!("unexpected decode of POC {}", state.poc), + } +} + +fn pocs(dpb: &DecodeDpb) -> Vec<(i32, bool)> { + dpb.references() + .iter() + .map(|r| (r.poc, r.long_term)) + .collect() +} + +fn idr() -> SliceHeader { + SliceHeader { + first_slice_segment_in_pic_flag: true, + pic_output_flag: true, + ..Default::default() + } +} + +/// The POC MSB is predicted from `prevTid0Pic` only: a picture of a higher +/// sub-layer, or a sub-layer non-reference one, sitting between two TemporalId +/// 0 pictures must not move the prediction, or the LSB wrap is misread. +#[test] +fn poc_wraps_against_prev_tid0_pic() { + for (between, tid) in [ + (NalType::TsaR, 1), + (NalType::TrailN, 0), + (NalType::RadlR, 0), + ] { + let sps = sps(2); + let mut dpb = DecodeDpb::new(6, Arc::default()); + let keep_all = |lsb, deltas: &[i32]| { + let s0: Vec<_> = deltas.iter().map(|d| (*d, true)).collect(); + slice(lsb, &s0, &[]) + }; + let mut poc = |nal_type, tid, slice: &SliceHeader| { + decode(&mut dpb, nal(nal_type, tid), slice, &sps).0.poc + }; + assert_eq!(poc(NalType::IdrNLp, 0, &idr()), 0); + assert_eq!(poc(NalType::TrailR, 0, &keep_all(8, &[-8])), 8); + // LSB 0 after 8 is a wrap: 16. + assert_eq!(poc(NalType::TrailR, 0, &keep_all(0, &[-8])), 16); + // LSB 12 is just before 16, not after it. + assert_eq!(poc(between, tid, &keep_all(12, &[-4])), 12, "{between:?}"); + // Still predicted from 16 (LSB 0): LSB 8 is 24. Predicting from 12 + // instead would give 8. + assert_eq!( + poc(NalType::TrailR, 0, &keep_all(8, &[-8])), + 24, + "{between:?}" + ); + } +} + +/// A picture with POC below the last TemporalId 0 picture's, across the +/// wrap, gets a negative MSB step. +#[test] +fn poc_steps_back_across_the_wrap() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(6, Arc::default()); + decode(&mut dpb, nal(NalType::IdrWRadl, 0), &idr(), &sps); + // A RADL picture with LSB 15 right after an IDR is POC -1. + let (state, _) = decode( + &mut dpb, + nal(NalType::RadlN, 0), + &slice(15, &[], &[(1, true)]), + &sps, + ); + assert_eq!(state.poc, -1); + assert!(!state.missing_reference); +} + +/// The set names what stays: pictures left out are dropped before the current +/// picture is decoded, and their slots come free for it. +#[test] +fn rps_keeps_what_it_names_and_maps_slots() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(3, Arc::default()); + let (_, slot0) = decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + let (s1, slot1) = decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(1, &[(-1, true)], &[]), + &sps, + ); + assert_eq!( + s1.rps.st_curr_before, + vec![RpsEntry { + poc: 0, + slot: Some(slot0) + }] + ); + + // POC 2 uses 1 and keeps 0 for later. + let (s2, slot2) = decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(2, &[(-1, true), (-2, false)], &[]), + &sps, + ); + assert_eq!( + s2.rps.st_curr_before, + vec![RpsEntry { + poc: 1, + slot: Some(slot1) + }] + ); + assert_eq!( + s2.rps.st_foll, + vec![RpsEntry { + poc: 0, + slot: Some(slot0) + }] + ); + assert_eq!(pocs(&dpb), vec![(0, false), (1, false), (2, false)]); + + // All three slots are taken. POC 3 keeps only 2, so 0 and 1 go before it + // needs a slot, and it gets one of theirs. + let (s3, slot3) = decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(3, &[(-1, true)], &[]), + &sps, + ); + assert_eq!( + s3.rps.st_curr_before, + vec![RpsEntry { + poc: 2, + slot: Some(slot2) + }] + ); + assert!(slot3 == slot0 || slot3 == slot1); + assert_eq!(pocs(&dpb), vec![(2, false), (3, false)]); + + // A B picture between them: StCurrAfter. + let (b, _) = decode( + &mut dpb, + nal(NalType::TrailN, 0), + &slice(5, &[(-2, true)], &[(1, false)]), + &sps, + ); + assert_eq!(b.rps.st_curr_before[0].poc, 3); + assert_eq!(b.rps.st_foll[0].poc, 6); + assert!(b.rps.st_foll[0].slot.is_none(), "POC 6 is not decoded yet"); + assert!(!b.missing_reference, "a missing Foll entry is harmless"); +} + +/// A picture predicting from something the DPB does not hold says so. +#[test] +fn missing_reference_is_reported() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(4, Arc::default()); + decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + let (state, _) = decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(4, &[(-2, true), (-4, true)], &[]), + &sps, + ); + assert!(state.missing_reference); + assert_eq!(state.rps.st_curr_before[0], RpsEntry { poc: 2, slot: None }); + assert_eq!(state.rps.st_curr_before[1].poc, 0); + assert!(state.rps.st_curr_before[1].slot.is_some()); +} + +/// Long-term entries pick a picture by LSB (or by full POC with the MSB +/// present), mark it long-term, take it out of reach of the short-term lists, +/// and drop it once no set names it. +#[test] +fn long_term_pictures() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(6, Arc::default()); + let lt = |poc_lsb_lt, used, msb: Option| LongTermRef { + poc_lsb_lt, + used_by_curr_pic_lt: used, + delta_poc_msb_present_flag: msb.is_some(), + delta_poc_msb_cycle_lt: msb.unwrap_or(0), + delta_poc_msb_cycle_lt_accumulated: msb.unwrap_or(0), + lt_idx_sps: None, + }; + let with_lt = |lsb, s0: &[(i32, bool)], lts: Vec| SliceHeader { + long_term_refs: lts, + ..slice(lsb, s0, &[]) + }; + + let (_, slot0) = decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(1, &[(-1, true)], &[]), + &sps, + ); + let (s2, _) = decode( + &mut dpb, + nal(NalType::TrailR, 0), + &with_lt(2, &[(-1, true)], vec![lt(0, true, None)]), + &sps, + ); + assert_eq!( + s2.rps.lt_curr, + vec![RpsEntry { + poc: 0, + slot: Some(slot0) + }] + ); + assert_eq!(pocs(&dpb), vec![(0, true), (1, false), (2, false)]); + + // Kept for later only; and by full POC with the MSB (cycle 0 is this + // picture's MSB, so POC 0 itself). + let (s3, _) = decode( + &mut dpb, + nal(NalType::TrailR, 0), + &with_lt(3, &[(-1, true)], vec![lt(0, false, Some(0))]), + &sps, + ); + assert_eq!( + s3.rps.lt_foll, + vec![RpsEntry { + poc: 0, + slot: Some(slot0) + }] + ); + assert!(s3.rps.lt_curr.is_empty()); + + // A long-term picture is not a short-term candidate any more. + let (s4, _) = decode( + &mut dpb, + nal(NalType::TrailR, 0), + &with_lt(4, &[(-1, true), (-4, true)], vec![lt(0, false, None)]), + &sps, + ); + assert_eq!(s4.rps.st_curr_before[1], RpsEntry { poc: 0, slot: None }); + assert!(s4.missing_reference); + + // Named by nothing: gone, slot free. + decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(5, &[(-1, true)], &[]), + &sps, + ); + assert!(dpb.references().iter().all(|r| r.poc != 0 && !r.long_term)); +} + +/// The MSB cycle reaches a long-term picture a whole LSB period back. +#[test] +fn long_term_msb_cycle() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(6, Arc::default()); + let (_, slot0) = decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + let keep0 = |lsb| SliceHeader { + long_term_refs: vec![LongTermRef { + poc_lsb_lt: 0, + used_by_curr_pic_lt: true, + delta_poc_msb_present_flag: true, + delta_poc_msb_cycle_lt: 1, + delta_poc_msb_cycle_lt_accumulated: 1, + lt_idx_sps: None, + }], + ..slice(lsb, &[], &[]) + }; + let lt_only = |lsb| SliceHeader { + long_term_refs: vec![LongTermRef { + poc_lsb_lt: 0, + used_by_curr_pic_lt: false, + delta_poc_msb_present_flag: false, + ..Default::default() + }], + ..slice(lsb, &[], &[]) + }; + // Walk up to POC 16 keeping POC 0 long-term by LSB. + for lsb in [4, 8, 12] { + decode(&mut dpb, nal(NalType::TrailR, 0), <_only(lsb), &sps); + } + // POC 16 has LSB 0, as POC 0 does; MSB cycle 1 makes it unambiguous. + let (state, _) = decode(&mut dpb, nal(NalType::TrailR, 0), &keep0(0), &sps); + assert_eq!(state.poc, 16); + assert_eq!( + state.rps.lt_curr, + vec![RpsEntry { + poc: 0, + slot: Some(slot0) + }] + ); +} + +/// A stream starting at a CRA: RASL pictures are skipped, RADL ones decode. +#[test] +fn cra_first_skips_rasl() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(6, Arc::default()); + assert_eq!( + skip_reason(&mut dpb, nal(NalType::TrailR, 0), &slice(3, &[], &[]), &sps), + SkipReason::AwaitingIrap + ); + // The CRA's own set keeps an earlier picture for its RASL pictures. + let (cra, cra_slot) = decode( + &mut dpb, + nal(NalType::Cra, 0), + &slice(8, &[(-2, false)], &[]), + &sps, + ); + assert_eq!(cra.poc, 8, "the MSB starts at 0"); + assert!(cra.is_keyframe && cra.no_rasl_output_flag); + assert!(cra.no_output_of_prior_pics, "inferred for a CRA"); + assert!(!cra.missing_reference); + assert_eq!( + skip_reason( + &mut dpb, + nal(NalType::RaslN, 0), + &slice(6, &[(-2, true)], &[(2, true)]), + &sps + ), + SkipReason::RaslWithoutReferences + ); + let (radl, _) = decode( + &mut dpb, + nal(NalType::RadlN, 0), + &slice(7, &[], &[(1, true)]), + &sps, + ); + assert_eq!(radl.poc, 7); + assert_eq!( + radl.rps.st_curr_after, + vec![RpsEntry { + poc: 8, + slot: Some(cra_slot) + }] + ); + let (trail, _) = decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(9, &[(-1, true)], &[]), + &sps, + ); + assert!(!trail.is_keyframe && !trail.missing_reference); +} + +/// A CRA mid-stream is an ordinary picture, and its RASL pictures decode. +#[test] +fn cra_mid_stream_keeps_rasl() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(6, Arc::default()); + decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(2, &[(-2, true)], &[]), + &sps, + ); + let (cra, _) = decode( + &mut dpb, + nal(NalType::Cra, 0), + &slice(4, &[(-2, false)], &[]), + &sps, + ); + assert!(!cra.is_keyframe && !cra.no_rasl_output_flag); + let (rasl, _) = decode( + &mut dpb, + nal(NalType::RaslN, 0), + &slice(3, &[(-1, true)], &[(1, true)]), + &sps, + ); + assert_eq!(rasl.poc, 3); + assert!(!rasl.missing_reference); +} + +/// After an end of sequence the next picture must be an IRAP, which starts +/// over: MSB 0, nothing kept, RASL pictures skipped. +#[test] +fn end_of_sequence_restarts() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(6, Arc::default()); + decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + for lsb in [6u32, 12, 2] { + decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(lsb, &[(-6, true)], &[]), + &sps, + ); + } + assert_eq!(pocs(&dpb).last(), Some(&(18, false))); + + dpb.end_of_sequence(); + assert_eq!( + skip_reason( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(4, &[(-1, true)], &[]), + &sps + ), + SkipReason::AwaitingIrap + ); + let (cra, _) = decode( + &mut dpb, + nal(NalType::Cra, 0), + &slice(4, &[(-2, false)], &[]), + &sps, + ); + assert_eq!(cra.poc, 4, "not 20: the MSB restarts"); + assert!(cra.is_keyframe); + assert_eq!(pocs(&dpb), vec![(4, false)]); + assert_eq!( + skip_reason( + &mut dpb, + nal(NalType::RaslR, 0), + &slice(3, &[(-1, true)], &[(1, true)]), + &sps + ), + SkipReason::RaslWithoutReferences + ); +} + +/// A BLA picture starts a new sequence wherever it is. +#[test] +fn bla_starts_a_new_sequence() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(6, Arc::default()); + decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(5, &[(-5, true)], &[]), + &sps, + ); + let mut header = slice(9, &[(-4, false)], &[]); + header.no_output_of_prior_pics_flag = false; + let (bla, _) = decode(&mut dpb, nal(NalType::BlaWLp, 0), &header, &sps); + assert_eq!(bla.poc, 9); + assert!(bla.is_keyframe); + assert!( + !bla.no_output_of_prior_pics, + "taken from the flag for a BLA" + ); + assert_eq!(bla.rps.st_foll, vec![RpsEntry { poc: 5, slot: None }]); + assert_eq!(pocs(&dpb), vec![(9, false)]); + assert_eq!( + skip_reason( + &mut dpb, + nal(NalType::RaslN, 0), + &slice(8, &[], &[(1, true)]), + &sps + ), + SkipReason::RaslWithoutReferences + ); +} + +/// An IDR clears the DPB; its own reference picture set is empty. +#[test] +fn idr_clears_everything() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(3, Arc::default()); + decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + decode( + &mut dpb, + nal(NalType::TrailR, 0), + &slice(1, &[(-1, true)], &[]), + &sps, + ); + let mut header = idr(); + header.no_output_of_prior_pics_flag = true; + let (state, slot) = decode(&mut dpb, nal(NalType::IdrWRadl, 0), &header, &sps); + assert_eq!(state.poc, 0); + assert_eq!(state.rps, RefPicSet::default()); + assert!(state.no_output_of_prior_pics); + assert_eq!(slot, 0, "every slot was freed"); + assert_eq!(pocs(&dpb), vec![(0, false)]); +} + +/// A sub-layer non-reference picture of the highest sub-layer is never +/// referenced, so its slot is released at once; in a lower sub-layer it is +/// kept, since higher sub-layers may predict from it. +#[test] +fn sub_layer_non_reference_pictures() { + let one_layer = sps(1); + let mut dpb = DecodeDpb::new(4, Arc::default()); + decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &one_layer); + let (state, slot) = decode( + &mut dpb, + nal(NalType::TrailN, 0), + &slice(1, &[(-1, true)], &[]), + &one_layer, + ); + assert!(!state.is_reference); + assert_eq!(dpb.try_allocate_slot(), Some(slot), "released"); + + let two_layers = sps(2); + let mut dpb = DecodeDpb::new(4, Arc::default()); + decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &two_layers); + let (state, _) = decode( + &mut dpb, + nal(NalType::TrailN, 0), + &slice(2, &[(-2, true)], &[]), + &two_layers, + ); + assert!(state.is_reference); + let (state, _) = decode( + &mut dpb, + nal(NalType::TsaN, 1), + &slice(1, &[(-1, true)], &[(1, true)]), + &two_layers, + ); + assert!(!state.is_reference); + assert!(!state.missing_reference); +} + +/// pic_output_flag passes through. +#[test] +fn pic_output_flag_passes_through() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(4, Arc::default()); + decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + let mut header = slice(1, &[(-1, true)], &[]); + header.pic_output_flag = false; + let (state, _) = decode(&mut dpb, nal(NalType::TrailR, 0), &header, &sps); + assert!(!state.pic_output_flag); +} + +#[test] +fn vulkan_lists_are_padded() { + let list = [ + RpsEntry { + poc: 1, + slot: Some(3), + }, + RpsEntry { poc: 2, slot: None }, + ]; + let out = RefPicSet::vulkan_list(&list).unwrap(); + assert_eq!( + out, + [3, NO_REFERENCE_PICTURE, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF] + ); + let too_long = [RpsEntry { + poc: 0, + slot: Some(0), + }; 9]; + assert!(RefPicSet::vulkan_list(&too_long).is_err()); +} + +/// A slot a handed-out frame is reading must not be decoded over, even once +/// the reference picture set has dropped it. +#[test] +fn pinned_slots_are_not_reallocated() { + let sps = sps(1); + let pins = Arc::new(SlotPins::default()); + let mut dpb = DecodeDpb::new(2, pins.clone()); + let (_, idr_slot) = decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + pins.pin(idr_slot); + assert!(dpb.has_pinned_slots()); + + // POC 1 drops the IDR, but the caller still holds it. + let (_, slot1) = decode(&mut dpb, nal(NalType::TrailR, 0), &slice(1, &[], &[]), &sps); + assert_ne!(slot1, idr_slot); + let state = match dpb + .begin_picture( + &nal(NalType::TrailR, 0), + &slice(2, &[(-1, true)], &[]), + &sps, + ) + .unwrap() + { + PictureDecision::Decode(state) => state, + PictureDecision::Skip(_) => unreachable!(), + }; + assert_eq!(dpb.try_allocate_slot(), None, "one reference, one pinned"); + + pins.release(idr_slot); + assert!(!dpb.has_pinned_slots()); + let slot = dpb.try_allocate_slot().unwrap(); + assert_eq!(slot, idr_slot); + dpb.end_picture(slot, &state); +} + +/// Malformed input is an error, not a panic. +#[test] +fn rejects_bad_input() { + let sps = sps(1); + let mut dpb = DecodeDpb::new(4, Arc::default()); + decode(&mut dpb, nal(NalType::IdrNLp, 0), &idr(), &sps); + // A non-IDR picture without any short-term set. + let header = SliceHeader { + slice_pic_order_cnt_lsb: 1, + first_slice_segment_in_pic_flag: true, + ..Default::default() + }; + assert!( + dpb.begin_picture(&nal(NalType::TrailR, 0), &header, &sps) + .is_err() + ); + // Not a slice. + assert!( + dpb.begin_picture(&nal(NalType::Sps, 0), &idr(), &sps) + .is_err() + ); +} diff --git a/src/decoder/h265/mod.rs b/src/decoder/h265/mod.rs new file mode 100644 index 00000000..4f17e859 --- /dev/null +++ b/src/decoder/h265/mod.rs @@ -0,0 +1,29 @@ +//! H.265 decoding: the host-side syntax parsing and reference bookkeeping. +//! +//! - [`parser`]: NAL units, the shared bit reader, `profile_tier_level()`, +//! HRD skipping and the VPS. +//! - [`sps`], [`pps`]: the parameter sets, with the scaling lists ([`scaling`]) +//! and short-term reference picture sets ([`rps`]) they contain. +//! - [`slice`]: the slice segment header, up to the reference syntax. +//! - [`dpb`]: picture order count, the reference picture set, random access +//! and DPB slots. +//! +//! None of this touches Vulkan; the session code fills the `StdVideoH265*` +//! structures from these types, whose field names follow the standard. + +pub(crate) mod dpb; +pub(crate) mod parser; +pub(crate) mod pps; +pub(crate) mod rps; +pub(crate) mod scaling; +pub(crate) mod slice; +pub(crate) mod sps; + +/// Parser and DPB tests against real x265 and NVENC streams (host-side, no +/// GPU), compiled only under `cargo test`. +#[cfg(test)] +mod parser_tests; + +/// Hand-written slice header syntax the fixtures do not cover. +#[cfg(test)] +mod syntax_tests; diff --git a/src/decoder/h265/parser.rs b/src/decoder/h265/parser.rs new file mode 100644 index 00000000..08beda7f --- /dev/null +++ b/src/decoder/h265/parser.rs @@ -0,0 +1,742 @@ +//! H.265 Annex B parsing: NAL unit splitting and headers, the bit reader the +//! other syntax parsers share, `profile_tier_level()`, `hrd_parameters()` and +//! the video parameter set. +//! +//! The SPS, PPS and slice segment header live in their own modules +//! ([`super::sps`], [`super::pps`], [`super::slice`]); this one holds what they +//! have in common. +//! +//! As with H.264, this is not a full parser. Vulkan Video consumes the raw +//! slice segment NAL units and parses the slice data in the driver; the host +//! needs only enough syntax to fill the `StdVideoH265*` parameter sets, derive +//! the picture order count and drive the reference picture set. + +use crate::decoder::bitreader::BitReader; +use crate::error::{PixelForgeError, Result}; + +/// Highest number of temporal sub-layers (`sps_max_sub_layers_minus1` is at +/// most 6). +pub(crate) const MAX_SUB_LAYERS: usize = 7; + +/// Build the error every parser in this module returns for bad syntax. +pub(crate) fn invalid(message: impl std::fmt::Display) -> PixelForgeError { + PixelForgeError::InvalidInput(format!("H.265: {message}")) +} + +/// `Ceil(Log2(x))`, the width of the fixed-length index fields. +pub(crate) fn ceil_log2(x: u32) -> u32 { + if x <= 1 { + 0 + } else { + 32 - (x - 1).leading_zeros() + } +} + +/// H.265 NAL unit types (Table 7-1). +/// +/// The reserved and unspecified values are kept as [`NalType::Other`] with +/// their raw value, so the range helpers still classify them correctly. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum NalType { + /// 0: trailing picture, sub-layer non-reference. + TrailN, + /// 1: trailing picture, sub-layer reference. + TrailR, + /// 2: temporal sub-layer access, sub-layer non-reference. + TsaN, + /// 3: temporal sub-layer access, sub-layer reference. + TsaR, + /// 4: step-wise temporal sub-layer access, sub-layer non-reference. + StsaN, + /// 5: step-wise temporal sub-layer access, sub-layer reference. + StsaR, + /// 6: random access decodable leading picture, sub-layer non-reference. + RadlN, + /// 7: random access decodable leading picture, sub-layer reference. + RadlR, + /// 8: random access skipped leading picture, sub-layer non-reference. + RaslN, + /// 9: random access skipped leading picture, sub-layer reference. + RaslR, + /// 16: broken link access, may have RASL and RADL leading pictures. + BlaWLp, + /// 17: broken link access, may have RADL leading pictures only. + BlaWRadl, + /// 18: broken link access, no leading pictures. + BlaNLp, + /// 19: instantaneous decoding refresh, may have RADL leading pictures. + IdrWRadl, + /// 20: instantaneous decoding refresh, no leading pictures. + IdrNLp, + /// 21: clean random access. + Cra, + /// 32: video parameter set. + Vps, + /// 33: sequence parameter set. + Sps, + /// 34: picture parameter set. + Pps, + /// 35: access unit delimiter. + Aud, + /// 36: end of sequence. + Eos, + /// 37: end of bitstream. + Eob, + /// 38: filler data. + Fd, + /// 39: prefix supplemental enhancement information. + SeiPrefix, + /// 40: suffix supplemental enhancement information. + SeiSuffix, + /// Any reserved or unspecified type, by raw value. + Other(u8), +} + +impl NalType { + pub fn from_raw(raw: u8) -> Self { + match raw { + 0 => NalType::TrailN, + 1 => NalType::TrailR, + 2 => NalType::TsaN, + 3 => NalType::TsaR, + 4 => NalType::StsaN, + 5 => NalType::StsaR, + 6 => NalType::RadlN, + 7 => NalType::RadlR, + 8 => NalType::RaslN, + 9 => NalType::RaslR, + 16 => NalType::BlaWLp, + 17 => NalType::BlaWRadl, + 18 => NalType::BlaNLp, + 19 => NalType::IdrWRadl, + 20 => NalType::IdrNLp, + 21 => NalType::Cra, + 32 => NalType::Vps, + 33 => NalType::Sps, + 34 => NalType::Pps, + 35 => NalType::Aud, + 36 => NalType::Eos, + 37 => NalType::Eob, + 38 => NalType::Fd, + 39 => NalType::SeiPrefix, + 40 => NalType::SeiSuffix, + other => NalType::Other(other), + } + } + + /// The `nal_unit_type` value. + pub fn raw(self) -> u8 { + match self { + NalType::TrailN => 0, + NalType::TrailR => 1, + NalType::TsaN => 2, + NalType::TsaR => 3, + NalType::StsaN => 4, + NalType::StsaR => 5, + NalType::RadlN => 6, + NalType::RadlR => 7, + NalType::RaslN => 8, + NalType::RaslR => 9, + NalType::BlaWLp => 16, + NalType::BlaWRadl => 17, + NalType::BlaNLp => 18, + NalType::IdrWRadl => 19, + NalType::IdrNLp => 20, + NalType::Cra => 21, + NalType::Vps => 32, + NalType::Sps => 33, + NalType::Pps => 34, + NalType::Aud => 35, + NalType::Eos => 36, + NalType::Eob => 37, + NalType::Fd => 38, + NalType::SeiPrefix => 39, + NalType::SeiSuffix => 40, + NalType::Other(raw) => raw, + } + } + + /// A coded slice segment (types 0..=31), including the reserved ones. + pub fn is_vcl(self) -> bool { + self.raw() < 32 + } + + /// An intra random access point picture: BLA, IDR, CRA or a reserved IRAP + /// type (16..=23). + pub fn is_irap(self) -> bool { + (16..=23).contains(&self.raw()) + } + + /// An IDR picture, with or without RADL leading pictures. + pub fn is_idr(self) -> bool { + matches!(self, NalType::IdrWRadl | NalType::IdrNLp) + } + + /// A broken link access picture. + pub fn is_bla(self) -> bool { + matches!(self, NalType::BlaWLp | NalType::BlaWRadl | NalType::BlaNLp) + } + + /// A random access skipped leading picture. + pub fn is_rasl(self) -> bool { + matches!(self, NalType::RaslN | NalType::RaslR) + } + + /// A random access decodable leading picture. + pub fn is_radl(self) -> bool { + matches!(self, NalType::RadlN | NalType::RadlR) + } + + /// A sub-layer non-reference picture: the even types up to 14 + /// (`TRAIL_N`, `TSA_N`, `STSA_N`, `RADL_N`, `RASL_N` and the reserved + /// `RSV_VCL_N10/12/14`). + /// + /// Such a picture is never referenced by a picture of the same temporal + /// sub-layer, but a higher sub-layer may still reference it. + pub fn is_sub_layer_non_reference(self) -> bool { + let raw = self.raw(); + raw <= 14 && raw.is_multiple_of(2) + } +} + +/// The two-byte NAL unit header (clause 7.3.1.2). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct NalHeader { + pub nal_type: NalType, + /// `nuh_layer_id`. Anything above 0 belongs to a layer this decoder does + /// not decode (scalable or multiview extensions) and should be dropped. + pub layer_id: u8, + /// `TemporalId`, which is `nuh_temporal_id_plus1 - 1`. + pub temporal_id: u8, +} + +/// Parse the NAL unit header from the bytes after the start code. +pub(crate) fn parse_nal_header(nal: &[u8]) -> Result { + if nal.len() < 2 { + return Err(invalid("NAL unit shorter than its header")); + } + if nal[0] & 0x80 != 0 { + return Err(invalid("forbidden_zero_bit is set")); + } + let temporal_id_plus1 = nal[1] & 0x7; + if temporal_id_plus1 == 0 { + return Err(invalid("nuh_temporal_id_plus1 is 0")); + } + Ok(NalHeader { + nal_type: NalType::from_raw((nal[0] >> 1) & 0x3F), + layer_id: ((nal[0] & 1) << 5) | (nal[1] >> 3), + temporal_id: temporal_id_plus1 - 1, + }) +} + +/// A single NAL unit within an Annex B stream. +#[derive(Debug, Clone, Copy)] +pub(crate) struct NalUnit<'a> { + pub header: NalHeader, + /// The complete NAL unit (2-byte header + EBSP payload), without start code. + pub data: &'a [u8], +} + +impl<'a> NalUnit<'a> { + pub fn nal_type(&self) -> NalType { + self.header.nal_type + } + + /// EBSP payload after the 2-byte NAL header. + pub fn payload(&self) -> &'a [u8] { + &self.data[2..] + } +} + +/// Iterate over NAL units in an Annex B byte stream (3- or 4-byte start codes). +/// +/// NAL units whose header is malformed (too short, forbidden bit set) are +/// skipped rather than ending the iteration. +pub(crate) fn iter_nal_units(data: &[u8]) -> impl Iterator> { + iter_nal_units_with_offsets(data).map(|(_, nal)| nal) +} + +/// Like [`iter_nal_units`], but also yields each NAL's start-code offset within +/// `data`. +pub(crate) fn iter_nal_units_with_offsets( + data: &[u8], +) -> impl Iterator)> { + NalIterator { data, pos: 0 } +} + +struct NalIterator<'a> { + data: &'a [u8], + pos: usize, +} + +/// Find the next start code at or after `from`. Returns (start_code_pos, payload_pos). +fn find_start_code(data: &[u8], from: usize) -> Option<(usize, usize)> { + let mut i = from; + while i + 3 <= data.len() { + if data[i] == 0 && data[i + 1] == 0 { + if data[i + 2] == 1 { + return Some((i, i + 3)); + } + if i + 4 <= data.len() && data[i + 2] == 0 && data[i + 3] == 1 { + return Some((i, i + 4)); + } + } + i += 1; + } + None +} + +impl<'a> Iterator for NalIterator<'a> { + type Item = (usize, NalUnit<'a>); + + fn next(&mut self) -> Option<(usize, NalUnit<'a>)> { + loop { + let (start_code_pos, payload_start) = find_start_code(self.data, self.pos)?; + let end = match find_start_code(self.data, payload_start) { + Some((next_start, _)) => next_start, + None => self.data.len(), + }; + self.pos = end; + + // Trim trailing zero padding before the next start code. + let mut trimmed_end = end; + while trimmed_end > payload_start && self.data[trimmed_end - 1] == 0 { + trimmed_end -= 1; + } + let nal = &self.data[payload_start..trimmed_end]; + if let Ok(header) = parse_nal_header(nal) { + return Some((start_code_pos, NalUnit { header, data: nal })); + } + } + } +} + +/// [`BitReader`] plus a count of the RBSP bits consumed. +/// +/// The count excludes emulation prevention bytes, which is what Vulkan's +/// `NumBitsForSTRefPicSetInSlice` is measured in. `ue(v)`/`se(v)` are +/// reimplemented here on top of [`BitReader::bit`] so every bit is counted. +pub(crate) struct Reader<'a> { + inner: BitReader<'a>, + position: usize, +} + +impl<'a> Reader<'a> { + pub fn new(payload: &'a [u8]) -> Self { + Self { + inner: BitReader::new(payload), + position: 0, + } + } + + /// RBSP bits consumed so far. + pub fn position(&self) -> usize { + self.position + } + + pub fn bit(&mut self) -> Result { + let bit = self.inner.bit()?; + self.position += 1; + Ok(bit) + } + + /// `u(n)`, n <= 32. + pub fn bits(&mut self, n: u32) -> Result { + debug_assert!(n <= 32); + let mut v = 0u64; + for _ in 0..n { + v = (v << 1) | self.bit()? as u64; + } + Ok(v as u32) + } + + /// `u(1)` as a boolean. + pub fn flag(&mut self) -> Result { + Ok(self.bit()? != 0) + } + + /// Skip `n` bits. + pub fn skip(&mut self, n: u32) -> Result<()> { + for _ in 0..n { + self.bit()?; + } + Ok(()) + } + + /// `ue(v)`. + pub fn ue(&mut self) -> Result { + let mut leading_zeros = 0u32; + while self.bit()? == 0 { + leading_zeros += 1; + if leading_zeros > 31 { + return Err(invalid("invalid Exp-Golomb code")); + } + } + if leading_zeros == 0 { + return Ok(0); + } + let suffix = self.bits(leading_zeros)? as u64; + Ok(((1u64 << leading_zeros) - 1 + suffix) as u32) + } + + /// `ue(v)` that must not exceed `max`; `what` names the syntax element. + pub fn ue_max(&mut self, max: u32, what: &str) -> Result { + let v = self.ue()?; + if v > max { + return Err(invalid(format_args!("{what} {v} exceeds {max}"))); + } + Ok(v) + } + + /// `se(v)`. + pub fn se(&mut self) -> Result { + let code = self.ue()? as i64; + // Mapping: 0 -> 0, 1 -> 1, 2 -> -1, 3 -> 2, 4 -> -2, ... + let value = (code + 1) / 2; + Ok(if code % 2 == 0 { -value } else { value } as i32) + } + + /// `se(v)` that must lie within `min..=max`. + pub fn se_range(&mut self, min: i32, max: i32, what: &str) -> Result { + let v = self.se()?; + if v < min || v > max { + return Err(invalid(format_args!("{what} {v} outside {min}..={max}"))); + } + Ok(v) + } +} + +/// The general part of `profile_tier_level()` (clause 7.3.3), which is what +/// `StdVideoH265ProfileTierLevel` carries. Sub-layer profiles and levels are +/// parsed past but not kept. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct ProfileTierLevel { + pub general_profile_space: u8, + pub general_tier_flag: bool, + /// 1 = Main, 2 = Main 10, 3 = Main Still Picture, 4 = format range + /// extensions, and so on. + pub general_profile_idc: u8, + /// `general_profile_compatibility_flag[j]`, bit 31 - j. + pub general_profile_compatibility_flags: u32, + pub general_progressive_source_flag: bool, + pub general_interlaced_source_flag: bool, + pub general_non_packed_constraint_flag: bool, + pub general_frame_only_constraint_flag: bool, + /// Raw `general_level_idc`: 30 times the level number (level 4.1 is 123). + /// `StdVideoH265LevelIdc` is an enumeration instead, so it needs mapping. + pub general_level_idc: u8, +} + +/// Parse `profile_tier_level(1, max_sub_layers_minus1)`. +pub(crate) fn parse_profile_tier_level( + r: &mut Reader, + max_sub_layers_minus1: u8, +) -> Result { + let mut ptl = ProfileTierLevel { + general_profile_space: r.bits(2)? as u8, + general_tier_flag: r.flag()?, + general_profile_idc: r.bits(5)? as u8, + general_profile_compatibility_flags: r.bits(32)?, + general_progressive_source_flag: r.flag()?, + general_interlaced_source_flag: r.flag()?, + general_non_packed_constraint_flag: r.flag()?, + general_frame_only_constraint_flag: r.flag()?, + ..Default::default() + }; + // 43 bits of profile-specific constraint flags, then general_inbld_flag + // (or a reserved bit). None of them matter to the decoder. + r.skip(44)?; + ptl.general_level_idc = r.bits(8)? as u8; + + let sub_layers = max_sub_layers_minus1 as usize; + let mut profile_present = [false; MAX_SUB_LAYERS]; + let mut level_present = [false; MAX_SUB_LAYERS]; + for i in 0..sub_layers { + profile_present[i] = r.flag()?; + level_present[i] = r.flag()?; + } + if sub_layers > 0 { + // reserved_zero_2bits for each unused sub-layer slot up to 8. + r.skip(2 * (8 - sub_layers as u32))?; + } + for i in 0..sub_layers { + if profile_present[i] { + // Profile space, tier, profile idc, 32 compatibility flags, the + // four source flags and 44 constraint/reserved bits. + r.skip(2 + 1 + 5 + 32 + 4 + 44)?; + } + if level_present[i] { + r.skip(8)?; + } + } + Ok(ptl) +} + +/// Per-sub-layer DPB sizing, as `StdVideoH265DecPicBufMgr` carries it. +/// +/// Every entry is filled: when the bitstream only codes the highest +/// sub-layer (`*_sub_layer_ordering_info_present_flag` = 0), the lower ones +/// are inferred equal to it (clause 7.4.3.2.1). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct DecPicBufMgr { + pub max_dec_pic_buffering_minus1: [u8; MAX_SUB_LAYERS], + pub max_num_reorder_pics: [u8; MAX_SUB_LAYERS], + pub max_latency_increase_plus1: [u32; MAX_SUB_LAYERS], +} + +/// Parse the sub-layer ordering loop shared by the VPS and SPS. +pub(crate) fn parse_dec_pic_buf_mgr( + r: &mut Reader, + max_sub_layers_minus1: u8, + info_present: bool, +) -> Result { + let mut mgr = DecPicBufMgr::default(); + let highest = max_sub_layers_minus1 as usize; + let first = if info_present { 0 } else { highest }; + for i in first..=highest { + // MaxDpbSize is at most 16, so max_dec_pic_buffering_minus1 <= 15. + mgr.max_dec_pic_buffering_minus1[i] = r.ue_max(15, "max_dec_pic_buffering_minus1")? as u8; + mgr.max_num_reorder_pics[i] = r.ue_max( + mgr.max_dec_pic_buffering_minus1[i] as u32, + "max_num_reorder_pics", + )? as u8; + mgr.max_latency_increase_plus1[i] = r.ue()?; + } + for i in 0..first { + mgr.max_dec_pic_buffering_minus1[i] = mgr.max_dec_pic_buffering_minus1[highest]; + mgr.max_num_reorder_pics[i] = mgr.max_num_reorder_pics[highest]; + mgr.max_latency_increase_plus1[i] = mgr.max_latency_increase_plus1[highest]; + } + Ok(mgr) +} + +/// Parse `hrd_parameters()` (clause E.2.2) only to advance past it. +/// +/// Vulkan decode does not use HRD information, and it is large, so nothing is +/// kept; but it sits in the middle of the VPS and the VUI, so it has to be +/// parsed exactly. +pub(crate) fn skip_hrd_parameters( + r: &mut Reader, + common_inf_present: bool, + max_sub_layers_minus1: u8, +) -> Result<()> { + let mut nal_hrd = false; + let mut vcl_hrd = false; + let mut sub_pic_hrd_params_present = false; + if common_inf_present { + nal_hrd = r.flag()?; + vcl_hrd = r.flag()?; + if nal_hrd || vcl_hrd { + sub_pic_hrd_params_present = r.flag()?; + if sub_pic_hrd_params_present { + // tick_divisor_minus2, du_cpb_removal_delay_increment_length_minus1, + // sub_pic_cpb_params_in_pic_timing_sei_flag, + // dpb_output_delay_du_length_minus1. + r.skip(8 + 5 + 1 + 5)?; + } + // bit_rate_scale, cpb_size_scale. + r.skip(4 + 4)?; + if sub_pic_hrd_params_present { + // cpb_size_du_scale. + r.skip(4)?; + } + // initial_cpb_removal_delay_length_minus1, + // au_cpb_removal_delay_length_minus1, dpb_output_delay_length_minus1. + r.skip(5 + 5 + 5)?; + } + } + + for _ in 0..=max_sub_layers_minus1 { + let fixed_pic_rate_general = r.flag()?; + // Inferred to be 1 when fixed_pic_rate_general_flag is 1. + let fixed_pic_rate_within_cvs = fixed_pic_rate_general || r.flag()?; + let mut low_delay_hrd = false; + if fixed_pic_rate_within_cvs { + let _elemental_duration_in_tc_minus1 = r.ue()?; + } else { + low_delay_hrd = r.flag()?; + } + let mut cpb_cnt_minus1 = 0; + if !low_delay_hrd { + cpb_cnt_minus1 = r.ue_max(31, "cpb_cnt_minus1")?; + } + for present in [nal_hrd, vcl_hrd] { + if !present { + continue; + } + // sub_layer_hrd_parameters(). + for _ in 0..=cpb_cnt_minus1 { + let _bit_rate_value_minus1 = r.ue()?; + let _cpb_size_value_minus1 = r.ue()?; + if sub_pic_hrd_params_present { + let _cpb_size_du_value_minus1 = r.ue()?; + let _bit_rate_du_value_minus1 = r.ue()?; + } + let _cbr_flag = r.flag()?; + } + } + } + Ok(()) +} + +/// Timing information from a VPS (`vps_timing_info_present_flag`). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct VpsTiming { + pub num_units_in_tick: u32, + pub time_scale: u32, + pub poc_proportional_to_timing_flag: bool, + pub num_ticks_poc_diff_one_minus1: u32, +} + +/// Parsed video parameter set (clause 7.3.2.1): the fields +/// `StdVideoH265VideoParameterSet` needs. HRD parameters are parsed past and +/// dropped, and the VPS extension is not parsed. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct Vps { + pub vps_video_parameter_set_id: u8, + pub vps_base_layer_internal_flag: bool, + pub vps_base_layer_available_flag: bool, + pub vps_max_layers_minus1: u8, + pub vps_max_sub_layers_minus1: u8, + pub vps_temporal_id_nesting_flag: bool, + pub profile_tier_level: ProfileTierLevel, + pub vps_sub_layer_ordering_info_present_flag: bool, + pub dec_pic_buf_mgr: DecPicBufMgr, + pub vps_max_layer_id: u8, + pub vps_num_layer_sets_minus1: u32, + /// Present when `vps_timing_info_present_flag` is set. + pub timing: Option, + pub vps_num_hrd_parameters: u32, + pub vps_extension_flag: bool, +} + +/// Parse a VPS NAL payload (EBSP, after the 2-byte NAL header). +pub(crate) fn parse_vps(payload: &[u8]) -> Result { + let mut r = Reader::new(payload); + let mut vps = Vps { + vps_video_parameter_set_id: r.bits(4)? as u8, + vps_base_layer_internal_flag: r.flag()?, + vps_base_layer_available_flag: r.flag()?, + vps_max_layers_minus1: r.bits(6)? as u8, + vps_max_sub_layers_minus1: r.bits(3)? as u8, + vps_temporal_id_nesting_flag: r.flag()?, + ..Default::default() + }; + if vps.vps_max_sub_layers_minus1 as usize >= MAX_SUB_LAYERS { + return Err(invalid("VPS: vps_max_sub_layers_minus1 is 7")); + } + let _vps_reserved_0xffff_16bits = r.bits(16)?; + vps.profile_tier_level = parse_profile_tier_level(&mut r, vps.vps_max_sub_layers_minus1)?; + vps.vps_sub_layer_ordering_info_present_flag = r.flag()?; + vps.dec_pic_buf_mgr = parse_dec_pic_buf_mgr( + &mut r, + vps.vps_max_sub_layers_minus1, + vps.vps_sub_layer_ordering_info_present_flag, + )?; + vps.vps_max_layer_id = r.bits(6)? as u8; + vps.vps_num_layer_sets_minus1 = r.ue_max(1023, "vps_num_layer_sets_minus1")?; + for _ in 1..=vps.vps_num_layer_sets_minus1 { + // layer_id_included_flag[i][j] for j in 0..=vps_max_layer_id. + r.skip(vps.vps_max_layer_id as u32 + 1)?; + } + if r.flag()? { + let mut timing = VpsTiming { + num_units_in_tick: r.bits(32)?, + time_scale: r.bits(32)?, + poc_proportional_to_timing_flag: r.flag()?, + num_ticks_poc_diff_one_minus1: 0, + }; + if timing.poc_proportional_to_timing_flag { + timing.num_ticks_poc_diff_one_minus1 = r.ue()?; + } + vps.timing = Some(timing); + vps.vps_num_hrd_parameters = + r.ue_max(vps.vps_num_layer_sets_minus1 + 1, "vps_num_hrd_parameters")?; + for i in 0..vps.vps_num_hrd_parameters { + let _hrd_layer_set_idx = r.ue()?; + // cprms_present_flag[0] is inferred to be 1. + let cprms_present = i == 0 || r.flag()?; + skip_hrd_parameters(&mut r, cprms_present, vps.vps_max_sub_layers_minus1)?; + } + } + vps.vps_extension_flag = r.flag()?; + Ok(vps) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn nal_iteration_and_header() { + let data = [ + 0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0xAA, // VPS, layer 0, tid 0 + 0x00, 0x00, 0x01, 0x26, 0x01, 0xBB, // IDR_W_RADL + 0x00, 0x00, 0x01, 0x02, 0x03, 0xCC, // TRAIL_R, tid 2 + 0x00, 0x00, 0x01, 0x80, 0x01, // forbidden bit set: skipped + 0x00, 0x00, 0x01, 0x4F, 0xF9, 0xDD, // SEI prefix, nuh_layer_id 63 + ]; + let nals: Vec<_> = iter_nal_units(&data).collect(); + assert_eq!(nals.len(), 4); + assert_eq!(nals[0].nal_type(), NalType::Vps); + assert_eq!(nals[0].payload(), &[0xAA]); + assert_eq!(nals[1].nal_type(), NalType::IdrWRadl); + assert!(nals[1].nal_type().is_irap() && nals[1].nal_type().is_idr()); + assert_eq!(nals[2].nal_type(), NalType::TrailR); + assert_eq!(nals[2].header.temporal_id, 2); + // 0x4F 0xF9: type 39, layer id (1 << 5) | 31 = 63, tid 0. + assert_eq!(nals[3].nal_type(), NalType::SeiPrefix); + assert_eq!(nals[3].header.layer_id, 63); + } + + #[test] + fn nal_type_classes() { + for raw in 0..64u8 { + let t = NalType::from_raw(raw); + assert_eq!(t.raw(), raw, "round trip of {raw}"); + assert_eq!(t.is_vcl(), raw < 32); + assert_eq!(t.is_irap(), (16..=23).contains(&raw)); + assert_eq!(t.is_idr(), raw == 19 || raw == 20); + assert_eq!(t.is_rasl(), raw == 8 || raw == 9); + assert_eq!(t.is_radl(), raw == 6 || raw == 7); + } + assert!(NalType::TrailN.is_sub_layer_non_reference()); + assert!(NalType::RaslN.is_sub_layer_non_reference()); + assert!(!NalType::TrailR.is_sub_layer_non_reference()); + assert!(!NalType::Cra.is_sub_layer_non_reference()); + assert!(NalType::Other(14).is_sub_layer_non_reference()); + } + + #[test] + fn reader_counts_rbsp_bits_not_emulation_bytes() { + // 00 00 03 01: the 03 is not part of the RBSP. + let data = [0x00, 0x00, 0x03, 0x01]; + let mut r = Reader::new(&data); + assert_eq!(r.bits(24).unwrap(), 1); + assert_eq!(r.position(), 24); + } + + #[test] + fn reader_exp_golomb() { + // ue: 1 -> 0, 010 -> 1, 011 -> 2; se: 00100 -> code 3 -> +2, + // 00101 -> code 4 -> -2. + let data = [0b1010_0110, 0b0100_0010, 0b1000_0000]; + let mut r = Reader::new(&data); + assert_eq!(r.ue().unwrap(), 0); + assert_eq!(r.ue().unwrap(), 1); + assert_eq!(r.ue().unwrap(), 2); + assert_eq!(r.se().unwrap(), 2); + assert_eq!(r.se().unwrap(), -2); + assert_eq!(r.position(), 17); + } + + #[test] + fn ceil_log2_values() { + assert_eq!(ceil_log2(0), 0); + assert_eq!(ceil_log2(1), 0); + assert_eq!(ceil_log2(2), 1); + assert_eq!(ceil_log2(3), 2); + assert_eq!(ceil_log2(4), 2); + assert_eq!(ceil_log2(9), 4); + } +} diff --git a/src/decoder/h265/parser_tests.rs b/src/decoder/h265/parser_tests.rs new file mode 100644 index 00000000..709fe3cb --- /dev/null +++ b/src/decoder/h265/parser_tests.rs @@ -0,0 +1,701 @@ +//! Parser and DPB tests against real H.265 streams. +//! +//! The fixtures in `tests/data/*.265` are 176x144 (one is 172x140) testsrc2 +//! clips of 10 to 30 frames. All but one come from libx265 4.3 through +//! `ffmpeg -c:v libx265 -crf 45 -x265-params log-level=error:`: +//! +//! | fixture | x265 params | +//! |-----------------|-------------------------------------------------------------------| +//! | `opengop` | `open-gop=1:keyint=12:min-keyint=12:scenecut=0:bframes=3:b-pyramid=1` | +//! | `closedgop` | `open-gop=0:keyint=10:min-keyint=10:scenecut=0:bframes=3:radl=2:wpp=0` | +//! | `slices` | `slices=4:keyint=15:bframes=2` (3 slices of 3 CTB rows) | +//! | `main10` | `keyint=15:bframes=2`, `-pix_fmt yuv420p10le`, 172x140 | +//! | `temporal` | `temporal-layers=3:bframes=3:b-pyramid=1:keyint=20:scaling-list=default` | +//! | `scaling` | `scaling-list=:bframes=2` | +//! +//! `nvenc` is `hevc_nvenc -bf 3 -b_ref_mode middle -g 10 -rc constqp -qp 45`: +//! unlike x265 it codes a short-term set in the SPS and has VUI HRD +//! parameters. `cra_start` is `opengop` cut at its second CRA (keeping the +//! parameter sets), so it starts with a CRA whose RASL pictures reference +//! pictures that are not there. +//! +//! The expected values below were read from `ffmpeg -bsf:v trace_headers`, +//! which prints every syntax element with its bit position, and the frame +//! counts from `ffprobe -count_frames` (which skips those RASL pictures too). +//! A one-off comparison of every parsed VPS/SPS/PPS field and every slice +//! header field against the trace found no differences; the assertions here +//! keep the parts that matter. + +use std::collections::HashMap; +use std::sync::Arc; + +use super::dpb::{DecodeDpb, PictureDecision, RefPicSet, SkipReason}; +use super::parser::{NalType, NalUnit, Vps, iter_nal_units, parse_vps}; +use super::pps::{Pps, parse_pps}; +use super::slice::{SliceHeader, parse_slice_header, slice_pps_id, starts_picture}; +use super::sps::{Sps, parse_sps}; + +const OPENGOP: &[u8] = include_bytes!("../../../tests/data/opengop.265"); +const CLOSEDGOP: &[u8] = include_bytes!("../../../tests/data/closedgop.265"); +const SLICES: &[u8] = include_bytes!("../../../tests/data/slices.265"); +const MAIN10: &[u8] = include_bytes!("../../../tests/data/main10.265"); +const NVENC: &[u8] = include_bytes!("../../../tests/data/nvenc.265"); +const CRA_START: &[u8] = include_bytes!("../../../tests/data/cra_start.265"); +const TEMPORAL: &[u8] = include_bytes!("../../../tests/data/temporal.265"); +const SCALING: &[u8] = include_bytes!("../../../tests/data/scaling.265"); + +/// Every fixture, with the number of pictures it holds and the number ffmpeg +/// outputs. +const ALL: &[(&str, &[u8], usize, usize)] = &[ + ("opengop", OPENGOP, 30, 30), + ("closedgop", CLOSEDGOP, 30, 30), + ("slices", SLICES, 20, 20), + ("main10", MAIN10, 20, 20), + ("nvenc", NVENC, 20, 20), + ("cra_start", CRA_START, 8, 6), + ("temporal", TEMPORAL, 20, 20), + ("scaling", SCALING, 10, 10), +]; + +/// One slice segment with the parameter sets in effect for it. +struct Segment<'a> { + nal: NalUnit<'a>, + header: SliceHeader, + sps: Sps, +} + +/// The parameter sets of a stream and all of its slice segments, parsed. +struct Parsed<'a> { + vps: Vec, + sps: Vec, + pps: Vec, + segments: Vec>, +} + +fn parse_stream<'a>(name: &str, data: &'a [u8]) -> Parsed<'a> { + let mut parsed = Parsed { + vps: Vec::new(), + sps: Vec::new(), + pps: Vec::new(), + segments: Vec::new(), + }; + let mut sps_by_id: HashMap = HashMap::new(); + let mut pps_by_id: HashMap = HashMap::new(); + for nal in iter_nal_units(data) { + assert_eq!(nal.header.layer_id, 0, "{name}: base layer only"); + match nal.nal_type() { + NalType::Vps => parsed + .vps + .push(parse_vps(nal.payload()).unwrap_or_else(|e| panic!("{name}: VPS: {e}"))), + NalType::Sps => { + let sps = parse_sps(nal.payload()).unwrap_or_else(|e| panic!("{name}: SPS: {e}")); + sps_by_id.insert(sps.sps_seq_parameter_set_id, sps.clone()); + parsed.sps.push(sps); + } + NalType::Pps => { + let pps = parse_pps(nal.payload()).unwrap_or_else(|e| panic!("{name}: PPS: {e}")); + pps_by_id.insert(pps.pps_pic_parameter_set_id, pps.clone()); + parsed.pps.push(pps); + } + t if t.is_vcl() => { + let pps = pps_by_id[&slice_pps_id(&nal).unwrap()].clone(); + let sps = sps_by_id[&pps.pps_seq_parameter_set_id].clone(); + let header = parse_slice_header(&nal, &sps, &pps) + .unwrap_or_else(|e| panic!("{name}: slice header: {e}")); + assert_eq!( + header.first_slice_segment_in_pic_flag, + starts_picture(&nal), + "{name}: starts_picture agrees with the parsed header" + ); + parsed.segments.push(Segment { nal, header, sps }); + } + _ => {} + } + } + parsed +} + +impl Parsed<'_> { + /// The first segment of each picture. + fn pictures(&self) -> impl Iterator> { + self.segments + .iter() + .filter(|s| s.header.first_slice_segment_in_pic_flag) + } +} + +/// What the DPB made of one picture. +struct Outcome { + nal_type: NalType, + /// `None` when skipped. + poc: Option, + skip: Option, + rps: RefPicSet, + missing_reference: bool, + is_keyframe: bool, +} + +/// Run every picture of `parsed` through a [`DecodeDpb`] with exactly +/// `sps_max_dec_pic_buffering` slots, which a conforming stream never +/// overflows. +fn run_dpb(name: &str, parsed: &Parsed) -> Vec { + let first_sps = &parsed.sps[0]; + let mut dpb = DecodeDpb::new(first_sps.max_dec_pic_buffering(), Arc::default()); + let mut outcomes = Vec::new(); + for segment in parsed.pictures() { + let decision = dpb + .begin_picture(&segment.nal.header, &segment.header, &segment.sps) + .unwrap_or_else(|e| panic!("{name}: begin_picture: {e}")); + let state = match decision { + PictureDecision::Skip(reason) => { + outcomes.push(Outcome { + nal_type: segment.nal.nal_type(), + poc: None, + skip: Some(reason), + rps: RefPicSet::default(), + missing_reference: false, + is_keyframe: false, + }); + continue; + } + PictureDecision::Decode(state) => state, + }; + // Every slot the reference picture set names holds that picture, and + // the DPB holds nothing else. + let rps = &state.rps; + let named: Vec<_> = [ + &rps.st_curr_before, + &rps.st_curr_after, + &rps.st_foll, + &rps.lt_curr, + &rps.lt_foll, + ] + .into_iter() + .flatten() + .filter_map(|e| e.slot.map(|slot| (slot, e.poc))) + .collect(); + for &(slot, poc) in &named { + assert!( + dpb.references() + .iter() + .any(|r| r.slot == slot && r.poc == poc), + "{name}: slot {slot} holds POC {poc}" + ); + } + assert_eq!( + named.len(), + dpb.references().len(), + "{name}: DPB holds the RPS" + ); + let slot = dpb + .try_allocate_slot() + .unwrap_or_else(|| panic!("{name}: DPB overflow at POC {}", state.poc)); + dpb.end_picture(slot, &state); + outcomes.push(Outcome { + nal_type: state.nal_type, + poc: Some(state.poc), + skip: None, + rps: state.rps, + missing_reference: state.missing_reference, + is_keyframe: state.is_keyframe, + }); + } + outcomes +} + +/// Every NAL unit of every fixture parses, and the picture structure is what +/// the encoder was asked for. +#[test] +fn parses_every_nal_unit() { + for &(name, data, pictures, _) in ALL { + let parsed = parse_stream(name, data); + assert!(!parsed.vps.is_empty(), "{name}: has a VPS"); + assert!(!parsed.sps.is_empty(), "{name}: has an SPS"); + assert!(!parsed.pps.is_empty(), "{name}: has a PPS"); + assert_eq!(parsed.pictures().count(), pictures, "{name}: pictures"); + let first = parsed.pictures().next().unwrap(); + assert!(first.nal.nal_type().is_irap(), "{name}: opens with an IRAP"); + } + // Three slices per picture, at CTB rows 0, 1 and 2. + let parsed = parse_stream("slices", SLICES); + assert_eq!(parsed.segments.len(), 60); + let addresses: Vec = parsed.segments[..3] + .iter() + .map(|s| s.header.slice_segment_address) + .collect(); + assert_eq!(addresses, vec![0, 3, 6]); + // Every slice of a picture carries the same POC LSB and slice type. + for picture in parsed.segments.chunks(3) { + for s in &picture[1..] { + assert!(!s.header.first_slice_segment_in_pic_flag); + assert_eq!( + s.header.slice_pic_order_cnt_lsb, + picture[0].header.slice_pic_order_cnt_lsb + ); + assert_eq!(s.header.slice_type, picture[0].header.slice_type); + } + } +} + +/// SPS and VPS values against trace_headers. +#[test] +fn parses_sps_and_vps() { + for &(name, data, _, _) in ALL { + let parsed = parse_stream(name, data); + let sps = &parsed.sps[0]; + let vps = &parsed.vps[0]; + assert_eq!(sps.chroma_format_idc, 1, "{name}: 4:2:0"); + let display = if name == "main10" { + (172, 140) + } else { + (176, 144) + }; + assert_eq!(sps.display_dimensions(), display, "{name}: display size"); + assert_eq!( + sps.log2_max_pic_order_cnt_lsb_minus4, 4, + "{name}: 8-bit POC LSB" + ); + assert_eq!(sps.max_pic_order_cnt_lsb(), 256); + assert!(!sps.long_term_ref_pics_present_flag, "{name}: no long-term"); + assert_eq!( + vps.profile_tier_level, sps.profile_tier_level, + "{name}: VPS and SPS agree on the profile" + ); + assert_eq!(vps.vps_max_sub_layers_minus1, sps.sps_max_sub_layers_minus1); + assert_eq!( + vps.dec_pic_buf_mgr, sps.dec_pic_buf_mgr, + "{name}: DPB sizes" + ); + if name != "nvenc" { + // x265 always codes every short-term set in the slice header. + assert_eq!(sps.num_short_term_ref_pic_sets(), 0, "{name}"); + assert_eq!(sps.max_dec_pic_buffering(), 5, "{name}"); + assert_eq!(sps.reorder_depth(), 2, "{name}"); + assert_eq!(sps.profile_tier_level.general_level_idc, 60, "{name}"); + assert_eq!( + ( + sps.pic_width_in_luma_samples, + sps.pic_height_in_luma_samples + ), + (176, 144), + "{name}: coded size" + ); + assert_eq!(sps.ctb_log2_size(), 6, "{name}: 64x64 CTBs"); + assert_eq!(sps.pic_size_in_ctbs(), 9, "{name}: 3x3 CTBs"); + assert!(sps.sps_temporal_mvp_enabled_flag, "{name}: TMVP"); + } + } + + let main = &parse_stream("opengop", OPENGOP).sps[0]; + assert_eq!(main.profile_tier_level.general_profile_idc, 1, "Main"); + assert_eq!(main.bit_depth_luma_minus8, 0); + assert!(!main.conformance_window_flag); + assert_eq!(main.display_dimensions(), (176, 144)); + assert!(main.sample_adaptive_offset_enabled_flag); + assert!(main.strong_intra_smoothing_enabled_flag); + assert!(!main.amp_enabled_flag); + assert_eq!(main.max_latency_pictures(), Some(5)); + let vui = main.vui.as_ref().expect("x265 writes a VUI"); + assert_eq!(vui.aspect_ratio_idc, 1); + assert_eq!(vui.video_format, 5); + assert_eq!((vui.vui_num_units_in_tick, vui.vui_time_scale), (1, 30)); + + // Main 10 at 172x140: coded 176x144 with a 2-sample (4 luma) crop. + let main10 = &parse_stream("main10", MAIN10).sps[0]; + assert_eq!(main10.profile_tier_level.general_profile_idc, 2); + assert_eq!( + (main10.bit_depth_luma_minus8, main10.bit_depth_chroma_minus8), + (2, 2) + ); + assert!(main10.conformance_window_flag); + assert_eq!( + (main10.conf_win_right_offset, main10.conf_win_bottom_offset), + (2, 2) + ); + assert_eq!(main10.display_dimensions(), (172, 140)); + + // Three temporal sub-layers; every one signals the same DPB sizes. + let parsed = parse_stream("temporal", TEMPORAL); + let (sps, vps) = (&parsed.sps[0], &parsed.vps[0]); + assert_eq!(sps.sps_max_sub_layers_minus1, 2); + assert_eq!(sps.highest_tid(), 2); + assert!(!sps.sps_temporal_id_nesting_flag && !vps.vps_temporal_id_nesting_flag); + assert!(vps.vps_sub_layer_ordering_info_present_flag); + assert_eq!( + vps.dec_pic_buf_mgr.max_dec_pic_buffering_minus1[..3], + [4, 4, 4] + ); + assert_eq!(vps.dec_pic_buf_mgr.max_num_reorder_pics[..3], [2, 2, 2]); + assert_eq!( + vps.dec_pic_buf_mgr.max_latency_increase_plus1[..3], + [4, 4, 4] + ); + assert_eq!(sps.reorder_depth(), 2); +} + +/// NVENC's SPS: one short-term set, and a VUI with HRD parameters that must be +/// skipped exactly to reach the bitstream restriction and extension flags. +#[test] +fn parses_nvenc_sps_rps_and_hrd() { + let parsed = parse_stream("nvenc", NVENC); + let sps = &parsed.sps[0]; + // 32x32 CTBs, coded at 192x160 and cropped by 8 chroma samples a side. + assert_eq!( + ( + sps.pic_width_in_luma_samples, + sps.pic_height_in_luma_samples + ), + (192, 160) + ); + assert_eq!( + (sps.conf_win_right_offset, sps.conf_win_bottom_offset), + (8, 8) + ); + assert_eq!(sps.ctb_log2_size(), 5); + assert_eq!(sps.pic_size_in_ctbs(), 30); + assert_eq!(sps.max_dec_pic_buffering(), 6); + assert_eq!(sps.max_latency_pictures(), None); + assert!(sps.amp_enabled_flag && !sps.sps_temporal_mvp_enabled_flag); + assert_eq!(sps.profile_tier_level.general_level_idc, 90); + assert_eq!(sps.num_short_term_ref_pic_sets(), 1); + let set = &sps.short_term_ref_pic_sets[0]; + assert!(!set.syntax.inter_ref_pic_set_prediction_flag); + assert_eq!(set.syntax.num_negative_pics, 5); + assert_eq!(set.syntax.num_positive_pics, 0); + assert_eq!(set.syntax.used_by_curr_pic_s0_flag, 0b00011); + assert_eq!(set.rps.delta_poc_s0, vec![-1, -2, -3, -4, -5]); + assert_eq!(set.rps.used_s0, vec![true, true, false, false, false]); + + let vui = sps.vui.as_ref().unwrap(); + assert!(vui.vui_hrd_parameters_present_flag); + assert!(!sps.sps_extension_present_flag); + + // Each slice codes its own set at index 1, so the inter prediction flag + // is coded (as 0) and counts towards NumBitsForSTRefPicSetInSlice. + for s in parsed.pictures().filter(|s| !s.nal.nal_type().is_idr()) { + let inline = s.header.st_ref_pic_set.as_ref().expect("inline set"); + assert!(!inline.syntax.inter_ref_pic_set_prediction_flag); + assert_eq!(s.header.num_delta_pocs_of_ref_rps_idx(sps), 0); + } +} + +/// PPS values against trace_headers. +#[test] +fn parses_pps() { + for &(name, data, _, _) in ALL { + let pps = &parse_stream(name, data).pps[0]; + assert_eq!(pps.pps_pic_parameter_set_id, 0, "{name}"); + assert_eq!(pps.pps_seq_parameter_set_id, 0, "{name}"); + assert!(!pps.tiles_enabled_flag, "{name}: x265 has no tiles"); + assert_eq!(pps.tile_column_widths(3), vec![3], "{name}"); + assert!(!pps.lists_modification_present_flag, "{name}"); + assert!(!pps.pps_extension_present_flag, "{name}"); + } + let x265 = &parse_stream("opengop", OPENGOP).pps[0]; + assert!( + x265.entropy_coding_sync_enabled_flag, + "WPP is x265's default" + ); + assert!(x265.sign_data_hiding_enabled_flag); + assert!(x265.cu_qp_delta_enabled_flag); + assert_eq!(x265.diff_cu_qp_delta_depth, 1); + assert!(x265.weighted_pred_flag); + assert!(!x265.weighted_bipred_flag); + assert!(x265.pps_loop_filter_across_slices_enabled_flag); + assert!(!x265.deblocking_filter_control_present_flag); + assert_eq!(x265.log2_parallel_merge_level_minus2, 0); + let closed = &parse_stream("closedgop", CLOSEDGOP).pps[0]; + assert!(!closed.entropy_coding_sync_enabled_flag, "wpp=0"); +} + +/// Explicit SPS scaling lists, against the values rebuilt from the trace's +/// `scaling_list_delta_coef`s, including lists predicted from the previous +/// matrix and the DC coefficients of the 16x16 and 32x32 lists. +#[test] +fn parses_scaling_lists() { + let sps = &parse_stream("scaling", SCALING).sps[0]; + assert!(sps.scaling_list_enabled_flag && sps.sps_scaling_list_data_present_flag); + let lists = sps.scaling_lists.as_ref().unwrap(); + // Coded order: the HM file's raster 4x4 starts 12, 14, 19, 24 / 22, ... + // and the up-right diagonal scan visits (0,0), (1,0), (0,1), (2,0), ... + assert_eq!( + lists.scaling_list_4x4[0][..8], + [12, 22, 14, 36, 25, 19, 18, 41] + ); + assert_eq!( + lists.scaling_list_8x8[0][..8], + [19, 44, 21, 40, 46, 27, 34, 38] + ); + assert_eq!( + lists.scaling_list_16x16[0][..8], + [22, 50, 27, 44, 49, 33, 40, 48] + ); + assert_eq!( + lists.scaling_list_32x32[1][..8], + [32, 53, 35, 49, 58, 35, 45, 52] + ); + let sums: Vec = (0..6) + .map(|m| lists.scaling_list_16x16[m].iter().map(|&c| c as u32).sum()) + .collect(); + assert_eq!(sums, vec![2424, 2476, 2537, 2592, 2708, 2708]); + // matrixId 5 is predicted from 4 at every size (delta 1). + assert_eq!(lists.scaling_list_4x4[5], lists.scaling_list_4x4[4]); + assert_eq!(lists.scaling_list_8x8[5], lists.scaling_list_8x8[4]); + assert_eq!(lists.scaling_list_16x16[5], lists.scaling_list_16x16[4]); + assert_eq!(lists.scaling_list_dc_coef_16x16, [23, 27, 27, 31, 28, 28]); + assert_eq!(lists.scaling_list_dc_coef_32x32, [32, 33]); + + // scaling-list=default: enabled, no data, so the Table 7-6 defaults. + let sps = &parse_stream("temporal", TEMPORAL).sps[0]; + assert!(sps.scaling_list_enabled_flag && !sps.sps_scaling_list_data_present_flag); + let defaults = sps.scaling_lists.as_ref().unwrap(); + assert_eq!(defaults.scaling_list_4x4[0], [16; 16]); + assert_eq!(defaults.scaling_list_8x8[0][63], 115, "intra default"); + assert_eq!(defaults.scaling_list_32x32[1][63], 91, "inter default"); + assert_eq!(defaults.scaling_list_dc_coef_32x32, [16, 16]); +} + +/// Per picture, in decoding order: nal_unit_type, slice_pic_order_cnt_lsb, +/// and the bits of the inline st_ref_pic_set() (from trace_headers' bit +/// positions: the element after short_term_ref_pic_set_sps_flag up to +/// slice_temporal_mvp_enabled_flag). +#[test] +fn slice_headers_match_trace() { + /// Name, stream, nal_unit_type, POC LSB and RPS bits per picture. + type Case<'a> = (&'a str, &'a [u8], &'a [u8], &'a [u32], &'a [u32]); + let cases: &[Case] = &[ + ( + "opengop", + OPENGOP, + &[ + 20, 1, 1, 1, 0, 0, 1, 1, 0, 0, 21, 9, 8, 1, 1, 0, 1, 1, 0, 1, 1, 0, 21, 9, 8, 1, 1, + 0, 0, 1, + ], + &[ + 0, 1, 5, 3, 2, 4, 9, 7, 6, 8, 12, 11, 10, 15, 14, 13, 18, 17, 16, 21, 20, 19, 24, + 23, 22, 28, 26, 25, 27, 29, + ], + &[ + 0, 6, 12, 16, 16, 18, 22, 24, 18, 20, 22, 24, 16, 8, 12, 12, 16, 20, 14, 18, 20, + 14, 18, 20, 14, 10, 14, 14, 14, 16, + ], + ), + ( + "closedgop", + CLOSEDGOP, + &[ + 20, 1, 1, 1, 0, 0, 1, 0, 19, 7, 6, 1, 1, 0, 1, 0, 1, 0, 19, 7, 6, 1, 1, 0, 1, 0, 1, + 0, 1, 0, + ], + &[ + 0, 1, 5, 3, 2, 4, 7, 6, 0, 255, 254, 3, 2, 1, 5, 4, 7, 6, 0, 255, 254, 3, 2, 1, 5, + 4, 7, 6, 9, 8, + ], + &[ + 0, 6, 12, 16, 16, 18, 20, 20, 0, 6, 8, 8, 12, 12, 16, 18, 20, 18, 0, 6, 8, 8, 12, + 12, 16, 18, 20, 18, 20, 20, + ], + ), + ( + "nvenc", + NVENC, + &[19, 1, 1, 0, 0, 1, 1, 0, 0, 1, 19, 1, 1, 0, 0, 1, 1, 0, 0, 1], + &[0, 4, 2, 1, 3, 8, 6, 5, 7, 9, 0, 4, 2, 1, 3, 8, 6, 5, 7, 9], + &[ + 0, 11, 15, 15, 15, 21, 25, 25, 25, 25, 0, 11, 15, 15, 15, 21, 25, 25, 25, 25, + ], + ), + ( + "temporal", + TEMPORAL, + &[20, 1, 3, 2, 2, 1, 3, 2, 2, 1, 3, 2, 2, 1, 3, 2, 2, 1, 1, 2], + &[ + 0, 4, 2, 1, 3, 8, 6, 5, 7, 12, 10, 9, 11, 16, 14, 13, 15, 19, 18, 17, + ], + &[ + 0, 10, 14, 14, 14, 16, 20, 20, 22, 24, 28, 20, 22, 24, 28, 20, 22, 22, 26, 18, + ], + ), + ]; + for &(name, data, types, lsbs, bits) in cases { + let parsed = parse_stream(name, data); + let pictures: Vec<_> = parsed.pictures().collect(); + let got: Vec = pictures.iter().map(|s| s.nal.nal_type().raw()).collect(); + assert_eq!(got, types, "{name}: nal_unit_type"); + let got: Vec = pictures + .iter() + .map(|s| s.header.slice_pic_order_cnt_lsb) + .collect(); + assert_eq!(got, lsbs, "{name}: slice_pic_order_cnt_lsb"); + let got: Vec = pictures + .iter() + .map(|s| s.header.num_bits_for_st_ref_pic_set) + .collect(); + assert_eq!(got, bits, "{name}: NumBitsForSTRefPicSetInSlice"); + for s in &pictures { + assert!( + s.header.pic_output_flag, + "{name}: no output_flag_present_flag" + ); + assert!( + !s.header.short_term_ref_pic_set_sps_flag, + "{name}: every encoder here codes the set inline" + ); + } + } + + // Temporal sub-layers: TemporalId per picture, and TSA types. + let parsed = parse_stream("temporal", TEMPORAL); + let tids: Vec = parsed + .pictures() + .map(|s| s.nal.header.temporal_id) + .collect(); + assert_eq!( + tids, + vec![0, 0, 1, 2, 2, 0, 1, 2, 2, 0, 1, 2, 2, 0, 1, 2, 2, 0, 0, 1] + ); + let types: Vec = parsed.pictures().map(|s| s.nal.nal_type()).collect(); + assert_eq!(types[2], NalType::TsaR); + assert_eq!(types[3], NalType::TsaN); +} + +/// POC in decoding order, derived by the DPB, against the trace's LSBs and +/// IRAP structure. closedgop's RADL pictures have LSB 255/254 after an IDR, +/// which is POC -1/-2. +#[test] +fn poc_sequences() { + let cases: &[(&str, &[u8], &[i32])] = &[ + ( + "opengop", + OPENGOP, + &[ + 0, 1, 5, 3, 2, 4, 9, 7, 6, 8, 12, 11, 10, 15, 14, 13, 18, 17, 16, 21, 20, 19, 24, + 23, 22, 28, 26, 25, 27, 29, + ], + ), + ( + "closedgop", + CLOSEDGOP, + &[ + 0, 1, 5, 3, 2, 4, 7, 6, 0, -1, -2, 3, 2, 1, 5, 4, 7, 6, 0, -1, -2, 3, 2, 1, 5, 4, + 7, 6, 9, 8, + ], + ), + ( + "nvenc", + NVENC, + &[0, 4, 2, 1, 3, 8, 6, 5, 7, 9, 0, 4, 2, 1, 3, 8, 6, 5, 7, 9], + ), + ( + "temporal", + TEMPORAL, + &[ + 0, 4, 2, 1, 3, 8, 6, 5, 7, 12, 10, 9, 11, 16, 14, 13, 15, 19, 18, 17, + ], + ), + ]; + for &(name, data, expected) in cases { + let outcomes = run_dpb(name, &parse_stream(name, data)); + let pocs: Vec = outcomes.iter().map(|o| o.poc.unwrap()).collect(); + assert_eq!(pocs, expected, "{name}: POC in decoding order"); + } + + // In every fixture, each coded video sequence covers a contiguous range of + // POCs, which is what a display-order sort of the output needs. + for &(name, data, _, output) in ALL { + let outcomes = run_dpb(name, &parse_stream(name, data)); + let decoded: Vec<_> = outcomes.iter().filter(|o| o.poc.is_some()).collect(); + assert_eq!(decoded.len(), output, "{name}: pictures output, as ffprobe"); + let mut sequences: Vec> = Vec::new(); + for o in decoded { + if o.is_keyframe { + sequences.push(Vec::new()); + } + sequences.last_mut().unwrap().push(o.poc.unwrap()); + } + for mut pocs in sequences { + pocs.sort(); + assert!( + pocs.windows(2).all(|w| w[1] == w[0] + 1), + "{name}: contiguous POCs {pocs:?}" + ); + } + } +} + +/// Every reference the slice headers name exists, in a DPB no larger than the +/// SPS allows, except where random access left them behind. +#[test] +fn reference_picture_sets_resolve() { + for &(name, data, _, _) in ALL { + let outcomes = run_dpb(name, &parse_stream(name, data)); + for (i, o) in outcomes.iter().enumerate() { + if o.skip.is_some() { + continue; + } + assert!( + !o.missing_reference, + "{name}: picture {i} misses a reference" + ); + let entries = o + .rps + .st_curr_before + .iter() + .chain(&o.rps.st_curr_after) + .chain(&o.rps.st_foll); + for e in entries { + if name == "cra_start" && i == 0 { + // The CRA keeps pictures for its RASL pictures, which + // this cut does not have. + assert!(e.slot.is_none(), "{name}: nothing before the CRA"); + } else { + assert!(e.slot.is_some(), "{name}: picture {i}: POC {} held", e.poc); + } + } + if o.nal_type.is_irap() { + assert!( + o.rps.st_curr_before.is_empty() && o.rps.st_curr_after.is_empty(), + "{name}: an IRAP predicts from nothing" + ); + } + // B pictures reference both directions, P pictures only the past. + let b = o.rps.st_curr_after.iter().all(|e| e.poc > o.poc.unwrap()); + let p = o.rps.st_curr_before.iter().all(|e| e.poc < o.poc.unwrap()); + assert!(b && p, "{name}: picture {i} list ordering"); + } + } +} + +/// A stream cut at a CRA: the CRA starts decoding, its two RASL pictures are +/// skipped, and everything after it decodes from the CRA alone. +#[test] +fn cra_start_skips_rasl() { + let outcomes = run_dpb("cra_start", &parse_stream("cra_start", CRA_START)); + let summary: Vec<(NalType, Option, Option)> = outcomes + .iter() + .map(|o| (o.nal_type, o.poc, o.skip)) + .collect(); + let rasl = Some(SkipReason::RaslWithoutReferences); + assert_eq!( + summary[..4], + [ + (NalType::Cra, Some(24), None), + (NalType::RaslR, None, rasl), + (NalType::RaslN, None, rasl), + (NalType::TrailR, Some(28), None), + ] + ); + assert!(outcomes[0].is_keyframe); + // POC 28 is a P picture predicting from the CRA only. + assert_eq!(outcomes[3].rps.st_curr_before.len(), 1); + assert_eq!(outcomes[3].rps.st_curr_before[0].poc, 24); + + // In the uncut stream the same CRA is not a keyframe, and its RASL + // pictures decode. + let outcomes = run_dpb("opengop", &parse_stream("opengop", OPENGOP)); + let cra = outcomes.iter().position(|o| o.poc == Some(24)).unwrap(); + assert_eq!(outcomes[cra].nal_type, NalType::Cra); + assert!(!outcomes[cra].is_keyframe); + assert_eq!(outcomes[cra + 1].poc, Some(23)); + assert!(!outcomes[cra + 1].missing_reference); +} diff --git a/src/decoder/h265/pps.rs b/src/decoder/h265/pps.rs new file mode 100644 index 00000000..96eae08d --- /dev/null +++ b/src/decoder/h265/pps.rs @@ -0,0 +1,251 @@ +//! The H.265 picture parameter set (clause 7.3.2.3). +//! +//! Field names follow the standard, which are also the names +//! `StdVideoH265PictureParameterSet` uses. + +use super::parser::Reader; +use super::scaling::{ScalingLists, parse_scaling_list_data}; +use crate::error::Result; + +/// Tile columns `StdVideoH265PictureParameterSet::column_width_minus1` holds. +pub(crate) const MAX_TILE_COLUMNS: usize = 20; +/// Tile rows `StdVideoH265PictureParameterSet::row_height_minus1` holds. +pub(crate) const MAX_TILE_ROWS: usize = 22; + +/// `pps_range_extension()` (clause 7.3.2.3.2). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct PpsRangeExtension { + pub log2_max_transform_skip_block_size_minus2: u8, + pub cross_component_prediction_enabled_flag: bool, + pub chroma_qp_offset_list_enabled_flag: bool, + pub diff_cu_chroma_qp_offset_depth: u8, + pub chroma_qp_offset_list_len_minus1: u8, + pub cb_qp_offset_list: [i8; 6], + pub cr_qp_offset_list: [i8; 6], + pub log2_sao_offset_scale_luma: u8, + pub log2_sao_offset_scale_chroma: u8, +} + +/// Parsed H.265 picture parameter set. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct Pps { + pub pps_pic_parameter_set_id: u8, + pub pps_seq_parameter_set_id: u8, + pub dependent_slice_segments_enabled_flag: bool, + pub output_flag_present_flag: bool, + pub num_extra_slice_header_bits: u8, + pub sign_data_hiding_enabled_flag: bool, + pub cabac_init_present_flag: bool, + pub num_ref_idx_l0_default_active_minus1: u8, + pub num_ref_idx_l1_default_active_minus1: u8, + pub init_qp_minus26: i8, + pub constrained_intra_pred_flag: bool, + pub transform_skip_enabled_flag: bool, + pub cu_qp_delta_enabled_flag: bool, + pub diff_cu_qp_delta_depth: u8, + pub pps_cb_qp_offset: i8, + pub pps_cr_qp_offset: i8, + pub pps_slice_chroma_qp_offsets_present_flag: bool, + pub weighted_pred_flag: bool, + pub weighted_bipred_flag: bool, + pub transquant_bypass_enabled_flag: bool, + pub tiles_enabled_flag: bool, + pub entropy_coding_sync_enabled_flag: bool, + pub num_tile_columns_minus1: u8, + pub num_tile_rows_minus1: u8, + pub uniform_spacing_flag: bool, + /// `column_width_minus1[i]`, coded only without uniform spacing; see + /// [`Pps::tile_column_widths`] for the widths either way. + pub column_width_minus1: Vec, + /// `row_height_minus1[i]`, coded only without uniform spacing. + pub row_height_minus1: Vec, + pub loop_filter_across_tiles_enabled_flag: bool, + pub pps_loop_filter_across_slices_enabled_flag: bool, + pub deblocking_filter_control_present_flag: bool, + pub deblocking_filter_override_enabled_flag: bool, + pub pps_deblocking_filter_disabled_flag: bool, + pub pps_beta_offset_div2: i8, + pub pps_tc_offset_div2: i8, + pub pps_scaling_list_data_present_flag: bool, + /// The coded lists when `pps_scaling_list_data_present_flag` is set; they + /// replace the SPS's for pictures using this PPS. + pub scaling_lists: Option, + pub lists_modification_present_flag: bool, + pub log2_parallel_merge_level_minus2: u8, + pub slice_segment_header_extension_present_flag: bool, + pub pps_extension_present_flag: bool, + pub pps_range_extension_flag: bool, + pub pps_multilayer_extension_flag: bool, + pub pps_3d_extension_flag: bool, + pub pps_scc_extension_flag: bool, + pub pps_extension_4bits: u8, + pub range_extension: PpsRangeExtension, +} + +impl Pps { + /// Tile column widths in CTBs (equation 6-3), for a picture + /// `pic_width_in_ctbs` wide. One column when tiles are off. + pub fn tile_column_widths(&self, pic_width_in_ctbs: u32) -> Vec { + tile_sizes( + self.tiles_enabled_flag, + self.uniform_spacing_flag, + self.num_tile_columns_minus1 as u32, + &self.column_width_minus1, + pic_width_in_ctbs, + ) + } + + /// Tile row heights in CTBs (equation 6-4). + pub fn tile_row_heights(&self, pic_height_in_ctbs: u32) -> Vec { + tile_sizes( + self.tiles_enabled_flag, + self.uniform_spacing_flag, + self.num_tile_rows_minus1 as u32, + &self.row_height_minus1, + pic_height_in_ctbs, + ) + } +} + +/// Equations 6-3 and 6-4, which have the same shape for columns and rows. +fn tile_sizes( + tiles: bool, + uniform: bool, + count_minus1: u32, + explicit_minus1: &[u16], + total: u32, +) -> Vec { + if !tiles { + return vec![total]; + } + let count = count_minus1 + 1; + if uniform { + return (0..count) + .map(|i| ((i + 1) * total) / count - (i * total) / count) + .collect(); + } + let mut sizes: Vec = explicit_minus1.iter().map(|w| *w as u32 + 1).collect(); + let used: u32 = sizes.iter().sum(); + // The last one takes whatever is left. + sizes.push(total.saturating_sub(used)); + sizes +} + +/// Parse a PPS NAL payload (EBSP, after the 2-byte NAL header). +pub(crate) fn parse_pps(payload: &[u8]) -> Result { + let mut r = Reader::new(payload); + let mut pps = Pps { + pps_pic_parameter_set_id: r.ue_max(63, "pps_pic_parameter_set_id")? as u8, + pps_seq_parameter_set_id: r.ue_max(15, "pps_seq_parameter_set_id")? as u8, + dependent_slice_segments_enabled_flag: r.flag()?, + output_flag_present_flag: r.flag()?, + num_extra_slice_header_bits: r.bits(3)? as u8, + sign_data_hiding_enabled_flag: r.flag()?, + cabac_init_present_flag: r.flag()?, + ..Default::default() + }; + pps.num_ref_idx_l0_default_active_minus1 = + r.ue_max(14, "num_ref_idx_l0_default_active_minus1")? as u8; + pps.num_ref_idx_l1_default_active_minus1 = + r.ue_max(14, "num_ref_idx_l1_default_active_minus1")? as u8; + // The lower bound depends on the bit depth (-(26 + QpBdOffsetY)), which + // the PPS cannot see; -26 - 48 covers 16-bit video. + pps.init_qp_minus26 = r.se_range(-74, 25, "init_qp_minus26")? as i8; + pps.constrained_intra_pred_flag = r.flag()?; + pps.transform_skip_enabled_flag = r.flag()?; + pps.cu_qp_delta_enabled_flag = r.flag()?; + if pps.cu_qp_delta_enabled_flag { + pps.diff_cu_qp_delta_depth = r.ue_max(3, "diff_cu_qp_delta_depth")? as u8; + } + pps.pps_cb_qp_offset = r.se_range(-12, 12, "pps_cb_qp_offset")? as i8; + pps.pps_cr_qp_offset = r.se_range(-12, 12, "pps_cr_qp_offset")? as i8; + pps.pps_slice_chroma_qp_offsets_present_flag = r.flag()?; + pps.weighted_pred_flag = r.flag()?; + pps.weighted_bipred_flag = r.flag()?; + pps.transquant_bypass_enabled_flag = r.flag()?; + pps.tiles_enabled_flag = r.flag()?; + pps.entropy_coding_sync_enabled_flag = r.flag()?; + + if pps.tiles_enabled_flag { + pps.num_tile_columns_minus1 = + r.ue_max(MAX_TILE_COLUMNS as u32 - 1, "num_tile_columns_minus1")? as u8; + pps.num_tile_rows_minus1 = + r.ue_max(MAX_TILE_ROWS as u32 - 1, "num_tile_rows_minus1")? as u8; + pps.uniform_spacing_flag = r.flag()?; + if !pps.uniform_spacing_flag { + for _ in 0..pps.num_tile_columns_minus1 { + pps.column_width_minus1 + .push(r.ue_max(u16::MAX as u32, "column_width_minus1")? as u16); + } + for _ in 0..pps.num_tile_rows_minus1 { + pps.row_height_minus1 + .push(r.ue_max(u16::MAX as u32, "row_height_minus1")? as u16); + } + } + pps.loop_filter_across_tiles_enabled_flag = r.flag()?; + } + + pps.pps_loop_filter_across_slices_enabled_flag = r.flag()?; + pps.deblocking_filter_control_present_flag = r.flag()?; + if pps.deblocking_filter_control_present_flag { + pps.deblocking_filter_override_enabled_flag = r.flag()?; + pps.pps_deblocking_filter_disabled_flag = r.flag()?; + if !pps.pps_deblocking_filter_disabled_flag { + pps.pps_beta_offset_div2 = r.se_range(-6, 6, "pps_beta_offset_div2")? as i8; + pps.pps_tc_offset_div2 = r.se_range(-6, 6, "pps_tc_offset_div2")? as i8; + } + } + pps.pps_scaling_list_data_present_flag = r.flag()?; + if pps.pps_scaling_list_data_present_flag { + pps.scaling_lists = Some(parse_scaling_list_data(&mut r)?); + } + pps.lists_modification_present_flag = r.flag()?; + pps.log2_parallel_merge_level_minus2 = r.ue_max(4, "log2_parallel_merge_level_minus2")? as u8; + pps.slice_segment_header_extension_present_flag = r.flag()?; + + pps.pps_extension_present_flag = r.flag()?; + if pps.pps_extension_present_flag { + pps.pps_range_extension_flag = r.flag()?; + pps.pps_multilayer_extension_flag = r.flag()?; + pps.pps_3d_extension_flag = r.flag()?; + pps.pps_scc_extension_flag = r.flag()?; + pps.pps_extension_4bits = r.bits(4)? as u8; + } + if pps.pps_range_extension_flag { + let ext = &mut pps.range_extension; + if pps.transform_skip_enabled_flag { + ext.log2_max_transform_skip_block_size_minus2 = + r.ue_max(3, "log2_max_transform_skip_block_size_minus2")? as u8; + } + ext.cross_component_prediction_enabled_flag = r.flag()?; + ext.chroma_qp_offset_list_enabled_flag = r.flag()?; + if ext.chroma_qp_offset_list_enabled_flag { + ext.diff_cu_chroma_qp_offset_depth = + r.ue_max(3, "diff_cu_chroma_qp_offset_depth")? as u8; + ext.chroma_qp_offset_list_len_minus1 = + r.ue_max(5, "chroma_qp_offset_list_len_minus1")? as u8; + for i in 0..=ext.chroma_qp_offset_list_len_minus1 as usize { + ext.cb_qp_offset_list[i] = r.se_range(-12, 12, "cb_qp_offset_list")? as i8; + ext.cr_qp_offset_list[i] = r.se_range(-12, 12, "cr_qp_offset_list")? as i8; + } + } + ext.log2_sao_offset_scale_luma = r.ue_max(6, "log2_sao_offset_scale_luma")? as u8; + ext.log2_sao_offset_scale_chroma = r.ue_max(6, "log2_sao_offset_scale_chroma")? as u8; + } + // As in the SPS, the remaining extensions are recorded by flag only. + Ok(pps) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn uniform_tiles_spread_the_remainder() { + // 10 CTBs over 3 columns: 3, 3, 4 (equation 6-3). + assert_eq!(tile_sizes(true, true, 2, &[], 10), vec![3, 3, 4]); + assert_eq!(tile_sizes(false, false, 0, &[], 10), vec![10]); + // Explicit widths: the last column takes the rest. + assert_eq!(tile_sizes(true, false, 2, &[1, 3], 10), vec![2, 4, 4]); + } +} diff --git a/src/decoder/h265/rps.rs b/src/decoder/h265/rps.rs new file mode 100644 index 00000000..622e23f9 --- /dev/null +++ b/src/decoder/h265/rps.rs @@ -0,0 +1,263 @@ +//! `st_ref_pic_set()` (clause 7.3.7) and its semantics (clause 7.4.8). +//! +//! Each set is kept twice: as the syntax that was coded, which is what +//! `StdVideoH265ShortTermRefPicSet` carries, and as the derived +//! `DeltaPocS0`/`DeltaPocS1`/`UsedByCurrPicS0`/`UsedByCurrPicS1` lists, which +//! is what the reference picture set derivation (clause 8.3.2) consumes. With +//! inter RPS prediction the two differ: the syntax is a delta against another +//! set, and only the derived form says which pictures are meant. + +use super::parser::{Reader, invalid}; +use crate::error::Result; + +/// The most entries a short-term set can have (`MaxDpbSize - 1` is at most 15, +/// and Vulkan sizes its arrays at 16). +pub(crate) const MAX_ST_REFS: usize = 16; + +/// `st_ref_pic_set()` as coded. +/// +/// Bit masks hold one flag per entry, bit `j` for entry `j`, as the Vulkan +/// structure does. Only the fields of the branch that was coded are set: the +/// inter prediction fields when `inter_ref_pic_set_prediction_flag` is 1, the +/// explicit lists otherwise. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct StRefPicSetSyntax { + pub inter_ref_pic_set_prediction_flag: bool, + /// Coded only in a slice header; 0 (inferred) in the SPS. + pub delta_idx_minus1: u32, + pub delta_rps_sign: bool, + pub abs_delta_rps_minus1: u32, + /// `used_by_curr_pic_flag[j]` for j in `0..=NumDeltaPocs[RefRpsIdx]`. + /// + /// That can be 17 entries, one more than the `u16` Vulkan uses holds; a + /// set that needs bit 16 cannot be described to Vulkan exactly. + pub used_by_curr_pic_flag: u32, + /// `use_delta_flag[j]`, with the inferred 1s filled in. + pub use_delta_flag: u32, + pub num_negative_pics: u8, + pub num_positive_pics: u8, + pub delta_poc_s0_minus1: [u16; MAX_ST_REFS], + pub delta_poc_s1_minus1: [u16; MAX_ST_REFS], + pub used_by_curr_pic_s0_flag: u16, + pub used_by_curr_pic_s1_flag: u16, +} + +/// The derived form of a short-term reference picture set (equations 7-61 +/// to 7-64): POC deltas relative to the current picture, closest first. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct StRps { + /// `DeltaPocS0`, all negative, in decreasing order. + pub delta_poc_s0: Vec, + /// `UsedByCurrPicS0`. + pub used_s0: Vec, + /// `DeltaPocS1`, all positive, in increasing order. + pub delta_poc_s1: Vec, + /// `UsedByCurrPicS1`. + pub used_s1: Vec, +} + +impl StRps { + pub fn num_negative_pics(&self) -> usize { + self.delta_poc_s0.len() + } + + pub fn num_positive_pics(&self) -> usize { + self.delta_poc_s1.len() + } + + /// `NumDeltaPocs`. + pub fn num_delta_pocs(&self) -> usize { + self.delta_poc_s0.len() + self.delta_poc_s1.len() + } +} + +/// One short-term reference picture set, coded and derived. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct StRefPicSet { + pub syntax: StRefPicSetSyntax, + pub rps: StRps, + /// `RefRpsIdx` when inter predicted, which Vulkan's + /// `NumDeltaPocsOfRefRpsIdx` is derived from. + pub ref_rps_idx: Option, +} + +/// Parse `st_ref_pic_set(st_rps_idx)`. +/// +/// `previous` holds the sets already parsed from the SPS (all of them, when +/// this is the set in a slice header, where `st_rps_idx` equals +/// `num_short_term_ref_pic_sets`). `max_refs` bounds the set's size; it is +/// `sps_max_dec_pic_buffering_minus1` of the highest sub-layer. +pub(crate) fn parse_st_ref_pic_set( + r: &mut Reader, + st_rps_idx: usize, + num_short_term_ref_pic_sets: usize, + previous: &[StRefPicSet], + max_refs: usize, +) -> Result { + let mut set = StRefPicSet::default(); + let syntax = &mut set.syntax; + if st_rps_idx != 0 { + syntax.inter_ref_pic_set_prediction_flag = r.flag()?; + } + + if syntax.inter_ref_pic_set_prediction_flag { + if st_rps_idx == num_short_term_ref_pic_sets { + syntax.delta_idx_minus1 = r.ue_max(st_rps_idx as u32 - 1, "delta_idx_minus1")?; + } + syntax.delta_rps_sign = r.flag()?; + syntax.abs_delta_rps_minus1 = r.ue_max(32767, "abs_delta_rps_minus1")?; + let ref_rps_idx = st_rps_idx - (syntax.delta_idx_minus1 as usize + 1); + let reference = &previous + .get(ref_rps_idx) + .ok_or_else(|| invalid("st_ref_pic_set references a missing set"))? + .rps; + set.ref_rps_idx = Some(ref_rps_idx); + + let num_delta_pocs = reference.num_delta_pocs(); + for j in 0..=num_delta_pocs { + let used = r.flag()?; + // use_delta_flag is inferred to be 1 when not present. + let use_delta = used || r.flag()?; + syntax.used_by_curr_pic_flag |= (used as u32) << j; + syntax.use_delta_flag |= (use_delta as u32) << j; + } + let delta_rps = + (1 - 2 * syntax.delta_rps_sign as i32) * (syntax.abs_delta_rps_minus1 as i32 + 1); + set.rps = predict_rps( + reference, + delta_rps, + syntax.used_by_curr_pic_flag, + syntax.use_delta_flag, + ); + if set.rps.num_delta_pocs() > max_refs.min(MAX_ST_REFS) { + return Err(invalid("predicted st_ref_pic_set has too many entries")); + } + return Ok(set); + } + + let limit = max_refs.min(MAX_ST_REFS) as u32; + syntax.num_negative_pics = r.ue_max(limit, "num_negative_pics")? as u8; + syntax.num_positive_pics = + r.ue_max(limit - syntax.num_negative_pics as u32, "num_positive_pics")? as u8; + + let mut poc = 0i32; + for i in 0..syntax.num_negative_pics as usize { + let minus1 = r.ue_max(32767, "delta_poc_s0_minus1")?; + let used = r.flag()?; + syntax.delta_poc_s0_minus1[i] = minus1 as u16; + syntax.used_by_curr_pic_s0_flag |= (used as u16) << i; + poc -= minus1 as i32 + 1; + set.rps.delta_poc_s0.push(poc); + set.rps.used_s0.push(used); + } + poc = 0; + for i in 0..syntax.num_positive_pics as usize { + let minus1 = r.ue_max(32767, "delta_poc_s1_minus1")?; + let used = r.flag()?; + syntax.delta_poc_s1_minus1[i] = minus1 as u16; + syntax.used_by_curr_pic_s1_flag |= (used as u16) << i; + poc += minus1 as i32 + 1; + set.rps.delta_poc_s1.push(poc); + set.rps.used_s1.push(used); + } + Ok(set) +} + +/// Equations 7-61 and 7-62: derive a set from `reference` shifted by +/// `delta_rps`, keeping the entries `use_delta` selects. +/// +/// The flags are indexed as the syntax indexes them: the reference's negative +/// entries first, then its positive entries, then one for the reference +/// picture itself (at `NumDeltaPocs[RefRpsIdx]`). +fn predict_rps(reference: &StRps, delta_rps: i32, used: u32, use_delta: u32) -> StRps { + let flag = |mask: u32, j: usize| mask & (1 << j) != 0; + let num_negative = reference.num_negative_pics(); + let num_delta = reference.num_delta_pocs(); + let mut out = StRps::default(); + + // Negative pictures, closest first. + for j in (0..reference.num_positive_pics()).rev() { + let d_poc = reference.delta_poc_s1[j] + delta_rps; + if d_poc < 0 && flag(use_delta, num_negative + j) { + out.delta_poc_s0.push(d_poc); + out.used_s0.push(flag(used, num_negative + j)); + } + } + if delta_rps < 0 && flag(use_delta, num_delta) { + out.delta_poc_s0.push(delta_rps); + out.used_s0.push(flag(used, num_delta)); + } + for j in 0..num_negative { + let d_poc = reference.delta_poc_s0[j] + delta_rps; + if d_poc < 0 && flag(use_delta, j) { + out.delta_poc_s0.push(d_poc); + out.used_s0.push(flag(used, j)); + } + } + + // Positive pictures, closest first. + for j in (0..num_negative).rev() { + let d_poc = reference.delta_poc_s0[j] + delta_rps; + if d_poc > 0 && flag(use_delta, j) { + out.delta_poc_s1.push(d_poc); + out.used_s1.push(flag(used, j)); + } + } + if delta_rps > 0 && flag(use_delta, num_delta) { + out.delta_poc_s1.push(delta_rps); + out.used_s1.push(flag(used, num_delta)); + } + for j in 0..reference.num_positive_pics() { + let d_poc = reference.delta_poc_s1[j] + delta_rps; + if d_poc > 0 && flag(use_delta, num_negative + j) { + out.delta_poc_s1.push(d_poc); + out.used_s1.push(flag(used, num_negative + j)); + } + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + fn rps(s0: &[(i32, bool)], s1: &[(i32, bool)]) -> StRps { + StRps { + delta_poc_s0: s0.iter().map(|e| e.0).collect(), + used_s0: s0.iter().map(|e| e.1).collect(), + delta_poc_s1: s1.iter().map(|e| e.0).collect(), + used_s1: s1.iter().map(|e| e.1).collect(), + } + } + + /// The reference set {-1, +1, +3} predicted with deltaRps = -2 gives + /// {-1 - 2, 1 - 2, 3 - 2} plus the reference picture itself at -2, which + /// sorted closest first is S0 = {-1, -2, -3}, S1 = {+1}. + #[test] + fn inter_prediction_shifts_and_resorts() { + let reference = rps(&[(-1, true)], &[(1, true), (3, true)]); + // Four flags: S0[0], S1[0], S1[1], then the reference picture. + let all = 0b1111; + let out = predict_rps(&reference, -2, all, all); + assert_eq!(out.delta_poc_s0, vec![-1, -2, -3]); + assert_eq!(out.delta_poc_s1, vec![1]); + assert!(out.used_s0.iter().chain(&out.used_s1).all(|u| *u)); + } + + /// use_delta_flag drops entries, and used_by_curr_pic_flag follows each + /// entry to its new position. + #[test] + fn inter_prediction_honours_flags() { + let reference = rps(&[(-1, true), (-3, true)], &[(2, true)]); + // deltaRps = +1: S0[0] -> 0 (dropped, never 0), S0[1] -> -2, + // S1[0] -> 3, the reference picture -> +1. + // Drop S1[0] with use_delta_flag; mark the reference picture unused. + let use_delta = 0b1011; + let used = 0b0011; + let out = predict_rps(&reference, 1, used, use_delta); + assert_eq!(out.delta_poc_s0, vec![-2]); + assert_eq!(out.used_s0, vec![true]); + assert_eq!(out.delta_poc_s1, vec![1]); + assert_eq!(out.used_s1, vec![false]); + } +} diff --git a/src/decoder/h265/scaling.rs b/src/decoder/h265/scaling.rs new file mode 100644 index 00000000..b469902e --- /dev/null +++ b/src/decoder/h265/scaling.rs @@ -0,0 +1,162 @@ +//! `scaling_list_data()` (clause 7.3.4) and the default scaling lists +//! (clause 7.4.5, Tables 7-5 and 7-6). + +use super::parser::{Reader, invalid}; +use crate::error::Result; + +/// Table 7-6, intra (matrixId 0..=2), in coded (up-right diagonal) order. +const DEFAULT_8X8_INTRA: [u8; 64] = [ + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 16, 17, 16, 17, 18, 17, 18, 18, 17, 18, 21, 19, 20, + 21, 20, 19, 21, 24, 22, 22, 24, 24, 22, 22, 24, 25, 25, 27, 30, 27, 25, 25, 29, 31, 35, 35, 31, + 29, 36, 41, 44, 41, 36, 47, 54, 54, 47, 65, 70, 65, 88, 88, 115, +]; + +/// Table 7-6, inter (matrixId 3..=5), in coded (up-right diagonal) order. +const DEFAULT_8X8_INTER: [u8; 64] = [ + 16, 16, 16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 17, 18, 18, 18, 18, 18, 18, 20, 20, 20, + 20, 20, 20, 20, 24, 24, 24, 24, 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 28, 28, 28, 28, 28, + 28, 33, 33, 33, 33, 33, 41, 41, 41, 41, 54, 54, 54, 71, 71, 91, +]; + +/// A complete set of scaling lists, laid out like `StdVideoH265ScalingLists`. +/// +/// Coefficients are in coded order (the up-right diagonal scan the bitstream +/// uses), not raster order; that is the order the Vulkan structure carries +/// them in. The 32x32 lists hold matrixId 0 (intra luma) and 3 (inter luma) +/// at indices 0 and 1. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct ScalingLists { + pub scaling_list_4x4: [[u8; 16]; 6], + pub scaling_list_8x8: [[u8; 64]; 6], + pub scaling_list_16x16: [[u8; 64]; 6], + pub scaling_list_32x32: [[u8; 64]; 2], + pub scaling_list_dc_coef_16x16: [u8; 6], + pub scaling_list_dc_coef_32x32: [u8; 2], +} + +impl Default for ScalingLists { + /// The default lists of Tables 7-5 and 7-6, which apply when + /// `scaling_list_enabled_flag` is 1 and no list data is coded. + fn default() -> Self { + let mut lists = Self { + scaling_list_4x4: [[16; 16]; 6], + scaling_list_8x8: [[0; 64]; 6], + scaling_list_16x16: [[0; 64]; 6], + scaling_list_32x32: [[0; 64]; 2], + scaling_list_dc_coef_16x16: [16; 6], + scaling_list_dc_coef_32x32: [16; 2], + }; + for size_id in 1..4 { + for matrix_id in matrix_ids(size_id) { + lists.set_default(size_id, matrix_id); + } + } + lists + } +} + +/// The matrixId values coded for `size_id`: all six, or only 0 and 3 for 32x32. +fn matrix_ids(size_id: usize) -> impl Iterator { + (0..6).step_by(if size_id == 3 { 3 } else { 1 }) +} + +impl ScalingLists { + /// The coefficients of list (`size_id`, `matrix_id`). + pub fn list(&self, size_id: usize, matrix_id: usize) -> &[u8] { + match size_id { + 0 => &self.scaling_list_4x4[matrix_id], + 1 => &self.scaling_list_8x8[matrix_id], + 2 => &self.scaling_list_16x16[matrix_id], + _ => &self.scaling_list_32x32[matrix_id / 3], + } + } + + fn list_mut(&mut self, size_id: usize, matrix_id: usize) -> &mut [u8] { + match size_id { + 0 => &mut self.scaling_list_4x4[matrix_id], + 1 => &mut self.scaling_list_8x8[matrix_id], + 2 => &mut self.scaling_list_16x16[matrix_id], + _ => &mut self.scaling_list_32x32[matrix_id / 3], + } + } + + /// The DC coefficient of a 16x16 or 32x32 list (`size_id` 2 or 3). + pub fn dc(&self, size_id: usize, matrix_id: usize) -> u8 { + match size_id { + 2 => self.scaling_list_dc_coef_16x16[matrix_id], + 3 => self.scaling_list_dc_coef_32x32[matrix_id / 3], + _ => 16, + } + } + + fn set_dc(&mut self, size_id: usize, matrix_id: usize, value: u8) { + match size_id { + 2 => self.scaling_list_dc_coef_16x16[matrix_id] = value, + 3 => self.scaling_list_dc_coef_32x32[matrix_id / 3] = value, + _ => {} + } + } + + fn set_default(&mut self, size_id: usize, matrix_id: usize) { + let list = self.list_mut(size_id, matrix_id); + if size_id == 0 { + list.fill(16); + } else if matrix_id < 3 { + list.copy_from_slice(&DEFAULT_8X8_INTRA); + } else { + list.copy_from_slice(&DEFAULT_8X8_INTER); + } + self.set_dc(size_id, matrix_id, 16); + } +} + +/// Parse `scaling_list_data()`. +/// +/// For a list predicted from another (`scaling_list_pred_mode_flag` = 0) the +/// reference's coefficients and DC value are copied, or the default list is +/// used when `scaling_list_pred_matrix_id_delta` is 0, so the result is always +/// fully resolved. +pub(crate) fn parse_scaling_list_data(r: &mut Reader) -> Result { + let mut lists = ScalingLists::default(); + for size_id in 0..4 { + let step = if size_id == 3 { 3 } else { 1 }; + for matrix_id in matrix_ids(size_id) { + let pred_mode_flag = r.flag()?; + if !pred_mode_flag { + let delta = r.ue_max( + (matrix_id / step) as u32, + "scaling_list_pred_matrix_id_delta", + )? as usize; + if delta == 0 { + lists.set_default(size_id, matrix_id); + } else { + let ref_matrix_id = matrix_id - delta * step; + let reference = lists.list(size_id, ref_matrix_id).to_vec(); + lists + .list_mut(size_id, matrix_id) + .copy_from_slice(&reference); + let dc = lists.dc(size_id, ref_matrix_id); + lists.set_dc(size_id, matrix_id, dc); + } + continue; + } + + let mut next_coef = 8i32; + if size_id > 1 { + let dc_minus8 = r.se_range(-7, 247, "scaling_list_dc_coef_minus8")?; + next_coef = dc_minus8 + 8; + lists.set_dc(size_id, matrix_id, next_coef as u8); + } + let list = lists.list_mut(size_id, matrix_id); + for coef in list.iter_mut() { + let delta = r.se_range(-128, 127, "scaling_list_delta_coef")?; + next_coef = (next_coef + delta + 256) % 256; + if next_coef == 0 { + return Err(invalid("scaling list coefficient is 0")); + } + *coef = next_coef as u8; + } + } + } + Ok(lists) +} diff --git a/src/decoder/h265/slice.rs b/src/decoder/h265/slice.rs new file mode 100644 index 00000000..e78b25d3 --- /dev/null +++ b/src/decoder/h265/slice.rs @@ -0,0 +1,268 @@ +//! The leading part of the H.265 slice segment header (clause 7.3.6.1). +//! +//! Parsing stops after `slice_temporal_mvp_enabled_flag`. Everything the host +//! needs is before that point: the picture order count, the short-term and +//! long-term reference picture set syntax, and the bit count Vulkan wants for +//! the inline short-term set. What follows (SAO, reference list sizes and +//! modification, weighted prediction, QP and deblocking overrides, entry +//! points) is parsed by the driver from the slice NAL unit itself. + +use super::parser::{NalType, NalUnit, Reader, ceil_log2, invalid}; +use super::pps::Pps; +use super::rps::{MAX_ST_REFS, StRefPicSet, parse_st_ref_pic_set}; +use super::sps::Sps; +use crate::error::Result; + +/// H.265 slice types (Table 7-7). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) enum SliceType { + B, + P, + #[default] + I, +} + +/// One long-term reference picture entry of the slice header, resolved +/// against the SPS where it was an index into `lt_ref_pic_poc_lsb_sps`. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub(crate) struct LongTermRef { + /// `lt_idx_sps[i]` for an entry taken from the SPS, `None` for one coded + /// in the slice header. + pub lt_idx_sps: Option, + /// `PocLsbLt[i]`. + pub poc_lsb_lt: u32, + /// `UsedByCurrPicLt[i]`. + pub used_by_curr_pic_lt: bool, + pub delta_poc_msb_present_flag: bool, + /// `delta_poc_msb_cycle_lt[i]` as coded. + pub delta_poc_msb_cycle_lt: u32, + /// `DeltaPocMsbCycleLt[i]` (equation 7-52), which accumulates the coded + /// values within each of the two groups (SPS entries, then slice entries). + pub delta_poc_msb_cycle_lt_accumulated: u32, +} + +/// The slice segment header fields the host needs. +/// +/// A dependent slice segment (`dependent_slice_segment_flag`) codes only the +/// fields up to `slice_segment_address`; the rest of its header is that of the +/// preceding independent segment, and is left at defaults here. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct SliceHeader { + pub first_slice_segment_in_pic_flag: bool, + /// Coded only for IRAP pictures. + pub no_output_of_prior_pics_flag: bool, + pub slice_pic_parameter_set_id: u8, + pub dependent_slice_segment_flag: bool, + pub slice_segment_address: u32, + pub slice_type: SliceType, + /// 1 when not coded (`output_flag_present_flag` = 0). + pub pic_output_flag: bool, + pub colour_plane_id: u8, + /// 0 for IDR pictures, which do not code it. + pub slice_pic_order_cnt_lsb: u32, + pub short_term_ref_pic_set_sps_flag: bool, + pub short_term_ref_pic_set_idx: u8, + /// The set coded in the slice header, when + /// `short_term_ref_pic_set_sps_flag` is 0 (and the picture is not IDR). + pub st_ref_pic_set: Option, + /// RBSP bits the inline `st_ref_pic_set()` occupied: Vulkan's + /// `NumBitsForSTRefPicSetInSlice`. 0 when the set came from the SPS. + pub num_bits_for_st_ref_pic_set: u32, + pub num_long_term_sps: u8, + pub num_long_term_pics: u8, + /// `num_long_term_sps + num_long_term_pics` entries, SPS ones first. + pub long_term_refs: Vec, + pub slice_temporal_mvp_enabled_flag: bool, +} + +impl SliceHeader { + /// The short-term set in effect: the inline one, or the SPS set + /// `short_term_ref_pic_set_idx` selects. `None` for an IDR picture, whose + /// reference picture set is empty. + pub fn short_term_ref_pic_set<'a>(&'a self, sps: &'a Sps) -> Option<&'a StRefPicSet> { + if self.short_term_ref_pic_set_sps_flag { + sps.short_term_ref_pic_sets + .get(self.short_term_ref_pic_set_idx as usize) + } else { + self.st_ref_pic_set.as_ref() + } + } + + /// Vulkan's `NumDeltaPocsOfRefRpsIdx`: `NumDeltaPocs[RefRpsIdx]` when the + /// slice header codes its own set with inter RPS prediction, else 0. + pub fn num_delta_pocs_of_ref_rps_idx(&self, sps: &Sps) -> u8 { + let Some(set) = self.st_ref_pic_set.as_ref() else { + return 0; + }; + set.ref_rps_idx + .and_then(|idx| sps.short_term_ref_pic_sets.get(idx)) + .map_or(0, |reference| reference.rps.num_delta_pocs() as u8) + } +} + +/// The PPS a slice segment refers to, read without parsing the rest of the +/// header, so the caller can find the PPS (and through it the SPS) first. +pub(crate) fn slice_pps_id(nal: &NalUnit) -> Result { + let mut r = Reader::new(nal.payload()); + let _first_slice_segment_in_pic_flag = r.flag()?; + if nal.nal_type().is_irap() { + let _no_output_of_prior_pics_flag = r.flag()?; + } + Ok(r.ue_max(63, "slice_pic_parameter_set_id")? as u8) +} + +/// Parse the leading part of a slice segment header; see the module +/// documentation for where it stops. +pub(crate) fn parse_slice_header(nal: &NalUnit, sps: &Sps, pps: &Pps) -> Result { + let nal_type = nal.nal_type(); + if !nal_type.is_vcl() { + return Err(invalid("slice header parse of a non-VCL NAL unit")); + } + let mut r = Reader::new(nal.payload()); + let mut h = SliceHeader { + first_slice_segment_in_pic_flag: r.flag()?, + pic_output_flag: true, + ..Default::default() + }; + if nal_type.is_irap() { + h.no_output_of_prior_pics_flag = r.flag()?; + } + h.slice_pic_parameter_set_id = r.ue_max(63, "slice_pic_parameter_set_id")? as u8; + if h.slice_pic_parameter_set_id != pps.pps_pic_parameter_set_id + || pps.pps_seq_parameter_set_id != sps.sps_seq_parameter_set_id + { + return Err(invalid("slice parsed against the wrong PPS or SPS")); + } + + if !h.first_slice_segment_in_pic_flag { + if pps.dependent_slice_segments_enabled_flag { + h.dependent_slice_segment_flag = r.flag()?; + } + let pic_size = sps.pic_size_in_ctbs(); + h.slice_segment_address = r.bits(ceil_log2(pic_size))?; + if h.slice_segment_address >= pic_size { + return Err(invalid("slice_segment_address outside the picture")); + } + } + if h.dependent_slice_segment_flag { + return Ok(h); + } + + // slice_reserved_flag[i]. + r.skip(pps.num_extra_slice_header_bits as u32)?; + h.slice_type = match r.ue()? { + 0 => SliceType::B, + 1 => SliceType::P, + 2 => SliceType::I, + other => return Err(invalid(format_args!("invalid slice_type {other}"))), + }; + if pps.output_flag_present_flag { + h.pic_output_flag = r.flag()?; + } + if sps.separate_colour_plane_flag { + h.colour_plane_id = r.bits(2)? as u8; + } + if nal_type.is_idr() { + return Ok(h); + } + + let lsb_bits = sps.log2_max_pic_order_cnt_lsb_minus4 as u32 + 4; + h.slice_pic_order_cnt_lsb = r.bits(lsb_bits)?; + h.short_term_ref_pic_set_sps_flag = r.flag()?; + let num_sets = sps.num_short_term_ref_pic_sets(); + if !h.short_term_ref_pic_set_sps_flag { + let start = r.position(); + let set = parse_st_ref_pic_set( + &mut r, + num_sets, + num_sets, + &sps.short_term_ref_pic_sets, + sps.max_dec_pic_buffering() - 1, + )?; + h.num_bits_for_st_ref_pic_set = (r.position() - start) as u32; + h.st_ref_pic_set = Some(set); + } else { + if num_sets == 0 { + return Err(invalid( + "slice selects an SPS short-term set, but the SPS has none", + )); + } + if num_sets > 1 { + h.short_term_ref_pic_set_idx = r.bits(ceil_log2(num_sets as u32))? as u8; + } + if h.short_term_ref_pic_set_idx as usize >= num_sets { + return Err(invalid("short_term_ref_pic_set_idx out of range")); + } + } + + if sps.long_term_ref_pics_present_flag { + parse_long_term_refs(&mut r, &mut h, sps)?; + } + if sps.sps_temporal_mvp_enabled_flag { + h.slice_temporal_mvp_enabled_flag = r.flag()?; + } + Ok(h) +} + +/// The long-term part of the slice header, from `num_long_term_sps` to the +/// last `delta_poc_msb_cycle_lt`. +fn parse_long_term_refs(r: &mut Reader, h: &mut SliceHeader, sps: &Sps) -> Result<()> { + let num_sps_entries = sps.num_long_term_ref_pics_sps(); + if num_sps_entries > 0 { + h.num_long_term_sps = r.ue_max(num_sps_entries as u32, "num_long_term_sps")? as u8; + } + h.num_long_term_pics = r.ue_max( + MAX_ST_REFS as u32 - h.num_long_term_sps as u32, + "num_long_term_pics", + )? as u8; + + let lsb_bits = sps.log2_max_pic_order_cnt_lsb_minus4 as u32 + 4; + let idx_bits = ceil_log2(num_sps_entries as u32); + let total = h.num_long_term_sps as usize + h.num_long_term_pics as usize; + let mut accumulated = 0u32; + for i in 0..total { + let mut entry = LongTermRef::default(); + if i < h.num_long_term_sps as usize { + let idx = if num_sps_entries > 1 { + r.bits(idx_bits)? + } else { + 0 + }; + let lsb = sps + .long_term_ref_pics + .lt_ref_pic_poc_lsb_sps + .get(idx as usize) + .ok_or_else(|| invalid("lt_idx_sps out of range"))?; + entry.lt_idx_sps = Some(idx as u8); + entry.poc_lsb_lt = *lsb; + entry.used_by_curr_pic_lt = + sps.long_term_ref_pics.used_by_curr_pic_lt_sps_flag & (1 << idx) != 0; + } else { + entry.poc_lsb_lt = r.bits(lsb_bits)?; + entry.used_by_curr_pic_lt = r.flag()?; + } + entry.delta_poc_msb_present_flag = r.flag()?; + if entry.delta_poc_msb_present_flag { + entry.delta_poc_msb_cycle_lt = r.ue()?; + } + // Equation 7-52: the sum restarts at the first slice-coded entry. + if i == 0 || i == h.num_long_term_sps as usize { + accumulated = entry.delta_poc_msb_cycle_lt; + } else { + accumulated = accumulated.saturating_add(entry.delta_poc_msb_cycle_lt); + } + entry.delta_poc_msb_cycle_lt_accumulated = accumulated; + h.long_term_refs.push(entry); + } + Ok(()) +} + +/// Whether `nal` starts a new coded picture: a slice segment of a known type +/// with `first_slice_segment_in_pic_flag` set, which is the first bit after +/// the NAL header. Reserved VCL types are ignored, as clause 7.4.2.2 requires. +pub(crate) fn starts_picture(nal: &NalUnit) -> bool { + let nal_type = nal.nal_type(); + nal_type.is_vcl() + && !matches!(nal_type, NalType::Other(_)) + && nal.payload().first().is_some_and(|b| b & 0x80 != 0) +} diff --git a/src/decoder/h265/sps.rs b/src/decoder/h265/sps.rs new file mode 100644 index 00000000..ded10eea --- /dev/null +++ b/src/decoder/h265/sps.rs @@ -0,0 +1,464 @@ +//! The H.265 sequence parameter set (clause 7.3.2.2) and its VUI (Annex E). +//! +//! Field names follow the standard, which are also the names +//! `StdVideoH265SequenceParameterSet` and its sub-structures use, so filling +//! the Vulkan structures is a field-by-field copy. + +use super::parser::{ + DecPicBufMgr, MAX_SUB_LAYERS, ProfileTierLevel, Reader, invalid, parse_dec_pic_buf_mgr, + parse_profile_tier_level, skip_hrd_parameters, +}; +use super::rps::{StRefPicSet, parse_st_ref_pic_set}; +use super::scaling::{ScalingLists, parse_scaling_list_data}; +use crate::error::Result; + +/// Most short-term reference picture sets an SPS may code. +pub(crate) const MAX_SPS_ST_REF_PIC_SETS: usize = 64; +/// Most long-term reference pictures an SPS may list. +pub(crate) const MAX_SPS_LT_REF_PICS: usize = 32; + +/// `vui_parameters()` (clause E.2.1), laid out like +/// `StdVideoH265SequenceParameterSetVui`. +/// +/// HRD parameters are parsed past but not kept; `vui_hrd_parameters_present_flag` +/// records whether there were any. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct Vui { + pub aspect_ratio_info_present_flag: bool, + pub aspect_ratio_idc: u8, + pub sar_width: u16, + pub sar_height: u16, + pub overscan_info_present_flag: bool, + pub overscan_appropriate_flag: bool, + pub video_signal_type_present_flag: bool, + pub video_format: u8, + pub video_full_range_flag: bool, + pub colour_description_present_flag: bool, + pub colour_primaries: u8, + pub transfer_characteristics: u8, + pub matrix_coeffs: u8, + pub chroma_loc_info_present_flag: bool, + pub chroma_sample_loc_type_top_field: u8, + pub chroma_sample_loc_type_bottom_field: u8, + pub neutral_chroma_indication_flag: bool, + pub field_seq_flag: bool, + pub frame_field_info_present_flag: bool, + pub default_display_window_flag: bool, + pub def_disp_win_left_offset: u16, + pub def_disp_win_right_offset: u16, + pub def_disp_win_top_offset: u16, + pub def_disp_win_bottom_offset: u16, + pub vui_timing_info_present_flag: bool, + pub vui_num_units_in_tick: u32, + pub vui_time_scale: u32, + pub vui_poc_proportional_to_timing_flag: bool, + pub vui_num_ticks_poc_diff_one_minus1: u32, + pub vui_hrd_parameters_present_flag: bool, + pub bitstream_restriction_flag: bool, + pub tiles_fixed_structure_flag: bool, + pub motion_vectors_over_pic_boundaries_flag: bool, + pub restricted_ref_pic_lists_flag: bool, + pub min_spatial_segmentation_idc: u16, + pub max_bytes_per_pic_denom: u8, + pub max_bits_per_min_cu_denom: u8, + pub log2_max_mv_length_horizontal: u8, + pub log2_max_mv_length_vertical: u8, +} + +/// Parse `vui_parameters()`. +fn parse_vui(r: &mut Reader, sps_max_sub_layers_minus1: u8) -> Result { + let mut vui = Vui { + aspect_ratio_info_present_flag: r.flag()?, + ..Default::default() + }; + if vui.aspect_ratio_info_present_flag { + vui.aspect_ratio_idc = r.bits(8)? as u8; + // EXTENDED_SAR. + if vui.aspect_ratio_idc == 255 { + vui.sar_width = r.bits(16)? as u16; + vui.sar_height = r.bits(16)? as u16; + } + } + vui.overscan_info_present_flag = r.flag()?; + if vui.overscan_info_present_flag { + vui.overscan_appropriate_flag = r.flag()?; + } + vui.video_signal_type_present_flag = r.flag()?; + if vui.video_signal_type_present_flag { + vui.video_format = r.bits(3)? as u8; + vui.video_full_range_flag = r.flag()?; + vui.colour_description_present_flag = r.flag()?; + if vui.colour_description_present_flag { + vui.colour_primaries = r.bits(8)? as u8; + vui.transfer_characteristics = r.bits(8)? as u8; + vui.matrix_coeffs = r.bits(8)? as u8; + } + } + vui.chroma_loc_info_present_flag = r.flag()?; + if vui.chroma_loc_info_present_flag { + vui.chroma_sample_loc_type_top_field = r.ue_max(5, "chroma_sample_loc_type")? as u8; + vui.chroma_sample_loc_type_bottom_field = r.ue_max(5, "chroma_sample_loc_type")? as u8; + } + vui.neutral_chroma_indication_flag = r.flag()?; + vui.field_seq_flag = r.flag()?; + vui.frame_field_info_present_flag = r.flag()?; + vui.default_display_window_flag = r.flag()?; + if vui.default_display_window_flag { + vui.def_disp_win_left_offset = r.ue_max(u16::MAX as u32, "def_disp_win offset")? as u16; + vui.def_disp_win_right_offset = r.ue_max(u16::MAX as u32, "def_disp_win offset")? as u16; + vui.def_disp_win_top_offset = r.ue_max(u16::MAX as u32, "def_disp_win offset")? as u16; + vui.def_disp_win_bottom_offset = r.ue_max(u16::MAX as u32, "def_disp_win offset")? as u16; + } + vui.vui_timing_info_present_flag = r.flag()?; + if vui.vui_timing_info_present_flag { + vui.vui_num_units_in_tick = r.bits(32)?; + vui.vui_time_scale = r.bits(32)?; + vui.vui_poc_proportional_to_timing_flag = r.flag()?; + if vui.vui_poc_proportional_to_timing_flag { + vui.vui_num_ticks_poc_diff_one_minus1 = r.ue()?; + } + vui.vui_hrd_parameters_present_flag = r.flag()?; + if vui.vui_hrd_parameters_present_flag { + skip_hrd_parameters(r, true, sps_max_sub_layers_minus1)?; + } + } + vui.bitstream_restriction_flag = r.flag()?; + if vui.bitstream_restriction_flag { + vui.tiles_fixed_structure_flag = r.flag()?; + vui.motion_vectors_over_pic_boundaries_flag = r.flag()?; + vui.restricted_ref_pic_lists_flag = r.flag()?; + vui.min_spatial_segmentation_idc = r.ue_max(4095, "min_spatial_segmentation_idc")? as u16; + vui.max_bytes_per_pic_denom = r.ue_max(16, "max_bytes_per_pic_denom")? as u8; + vui.max_bits_per_min_cu_denom = r.ue_max(16, "max_bits_per_min_cu_denom")? as u8; + vui.log2_max_mv_length_horizontal = r.ue_max(16, "log2_max_mv_length_horizontal")? as u8; + vui.log2_max_mv_length_vertical = r.ue_max(15, "log2_max_mv_length_vertical")? as u8; + } + Ok(vui) +} + +/// `sps_range_extension()` (clause 7.3.2.2.2). +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct SpsRangeExtension { + pub transform_skip_rotation_enabled_flag: bool, + pub transform_skip_context_enabled_flag: bool, + pub implicit_rdpcm_enabled_flag: bool, + pub explicit_rdpcm_enabled_flag: bool, + pub extended_precision_processing_flag: bool, + pub intra_smoothing_disabled_flag: bool, + pub high_precision_offsets_enabled_flag: bool, + pub persistent_rice_adaptation_enabled_flag: bool, + pub cabac_bypass_alignment_enabled_flag: bool, +} + +/// Long-term reference pictures listed in the SPS, as +/// `StdVideoH265LongTermRefPicsSps` carries them. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct LongTermRefPicsSps { + /// `lt_ref_pic_poc_lsb_sps[i]`, `num_long_term_ref_pics_sps` entries. + pub lt_ref_pic_poc_lsb_sps: Vec, + /// `used_by_curr_pic_lt_sps_flag[i]`, bit `i`. + pub used_by_curr_pic_lt_sps_flag: u32, +} + +/// Parsed H.265 sequence parameter set. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub(crate) struct Sps { + pub sps_video_parameter_set_id: u8, + pub sps_max_sub_layers_minus1: u8, + pub sps_temporal_id_nesting_flag: bool, + pub profile_tier_level: ProfileTierLevel, + pub sps_seq_parameter_set_id: u8, + pub chroma_format_idc: u8, + pub separate_colour_plane_flag: bool, + pub pic_width_in_luma_samples: u32, + pub pic_height_in_luma_samples: u32, + pub conformance_window_flag: bool, + pub conf_win_left_offset: u32, + pub conf_win_right_offset: u32, + pub conf_win_top_offset: u32, + pub conf_win_bottom_offset: u32, + pub bit_depth_luma_minus8: u8, + pub bit_depth_chroma_minus8: u8, + pub log2_max_pic_order_cnt_lsb_minus4: u8, + pub sps_sub_layer_ordering_info_present_flag: bool, + /// Filled for every sub-layer up to `sps_max_sub_layers_minus1`. + pub dec_pic_buf_mgr: DecPicBufMgr, + pub log2_min_luma_coding_block_size_minus3: u8, + pub log2_diff_max_min_luma_coding_block_size: u8, + pub log2_min_luma_transform_block_size_minus2: u8, + pub log2_diff_max_min_luma_transform_block_size: u8, + pub max_transform_hierarchy_depth_inter: u8, + pub max_transform_hierarchy_depth_intra: u8, + pub scaling_list_enabled_flag: bool, + pub sps_scaling_list_data_present_flag: bool, + /// The lists in effect for the sequence: the coded ones when + /// `sps_scaling_list_data_present_flag` is set, the defaults when only + /// `scaling_list_enabled_flag` is, `None` when scaling lists are off (flat + /// scaling). A PPS may override them. + pub scaling_lists: Option, + pub amp_enabled_flag: bool, + pub sample_adaptive_offset_enabled_flag: bool, + pub pcm_enabled_flag: bool, + pub pcm_sample_bit_depth_luma_minus1: u8, + pub pcm_sample_bit_depth_chroma_minus1: u8, + pub log2_min_pcm_luma_coding_block_size_minus3: u8, + pub log2_diff_max_min_pcm_luma_coding_block_size: u8, + pub pcm_loop_filter_disabled_flag: bool, + /// The `num_short_term_ref_pic_sets` sets coded in the SPS. + pub short_term_ref_pic_sets: Vec, + pub long_term_ref_pics_present_flag: bool, + pub long_term_ref_pics: LongTermRefPicsSps, + pub sps_temporal_mvp_enabled_flag: bool, + pub strong_intra_smoothing_enabled_flag: bool, + pub vui_parameters_present_flag: bool, + pub vui: Option, + pub sps_extension_present_flag: bool, + pub sps_range_extension_flag: bool, + pub sps_multilayer_extension_flag: bool, + pub sps_3d_extension_flag: bool, + pub sps_scc_extension_flag: bool, + pub sps_extension_4bits: u8, + pub range_extension: SpsRangeExtension, +} + +impl Sps { + /// `num_short_term_ref_pic_sets`. + pub fn num_short_term_ref_pic_sets(&self) -> usize { + self.short_term_ref_pic_sets.len() + } + + /// `num_long_term_ref_pics_sps`. + pub fn num_long_term_ref_pics_sps(&self) -> usize { + self.long_term_ref_pics.lt_ref_pic_poc_lsb_sps.len() + } + + /// `MaxPicOrderCntLsb`. + pub fn max_pic_order_cnt_lsb(&self) -> u32 { + 1 << (self.log2_max_pic_order_cnt_lsb_minus4 as u32 + 4) + } + + /// `CtbLog2SizeY`. + pub fn ctb_log2_size(&self) -> u32 { + self.log2_min_luma_coding_block_size_minus3 as u32 + + 3 + + self.log2_diff_max_min_luma_coding_block_size as u32 + } + + /// `PicWidthInCtbsY`. + pub fn pic_width_in_ctbs(&self) -> u32 { + self.pic_width_in_luma_samples + .div_ceil(1 << self.ctb_log2_size()) + } + + /// `PicHeightInCtbsY`. + pub fn pic_height_in_ctbs(&self) -> u32 { + self.pic_height_in_luma_samples + .div_ceil(1 << self.ctb_log2_size()) + } + + /// `PicSizeInCtbsY`. + pub fn pic_size_in_ctbs(&self) -> u32 { + self.pic_width_in_ctbs() * self.pic_height_in_ctbs() + } + + /// `ChromaArrayType`: 0 for monochrome or separately coded colour planes. + pub fn chroma_array_type(&self) -> u8 { + if self.separate_colour_plane_flag { + 0 + } else { + self.chroma_format_idc + } + } + + /// `HighestTid` when every sub-layer is decoded. + pub fn highest_tid(&self) -> usize { + self.sps_max_sub_layers_minus1 as usize + } + + /// Pictures the DPB must hold (`sps_max_dec_pic_buffering_minus1 + 1` of + /// the highest sub-layer), the current picture included. + pub fn max_dec_pic_buffering(&self) -> usize { + self.dec_pic_buf_mgr.max_dec_pic_buffering_minus1[self.highest_tid()] as usize + 1 + } + + /// `sps_max_num_reorder_pics` of the highest sub-layer: how many pictures + /// can precede any picture in decoding order and follow it in output + /// order, which is the depth a reorder buffer needs. + pub fn reorder_depth(&self) -> usize { + self.dec_pic_buf_mgr.max_num_reorder_pics[self.highest_tid()] as usize + } + + /// `SpsMaxLatencyPictures` of the highest sub-layer, or `None` when + /// `sps_max_latency_increase_plus1` is 0 (no limit). + pub fn max_latency_pictures(&self) -> Option { + let tid = self.highest_tid(); + let plus1 = self.dec_pic_buf_mgr.max_latency_increase_plus1[tid] as usize; + (plus1 != 0).then(|| self.dec_pic_buf_mgr.max_num_reorder_pics[tid] as usize + plus1 - 1) + } + + /// Display (conformance-window cropped) dimensions. + /// + /// The offsets are in chroma sample units (clause 7.4.3.2.1). + pub fn display_dimensions(&self) -> (u32, u32) { + let (sub_width_c, sub_height_c) = match self.chroma_array_type() { + 1 => (2, 2), + 2 => (2, 1), + _ => (1, 1), + }; + let mut width = self.pic_width_in_luma_samples; + let mut height = self.pic_height_in_luma_samples; + if self.conformance_window_flag { + width = width.saturating_sub( + sub_width_c * (self.conf_win_left_offset + self.conf_win_right_offset), + ); + height = height.saturating_sub( + sub_height_c * (self.conf_win_top_offset + self.conf_win_bottom_offset), + ); + } + (width, height) + } +} + +/// Parse an SPS NAL payload (EBSP, after the 2-byte NAL header). +pub(crate) fn parse_sps(payload: &[u8]) -> Result { + let mut r = Reader::new(payload); + let mut sps = Sps { + sps_video_parameter_set_id: r.bits(4)? as u8, + sps_max_sub_layers_minus1: r.bits(3)? as u8, + sps_temporal_id_nesting_flag: r.flag()?, + ..Default::default() + }; + if sps.sps_max_sub_layers_minus1 as usize >= MAX_SUB_LAYERS { + return Err(invalid("SPS: sps_max_sub_layers_minus1 is 7")); + } + sps.profile_tier_level = parse_profile_tier_level(&mut r, sps.sps_max_sub_layers_minus1)?; + sps.sps_seq_parameter_set_id = r.ue_max(15, "sps_seq_parameter_set_id")? as u8; + sps.chroma_format_idc = r.ue_max(3, "chroma_format_idc")? as u8; + if sps.chroma_format_idc == 3 { + sps.separate_colour_plane_flag = r.flag()?; + } + sps.pic_width_in_luma_samples = r.ue_max(16888, "pic_width_in_luma_samples")?; + sps.pic_height_in_luma_samples = r.ue_max(16888, "pic_height_in_luma_samples")?; + if sps.pic_width_in_luma_samples == 0 || sps.pic_height_in_luma_samples == 0 { + return Err(invalid("SPS: zero picture size")); + } + sps.conformance_window_flag = r.flag()?; + if sps.conformance_window_flag { + sps.conf_win_left_offset = r.ue()?; + sps.conf_win_right_offset = r.ue()?; + sps.conf_win_top_offset = r.ue()?; + sps.conf_win_bottom_offset = r.ue()?; + } + sps.bit_depth_luma_minus8 = r.ue_max(8, "bit_depth_luma_minus8")? as u8; + sps.bit_depth_chroma_minus8 = r.ue_max(8, "bit_depth_chroma_minus8")? as u8; + sps.log2_max_pic_order_cnt_lsb_minus4 = + r.ue_max(12, "log2_max_pic_order_cnt_lsb_minus4")? as u8; + sps.sps_sub_layer_ordering_info_present_flag = r.flag()?; + sps.dec_pic_buf_mgr = parse_dec_pic_buf_mgr( + &mut r, + sps.sps_max_sub_layers_minus1, + sps.sps_sub_layer_ordering_info_present_flag, + )?; + + sps.log2_min_luma_coding_block_size_minus3 = + r.ue_max(3, "log2_min_luma_coding_block_size_minus3")? as u8; + sps.log2_diff_max_min_luma_coding_block_size = + r.ue_max(3, "log2_diff_max_min_luma_coding_block_size")? as u8; + if sps.ctb_log2_size() > 6 { + return Err(invalid("SPS: CTB larger than 64x64")); + } + let min_cb = 1u32 << (sps.log2_min_luma_coding_block_size_minus3 + 3); + if !sps.pic_width_in_luma_samples.is_multiple_of(min_cb) + || !sps.pic_height_in_luma_samples.is_multiple_of(min_cb) + { + return Err(invalid( + "SPS: picture size is not a multiple of the minimum coding block", + )); + } + sps.log2_min_luma_transform_block_size_minus2 = + r.ue_max(3, "log2_min_luma_transform_block_size_minus2")? as u8; + sps.log2_diff_max_min_luma_transform_block_size = + r.ue_max(3, "log2_diff_max_min_luma_transform_block_size")? as u8; + sps.max_transform_hierarchy_depth_inter = + r.ue_max(4, "max_transform_hierarchy_depth_inter")? as u8; + sps.max_transform_hierarchy_depth_intra = + r.ue_max(4, "max_transform_hierarchy_depth_intra")? as u8; + + sps.scaling_list_enabled_flag = r.flag()?; + if sps.scaling_list_enabled_flag { + sps.sps_scaling_list_data_present_flag = r.flag()?; + sps.scaling_lists = Some(if sps.sps_scaling_list_data_present_flag { + parse_scaling_list_data(&mut r)? + } else { + ScalingLists::default() + }); + } + + sps.amp_enabled_flag = r.flag()?; + sps.sample_adaptive_offset_enabled_flag = r.flag()?; + sps.pcm_enabled_flag = r.flag()?; + if sps.pcm_enabled_flag { + sps.pcm_sample_bit_depth_luma_minus1 = r.bits(4)? as u8; + sps.pcm_sample_bit_depth_chroma_minus1 = r.bits(4)? as u8; + sps.log2_min_pcm_luma_coding_block_size_minus3 = + r.ue_max(2, "log2_min_pcm_luma_coding_block_size_minus3")? as u8; + sps.log2_diff_max_min_pcm_luma_coding_block_size = + r.ue_max(2, "log2_diff_max_min_pcm_luma_coding_block_size")? as u8; + sps.pcm_loop_filter_disabled_flag = r.flag()?; + } + + let num_sets = r.ue_max( + MAX_SPS_ST_REF_PIC_SETS as u32, + "num_short_term_ref_pic_sets", + )? as usize; + let max_refs = sps.max_dec_pic_buffering() - 1; + for i in 0..num_sets { + let set = + parse_st_ref_pic_set(&mut r, i, num_sets, &sps.short_term_ref_pic_sets, max_refs)?; + sps.short_term_ref_pic_sets.push(set); + } + + sps.long_term_ref_pics_present_flag = r.flag()?; + if sps.long_term_ref_pics_present_flag { + let count = r.ue_max(MAX_SPS_LT_REF_PICS as u32, "num_long_term_ref_pics_sps")?; + let lsb_bits = sps.log2_max_pic_order_cnt_lsb_minus4 as u32 + 4; + for i in 0..count { + sps.long_term_ref_pics + .lt_ref_pic_poc_lsb_sps + .push(r.bits(lsb_bits)?); + sps.long_term_ref_pics.used_by_curr_pic_lt_sps_flag |= (r.flag()? as u32) << i; + } + } + sps.sps_temporal_mvp_enabled_flag = r.flag()?; + sps.strong_intra_smoothing_enabled_flag = r.flag()?; + sps.vui_parameters_present_flag = r.flag()?; + if sps.vui_parameters_present_flag { + sps.vui = Some(parse_vui(&mut r, sps.sps_max_sub_layers_minus1)?); + } + + sps.sps_extension_present_flag = r.flag()?; + if sps.sps_extension_present_flag { + sps.sps_range_extension_flag = r.flag()?; + sps.sps_multilayer_extension_flag = r.flag()?; + sps.sps_3d_extension_flag = r.flag()?; + sps.sps_scc_extension_flag = r.flag()?; + sps.sps_extension_4bits = r.bits(4)? as u8; + } + if sps.sps_range_extension_flag { + sps.range_extension = SpsRangeExtension { + transform_skip_rotation_enabled_flag: r.flag()?, + transform_skip_context_enabled_flag: r.flag()?, + implicit_rdpcm_enabled_flag: r.flag()?, + explicit_rdpcm_enabled_flag: r.flag()?, + extended_precision_processing_flag: r.flag()?, + intra_smoothing_disabled_flag: r.flag()?, + high_precision_offsets_enabled_flag: r.flag()?, + persistent_rice_adaptation_enabled_flag: r.flag()?, + cabac_bypass_alignment_enabled_flag: r.flag()?, + }; + } + // The multilayer, 3D and SCC extensions follow. Nothing after them matters + // to a base-layer decoder, and their flags are recorded above, so parsing + // stops here. + Ok(sps) +} diff --git a/src/decoder/h265/syntax_tests.rs b/src/decoder/h265/syntax_tests.rs new file mode 100644 index 00000000..f0d50e81 --- /dev/null +++ b/src/decoder/h265/syntax_tests.rs @@ -0,0 +1,229 @@ +//! Slice header syntax that no fixture encoder produces: inter RPS prediction, +//! long-term pictures, and emulation prevention inside the short-term set. +//! The bitstreams are written by hand with a small bit writer. + +use super::parser::{NalType, NalUnit, Reader, parse_nal_header}; +use super::pps::Pps; +use super::rps::parse_st_ref_pic_set; +use super::slice::parse_slice_header; +use super::sps::{LongTermRefPicsSps, Sps}; + +/// Writes RBSP bits and produces an EBSP payload. +#[derive(Default)] +struct BitWriter { + bits: Vec, +} + +impl BitWriter { + fn u(&mut self, n: u32, v: u32) -> &mut Self { + for i in (0..n).rev() { + self.bits.push(v >> i & 1 != 0); + } + self + } + + fn flag(&mut self, v: bool) -> &mut Self { + self.u(1, v as u32) + } + + fn ue(&mut self, v: u32) -> &mut Self { + let code = v as u64 + 1; + let len = 64 - code.leading_zeros(); + self.u(len - 1, 0); + for i in (0..len).rev() { + self.bits.push(code >> i & 1 != 0); + } + self + } + + fn len(&self) -> usize { + self.bits.len() + } + + /// Append rbsp_trailing_bits() and insert emulation prevention bytes. + fn finish(&mut self) -> Vec { + self.flag(true); + while !self.bits.len().is_multiple_of(8) { + self.bits.push(false); + } + let rbsp: Vec = self + .bits + .chunks(8) + .map(|c| c.iter().fold(0u8, |b, bit| b << 1 | *bit as u8)) + .collect(); + let mut out = Vec::new(); + let mut zeros = 0; + for byte in rbsp { + if zeros >= 2 && byte <= 3 { + out.push(3); + zeros = 0; + } + out.push(byte); + zeros = if byte == 0 { zeros + 1 } else { 0 }; + } + out + } +} + +fn nal_bytes(nal_type: NalType, payload: &[u8]) -> Vec { + let mut data = vec![nal_type.raw() << 1, 1]; + data.extend_from_slice(payload); + data +} + +fn base_sps() -> Sps { + let mut sps = Sps { + pic_width_in_luma_samples: 64, + pic_height_in_luma_samples: 64, + log2_max_pic_order_cnt_lsb_minus4: 0, + ..Default::default() + }; + sps.dec_pic_buf_mgr.max_dec_pic_buffering_minus1 = [5; 7]; + sps +} + +/// The leading slice header bits of a P picture up to the short-term set. +fn slice_prefix(w: &mut BitWriter, lsb: u32) { + w.flag(true) // first_slice_segment_in_pic_flag + .ue(0) // slice_pic_parameter_set_id + .ue(1) // slice_type P + .u(4, lsb) + .flag(false); // short_term_ref_pic_set_sps_flag +} + +/// SPS set 0 explicit, set 1 predicted from it; the slice predicts from set +/// 0 with delta_idx_minus1 = 1. Checks the derived sets, the bit count and +/// NumDeltaPocsOfRefRpsIdx. +#[test] +fn inter_rps_prediction_in_sps_and_slice() { + let mut sps = base_sps(); + let mut w = BitWriter::default(); + // Set 0: S0 = {-1, -3}, S1 = {+2}, all used. + w.ue(2) + .ue(1) + .ue(0) + .flag(true) + .ue(1) + .flag(true) + .ue(1) + .flag(true); + // Set 1: inter, deltaRps = -1, all four entries used. + w.flag(true).flag(true).ue(0); + for _ in 0..4 { + w.flag(true); + } + let payload = w.finish(); + let mut r = Reader::new(&payload); + for i in 0..2 { + let set = parse_st_ref_pic_set(&mut r, i, 2, &sps.short_term_ref_pic_sets, 5).unwrap(); + sps.short_term_ref_pic_sets.push(set); + } + let set0 = &sps.short_term_ref_pic_sets[0].rps; + assert_eq!( + (set0.delta_poc_s0.clone(), set0.delta_poc_s1.clone()), + (vec![-1, -3], vec![2]) + ); + let set1 = &sps.short_term_ref_pic_sets[1]; + assert!(set1.syntax.inter_ref_pic_set_prediction_flag); + assert_eq!(set1.rps.delta_poc_s0, vec![-1, -2, -4]); + assert_eq!(set1.rps.delta_poc_s1, vec![1]); + + let mut w = BitWriter::default(); + slice_prefix(&mut w, 5); + let start = w.len(); + w.flag(true) // inter_ref_pic_set_prediction_flag + .ue(1) // delta_idx_minus1: RefRpsIdx = 0 + .flag(false) // delta_rps_sign: deltaRps = +1 + .ue(0); + // used_by_curr_pic_flag / use_delta_flag for j = 0..=3. + w.flag(true); + w.flag(false).flag(true); + w.flag(false).flag(false); + w.flag(true); + let rps_bits = w.len() - start; + let data = nal_bytes(NalType::TrailR, &w.finish()); + let nal = NalUnit { + header: parse_nal_header(&data).unwrap(), + data: &data, + }; + let h = parse_slice_header(&nal, &sps, &Pps::default()).unwrap(); + assert_eq!(h.num_bits_for_st_ref_pic_set as usize, rps_bits); + assert_eq!(rps_bits, 12); + assert_eq!(h.num_delta_pocs_of_ref_rps_idx(&sps), 3); + let set = h.st_ref_pic_set.as_ref().unwrap(); + assert_eq!(set.syntax.delta_idx_minus1, 1); + assert_eq!(set.syntax.used_by_curr_pic_flag, 0b1001); + assert_eq!(set.syntax.use_delta_flag, 0b1011); + assert_eq!(set.rps.delta_poc_s0, vec![-2]); + assert_eq!(set.rps.used_s0, vec![false]); + assert_eq!(set.rps.delta_poc_s1, vec![1]); + assert_eq!(set.rps.used_s1, vec![true]); +} + +/// Long-term entries from the SPS and the slice, with the MSB cycle sum +/// restarting at the first slice-coded entry (equation 7-52). +#[test] +fn long_term_slice_syntax() { + let mut sps = base_sps(); + sps.long_term_ref_pics_present_flag = true; + sps.long_term_ref_pics = LongTermRefPicsSps { + lt_ref_pic_poc_lsb_sps: vec![5, 9, 13], + used_by_curr_pic_lt_sps_flag: 0b010, + }; + sps.sps_temporal_mvp_enabled_flag = true; + + let mut w = BitWriter::default(); + slice_prefix(&mut w, 15); + w.ue(1).ue(0).ue(0).flag(true); // S0 = {-1} + w.ue(1).ue(2); // num_long_term_sps, num_long_term_pics + w.u(2, 1).flag(true).ue(2); // lt_idx_sps 1, msb cycle 2 + w.u(4, 7).flag(false).flag(true).ue(1); // slice entry, msb cycle 1 + w.u(4, 3).flag(true).flag(true).ue(4); // slice entry, msb cycle 4 + w.flag(true); // slice_temporal_mvp_enabled_flag + let data = nal_bytes(NalType::TrailR, &w.finish()); + let nal = NalUnit { + header: parse_nal_header(&data).unwrap(), + data: &data, + }; + let h = parse_slice_header(&nal, &sps, &Pps::default()).unwrap(); + assert_eq!((h.num_long_term_sps, h.num_long_term_pics), (1, 2)); + let lt = &h.long_term_refs; + assert_eq!(lt[0].lt_idx_sps, Some(1)); + assert_eq!((lt[0].poc_lsb_lt, lt[0].used_by_curr_pic_lt), (9, true)); + assert_eq!((lt[1].poc_lsb_lt, lt[1].used_by_curr_pic_lt), (7, false)); + assert_eq!((lt[2].poc_lsb_lt, lt[2].used_by_curr_pic_lt), (3, true)); + let acc: Vec = lt + .iter() + .map(|e| e.delta_poc_msb_cycle_lt_accumulated) + .collect(); + assert_eq!(acc, vec![2, 1, 5]); + assert!(h.slice_temporal_mvp_enabled_flag); +} + +/// Long zero runs inside the short-term set force emulation prevention +/// bytes; the bit count must not include them. +#[test] +fn rps_bit_count_excludes_emulation_prevention() { + let sps = base_sps(); + let mut w = BitWriter::default(); + slice_prefix(&mut w, 0); + let start = w.len(); + w.ue(2).ue(0); + w.ue(32767).flag(false).ue(32767).flag(true); + let rps_bits = w.len() - start; + let payload = w.finish(); + assert!( + payload.windows(3).any(|b| b == [0, 0, 3]), + "the test needs an emulation prevention byte" + ); + let data = nal_bytes(NalType::TrailR, &payload); + let nal = NalUnit { + header: parse_nal_header(&data).unwrap(), + data: &data, + }; + let h = parse_slice_header(&nal, &sps, &Pps::default()).unwrap(); + assert_eq!(h.num_bits_for_st_ref_pic_set as usize, rps_bits); + let set = &h.st_ref_pic_set.as_ref().unwrap().rps; + assert_eq!(set.delta_poc_s0, vec![-32768, -65536]); + assert_eq!(set.used_s0, vec![false, true]); +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index c8c6f99e..e65e5040 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -52,6 +52,9 @@ pub(crate) mod driver; pub(crate) mod frames; /// H.264 decoding. pub(crate) mod h264; +/// H.265 parsing and reference bookkeeping. Nothing uses it yet. +#[allow(dead_code)] +pub(crate) mod h265; pub(crate) mod pipeline; /// Turning what a stream needs into a session plan. pub(crate) mod plan; diff --git a/tests/data/closedgop.265 b/tests/data/closedgop.265 new file mode 100644 index 0000000000000000000000000000000000000000..53e57a8b55f4dea6ac4cc85fef0c322f293be054 GIT binary patch literal 10845 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 literal 0 HcmV?d00001 diff --git a/tests/data/cra_start.265 b/tests/data/cra_start.265 new file mode 100644 index 0000000000000000000000000000000000000000..61565eee30aa088071ea755284b6ae92bf8d2314 GIT binary patch literal 1222 zcmV;%1UdTv0003%0Sp2E{{dhC00RJ!0009300RI#m6Qnp0003(0RaJE00093kN^M! z00093JfIdJBw1x;l1#4wXb1oR0{{pB00RKm&&i60Oh-o&ECD2pme>#T_>9*~q< z6FdK~lWO~L6VTl4P!)8b6J_MWefHb-n=15}aJ11NHLQ*`=6_;1Chz9e#wIyVAy_d{ z#1mQ${C;2)up7i}_l55MK^4GW*&}^^&0Vy`JdXccG50Yh7$eZW*1C2Edjig5^0ujr zsuK#lct#S{L93SEfysDf;xFdAZMVr4h+291DNiw-;bB~zA3EsTZp8NVn;sm0V-qvD ziLM_hH^siCfK?{piK(JrG98&VoXfdAg}#_{*eM$My(n#Js-)@D(R}8ME*Nf^RA(O| zed1=MjO@6=?$Xdtgc2kT6vOV5IkCcZ|We{&cAJ& zT6I?EIzrPNyS_8tlv!oIjd3QO^`B7Bq1Q4A^$4YZ^C7kTDG6-yVxa`I@@Y`U@U{2P z!4{(yKCfWzH08KN*pzk7OYXU;FE6kqJtB4Q^pr zXl*yCRzw|bXl#hj6avu?wpKrLP1WofCo{ofvYt5XEtiI@yT80xAo~WDk-wWL5 z`MtYpN4j>{{C0Ig&^3&ecN)iF&&1R@pb$Xh)3|wd;2rn>RmB?L-K&OxM2LloiP{md zp6xSJJewJ+p`zJO0E>N~gvq?xUfj%(6+Qnhs!9IQN$5#R^_OTETDOKvO$~)GS+Q)c%NDOh{nL3q;sJ2TzYTyjY6Ghjp@ zt2VmtcL!IR{%G}SD9bGHA582mgS(y7Di9SHc!zn$fhl2?>)%+H&B^9vgqXCJrfR7` zct- z1#v(IW8eS)009C4(BVnp3}DL5lu-V!bl%Z)L$oqf8nEa@EQy&Y0X<{a z;1tdvmo$N5PC^Q6JdHU%000010s-Shm3Uyv+_3J(p|C)C{Tp=tCjo)50000100HA9 z{eFuER@8v!h8&F|Y9t6Xbl?C000962<7Tz}MhvZ~epkQ;0ELAh1c+b&000330nq53 kSKLDZgCinT{>l-OzyjjzsIYTT_A3idc#|SGA^-pY literal 0 HcmV?d00001 diff --git a/tests/data/main10.265 b/tests/data/main10.265 new file mode 100644 index 0000000000000000000000000000000000000000..0f7d2953460041c9f40dd2d61aff7338e1dfcdcf GIT binary patch literal 7586 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 ReR`Iq`hm^F?aJK3{{T51#2x?u literal 0 HcmV?d00001 diff --git a/tests/data/nvenc.265 b/tests/data/nvenc.265 new file mode 100644 index 0000000000000000000000000000000000000000..8cf900b0e2492d0ca7bb5bcd0bb16bdd1d0b2675 GIT binary patch literal 6781 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 literal 0 HcmV?d00001 diff --git a/tests/data/opengop.265 b/tests/data/opengop.265 new file mode 100644 index 0000000000000000000000000000000000000000..4b8e77bdafcde29e26f26878f5e57184c284262e GIT binary patch literal 11033 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 B-EIH? literal 0 HcmV?d00001 diff --git a/tests/data/scaling.265 b/tests/data/scaling.265 new file mode 100644 index 0000000000000000000000000000000000000000..d464999fdc6b813dcddbed7e1a6afec62be902da GIT binary patch literal 4138 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 Ezxr{}o&W#< literal 0 HcmV?d00001 diff --git a/tests/data/slices.265 b/tests/data/slices.265 new file mode 100644 index 0000000000000000000000000000000000000000..e51a21f62781b0576210c72b6b6380242dad79a5 GIT binary patch literal 8572 zcmeHLc|26@+c(yVL}}4xWT`M1JDDg7Sqe!!S;ovUGtA5xb7m}+vP7~}BoZwWQTnB* zw8&Dl(LzO*hX`#-p)AFFpK~V9?|DC;_mB7ge0=8I*L5%Fx~}i{x@PW6OiYYmO^_o@ zO%eRX#3aNHqaWbk^6252Vqk7Pf$+a3PA?#;dqMOxr%MFp40I{>F=&mhwk1^a%hrP- z!G$0-CHniWbLQ`vR~Oo~-SRj)xwq!0OSplFF>zIZkj17G4alaei6o+tuA#+WPAsl4 z+~+Sl8yh0oM3<~f_W8@ih#0^U_z=y&r#4w1`!oQbV2iyjI7ZKY!!{ct4W>ghpt6B^ z5quVdDIgl?lMPAW!;H9!wwmZHWD8g{m=A?f`H&9Lfr~C?3Ise0Jw4PIU6{`R8ki?w z!Cb_GNaG1vbP5^#=^L681Nl@AL=rIh5S5NljEG@89+3-^cpUIne1Ja?E*}*UgvBAU zVFrl}g+gqKKI#A+;t7}(Ga`%26ADN)ghwG019=F=P+yNMQt}~$LN+xCCmR_7jR4}a zsWb@e7Qg~3n}peb01-Qs#G=!{m>CNilA6Ez0n z@L)c$hfR$D+kpn=L0l38<`Fq8E-4s_0HyfJCKJ zc>)SL0~1K0zm@tW)f)vAq7wjwVfs;A*`Jc012$(Lo@;Uq;h~`)NqnO zG-+r+1VaoS5{@ZZqyUHhR$O@aDkIF5+B4tS0A?va0SRjgvH=8 zSvZ{pNH7Z!jE|d&+K&wZ%GeeFCIVbwaUnXsf(S&I%fJN^!hvA{lf`9Vc5@&;0|Iow zO#)s6j-h_xGD={C$boQ;DLu(WAfvBc^BygbCP)Kx`0oRw&p?g+ZD@ zR6aX`gaw0-hXmL_1^8kRqQ_v7!gQzv0A#}m!iEq8Flu@c3cY|&7ac?gbfvNd5g?X4 zBm#xP0EGe{!y(hafDIs8Ajs&+)5p>RLU?c>2%tRsARcfA5l{sPDU8n&pgE08XgvV}i^r#exoh3>z94 z8>98pfP`vzFem~@07%PFAqx~cU^Gf~P?As?)F)gJ6$GsCfig}C;gNVetc0NwfD%w1 zP=}3$@b?r4{aPULs0c#BR)Gz_Ylo-=>FOJ!(L-kez332IKm|vmDhwO+>jD*+bUqo2 zKQ=&@qgwjEBo>{ImK8L%fYlHSlwOc12nzvPAp%WTiD@-L1*!0V%&}oAO3?q_2LNM3 zAQ@-^7_AgIdqHpjKK~N2IwG3ogaWihV^9i704A^-^$KhO9^hpHOifQzy=odXeilYBt-+dzXkyP@kL8Azc^j)@|+5_Ws=N>SyCsf1lsobLlZ3dLy%YbKFsn zn;tQ@2b2s8^d`JDjUu&~*IE^e!^rfDsmDC`sF~ef8fTuNGdCqga@)dPZ3EwaZFwm9 z@NC)Qvi!$;6-�#c#|RwNMbsWynnZbeW;y;Hr_FRyo(cw;*q?!%U~1;;O_w>DiVi zPi18wcEcCPeaQOqro5t7&7XbO_Dw}-tlAOU?;QNFZb$rB?Ta}kgqSqLvAkRSSMGLkhdlhF7BkX| zuHV0^p68*)TiH9jJm!@`>WT4&GXvsnos(OV_KfI#E8lfuM!>n4b%i5g~Fdw=4aO!ZEgrC)UgI!Byd|zr=>1f>;?0GV`^zJ@_+_6jB zWV}t>ljmD2ww8M=(Ehn*@y45bKRDzKY0oOG^(`VZ%bg>7jy658<79o8)9*TG{1g%^ zkw`Ria`g;3(|D%}^H2wP_|w?G_DkW%4`YUHjg_A}R$fqviA-#5e2z#Yxu>X}PoHRD z9#MQ3u1VGQ%pLNqoyl$67Bnw*^PO^=FMEh$;23ejGiAShy;gJOJ*Pc_?I)`{Q1Z;v#Xm6E*IG4DV}ZIy`XjLjz=ZdcDX8*y9Tn9 z&(S-R<4^4wu}zYBI8bHvWLC^@wAj}0!ukr~P_cB&`k86p*AxX#j;wCNY7xEtqlheBbrDuqw6Jxhf;3Q66t%>z5HHH0t~ot;^q$u_H}S z@BIF+*JS1-HQbfzahRnBr$roTn?>GmA~U1eByzD+PUMW>wH@KnOk>MWD^+@LC+t(9 zB&K68^Af#mZ0RxF@h;oUH?cbX@#@&Li=@LIU`qdS>f~Na5E>8}Z(Jqri>?nQu<8^HZVeDlWscjeQeen7xBd)s`{DD?KT7u*GWZpuklSEl)5}6Pk%Xjt*_}@ z^JwQYxvvij%T>CnQtxbtydHGI{wt9ZXFYLSU>n=5ZoZ~2zx424Qud}<9J?DU92K&v zpu4sU$20?Ny9~Y^t$pO@y)WeMg$$Bo`{u

|cl2#lF*>eI@@!^P=QyxarOkGb1DK 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 literal 0 HcmV?d00001 From 2f9fcab98fbd23eb288d8dc7a139a780292293fb Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 12:25:27 +0100 Subject: [PATCH 13/17] refactor(decoder): split H.265 NAL units with the shared Annex B iterator --- src/decoder/h265/parser.rs | 53 ++++---------------------------------- 1 file changed, 5 insertions(+), 48 deletions(-) diff --git a/src/decoder/h265/parser.rs b/src/decoder/h265/parser.rs index 08beda7f..7263f252 100644 --- a/src/decoder/h265/parser.rs +++ b/src/decoder/h265/parser.rs @@ -11,6 +11,7 @@ //! needs only enough syntax to fill the `StdVideoH265*` parameter sets, derive //! the picture order count and drive the reference picture set. +use crate::decoder::annexb; use crate::decoder::bitreader::BitReader; use crate::error::{PixelForgeError, Result}; @@ -261,54 +262,10 @@ pub(crate) fn iter_nal_units(data: &[u8]) -> impl Iterator> { pub(crate) fn iter_nal_units_with_offsets( data: &[u8], ) -> impl Iterator)> { - NalIterator { data, pos: 0 } -} - -struct NalIterator<'a> { - data: &'a [u8], - pos: usize, -} - -/// Find the next start code at or after `from`. Returns (start_code_pos, payload_pos). -fn find_start_code(data: &[u8], from: usize) -> Option<(usize, usize)> { - let mut i = from; - while i + 3 <= data.len() { - if data[i] == 0 && data[i + 1] == 0 { - if data[i + 2] == 1 { - return Some((i, i + 3)); - } - if i + 4 <= data.len() && data[i + 2] == 0 && data[i + 3] == 1 { - return Some((i, i + 4)); - } - } - i += 1; - } - None -} - -impl<'a> Iterator for NalIterator<'a> { - type Item = (usize, NalUnit<'a>); - - fn next(&mut self) -> Option<(usize, NalUnit<'a>)> { - loop { - let (start_code_pos, payload_start) = find_start_code(self.data, self.pos)?; - let end = match find_start_code(self.data, payload_start) { - Some((next_start, _)) => next_start, - None => self.data.len(), - }; - self.pos = end; - - // Trim trailing zero padding before the next start code. - let mut trimmed_end = end; - while trimmed_end > payload_start && self.data[trimmed_end - 1] == 0 { - trimmed_end -= 1; - } - let nal = &self.data[payload_start..trimmed_end]; - if let Ok(header) = parse_nal_header(nal) { - return Some((start_code_pos, NalUnit { header, data: nal })); - } - } - } + annexb::iter_nal_units(data).filter_map(|(offset, nal)| { + let header = parse_nal_header(nal).ok()?; + Some((offset, NalUnit { header, data: nal })) + }) } /// [`BitReader`] plus a count of the RBSP bits consumed. From 7a9eafacd59bcb4b8c667501a8eaeadfb227b290 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 12:32:12 +0100 Subject: [PATCH 14/17] feat(decoder): decode H.265 --- src/decoder/driver.rs | 35 ++- src/decoder/h264/session.rs | 15 +- src/decoder/h265/mod.rs | 33 +++ src/decoder/h265/pps.rs | 16 +- src/decoder/h265/record.rs | 29 ++ src/decoder/h265/session.rs | 495 +++++++++++++++++++++++++++++++++ src/decoder/h265/sps.rs | 4 +- src/decoder/h265/std_params.rs | 407 +++++++++++++++++++++++++++ src/decoder/mod.rs | 7 +- src/vulkan.rs | 2 +- tests/decode_streams.rs | 86 ++++++ tests/roundtrip.rs | 45 ++- 12 files changed, 1126 insertions(+), 48 deletions(-) create mode 100644 src/decoder/h265/record.rs create mode 100644 src/decoder/h265/session.rs create mode 100644 src/decoder/h265/std_params.rs create mode 100644 tests/decode_streams.rs diff --git a/src/decoder/driver.rs b/src/decoder/driver.rs index 7ddf953c..a1cfbf0e 100644 --- a/src/decoder/driver.rs +++ b/src/decoder/driver.rs @@ -46,16 +46,25 @@ pub(crate) trait CodecDecoder: Send { fn is_keyframe(unit: &Self::Unit<'_>) -> bool; /// Decode one coded picture. - /// - /// Returns the picture to output, or `None` for one that is decoded but - /// not shown. - fn decode_unit(&mut self, unit: &Self::Unit<'_>, pts: u64) -> Result>; + fn decode_unit(&mut self, unit: &Self::Unit<'_>, pts: u64) -> Result; /// How many pictures the active stream may hold back for display-order /// reordering. fn reorder_depth(&self) -> usize; } +/// What [`CodecDecoder::decode_unit`] did with a coded picture. +pub(crate) enum Decoded { + /// Decoded, and to be output. + Shown(DecodedPicture), + /// Decoded, but not output: an AV1 frame with `show_frame` 0, or an H.265 + /// picture with `pic_output_flag` 0. + Hidden, + /// Not decoded, and that is normal: an H.265 RASL picture after the CRA + /// that started decoding, which references pictures from before it. + Skipped, +} + /// Drives a [`CodecDecoder`]. pub(crate) struct StreamDecoder { codec: C, @@ -155,20 +164,20 @@ impl StreamDecoder { continue; } - let picture = self.codec.decode_unit(&unit, pts)?; - decoded_any = true; - // A decoded keyframe re-establishes a valid decode point. - if is_keyframe { - self.awaiting_keyframe = false; - } - let ready = match picture { - Some(picture) => { + let ready = match self.codec.decode_unit(&unit, pts)? { + Decoded::Shown(picture) => { let depth = self.codec.reorder_depth(); self.reorder .push(self.codec.common_mut(), &picture, depth)? } - None => Vec::new(), + Decoded::Hidden => Vec::new(), + Decoded::Skipped => continue, }; + decoded_any = true; + // A decoded keyframe re-establishes a valid decode point. + if is_keyframe { + self.awaiting_keyframe = false; + } // The picture's submissions are complete as far as recording goes; // hand its slot and frames to the completion thread and move on to // the next slot. diff --git a/src/decoder/h264/session.rs b/src/decoder/h264/session.rs index 71973dbe..ce7c71a2 100644 --- a/src/decoder/h264/session.rs +++ b/src/decoder/h264/session.rs @@ -10,7 +10,7 @@ use tracing::debug; use crate::decoder::DecodeConfig; use crate::decoder::annexb::START_CODE; use crate::decoder::common::DecoderCommon; -use crate::decoder::driver::CodecDecoder; +use crate::decoder::driver::{CodecDecoder, Decoded}; use crate::decoder::frames::DecodedPicture; use crate::decoder::h264::dpb::{DecodeDpb, PictureState}; use crate::decoder::h264::parser::{ @@ -381,12 +381,7 @@ impl H264Decoder { } /// Record and submit the decode of a single picture, without waiting for it. - fn decode_picture( - &mut self, - picture: &Picture, - sps: &Sps, - pts: u64, - ) -> Result> { + fn decode_picture(&mut self, picture: &Picture, sps: &Sps, pts: u64) -> Result { // --- Host-side bookkeeping: POC, slot, reference set --- let state = { let dpb = self.dpb.as_mut().expect("activated above"); @@ -440,7 +435,7 @@ impl H264Decoder { .expect("active") .end_picture(slot, &state, sps); - Ok(Some(frame)) + Ok(frame) } /// Record `vkCmdBeginVideoCoding` / `vkCmdDecodeVideo` / `vkCmdEndVideoCoding`. @@ -569,7 +564,7 @@ impl CodecDecoder for H264Decoder { picture.nal_type == NalType::IdrSlice } - fn decode_unit(&mut self, picture: &Picture<'_>, pts: u64) -> Result> { + fn decode_unit(&mut self, picture: &Picture<'_>, pts: u64) -> Result { // The parameter sets this picture was grouped under, not whatever // currently sits at those ids: a later SPS in the same chunk must not // reach back and change how this picture is decoded. @@ -577,7 +572,7 @@ impl CodecDecoder for H264Decoder { // (Re)build the session if the stream's geometry or ids changed. self.activate(&sps, &pps)?; - self.decode_picture(picture, &sps, pts) + self.decode_picture(picture, &sps, pts).map(Decoded::Shown) } fn reorder_depth(&self) -> usize { diff --git a/src/decoder/h265/mod.rs b/src/decoder/h265/mod.rs index 4f17e859..09daf8d8 100644 --- a/src/decoder/h265/mod.rs +++ b/src/decoder/h265/mod.rs @@ -19,6 +19,12 @@ pub(crate) mod scaling; pub(crate) mod slice; pub(crate) mod sps; +mod record; +mod session; +mod std_params; + +pub(crate) use session::H265Decoder; + /// Parser and DPB tests against real x265 and NVENC streams (host-side, no /// GPU), compiled only under `cargo test`. #[cfg(test)] @@ -27,3 +33,30 @@ mod parser_tests; /// Hand-written slice header syntax the fixtures do not cover. #[cfg(test)] mod syntax_tests; + +/// Byte offsets at which each coded picture begins in an Annex B stream. +/// +/// A new picture begins at each VCL NAL unit with +/// `first_slice_segment_in_pic_flag` set. Parameter sets, SEI, access unit +/// delimiters and end of sequence NAL units are attached to the picture that +/// follows them, so the offsets carve the stream up without loss. +pub(crate) fn picture_starts(stream: &[u8]) -> Vec { + let mut starts = Vec::new(); + let mut pending_start: Option = None; + for (offset, nal) in parser::iter_nal_units_with_offsets(stream) { + if nal.nal_type().is_vcl() { + if slice::starts_picture(&nal) { + starts.push(pending_start.take().unwrap_or(offset)); + } + pending_start = None; + } else if pending_start.is_none() { + pending_start = Some(offset); + } + } + starts +} + +/// How many leading bytes of `buffer` hold pictures known to be complete. +pub(crate) fn complete_prefix(buffer: &[u8]) -> usize { + picture_starts(buffer).last().copied().unwrap_or(0) +} diff --git a/src/decoder/h265/pps.rs b/src/decoder/h265/pps.rs index 96eae08d..1cc5c385 100644 --- a/src/decoder/h265/pps.rs +++ b/src/decoder/h265/pps.rs @@ -55,7 +55,7 @@ pub(crate) struct Pps { pub num_tile_rows_minus1: u8, pub uniform_spacing_flag: bool, /// `column_width_minus1[i]`, coded only without uniform spacing; see - /// [`Pps::tile_column_widths`] for the widths either way. + /// `Pps::tile_column_widths` for the widths either way. pub column_width_minus1: Vec, /// `row_height_minus1[i]`, coded only without uniform spacing. pub row_height_minus1: Vec, @@ -82,6 +82,8 @@ pub(crate) struct Pps { pub range_extension: PpsRangeExtension, } +// The driver takes the coded tile syntax; the derived sizes are for tests. +#[cfg(test)] impl Pps { /// Tile column widths in CTBs (equation 6-3), for a picture /// `pic_width_in_ctbs` wide. One column when tiles are off. @@ -94,20 +96,10 @@ impl Pps { pic_width_in_ctbs, ) } - - /// Tile row heights in CTBs (equation 6-4). - pub fn tile_row_heights(&self, pic_height_in_ctbs: u32) -> Vec { - tile_sizes( - self.tiles_enabled_flag, - self.uniform_spacing_flag, - self.num_tile_rows_minus1 as u32, - &self.row_height_minus1, - pic_height_in_ctbs, - ) - } } /// Equations 6-3 and 6-4, which have the same shape for columns and rows. +#[cfg(test)] fn tile_sizes( tiles: bool, uniform: bool, diff --git a/src/decoder/h265/record.rs b/src/decoder/h265/record.rs new file mode 100644 index 00000000..51ae32fb --- /dev/null +++ b/src/decoder/h265/record.rs @@ -0,0 +1,29 @@ +//! The H.265 structures of a decode command. + +use ash::vk; +use ash::vk::native::{StdVideoDecodeH265PictureInfo, StdVideoDecodeH265ReferenceInfo}; + +use crate::decoder::record::RecordCodec; + +/// H.265's part of a [`DecodeRecord`](crate::decoder::record::DecodeRecord). +pub(super) struct H265Record; + +impl RecordCodec for H265Record { + type RefStd = StdVideoDecodeH265ReferenceInfo; + type DpbSlot = vk::VideoDecodeH265DpbSlotInfoKHR<'static>; + type Picture = StdVideoDecodeH265PictureInfo; + type PictureInfo = vk::VideoDecodeH265PictureInfoKHR<'static>; + + fn dpb_slot(std: &'static StdVideoDecodeH265ReferenceInfo) -> Self::DpbSlot { + vk::VideoDecodeH265DpbSlotInfoKHR::default().std_reference_info(std) + } + + fn picture_info( + picture: &'static mut StdVideoDecodeH265PictureInfo, + offsets: &'static [u32], + ) -> Self::PictureInfo { + vk::VideoDecodeH265PictureInfoKHR::default() + .std_picture_info(picture) + .slice_segment_offsets(offsets) + } +} diff --git a/src/decoder/h265/session.rs b/src/decoder/h265/session.rs new file mode 100644 index 00000000..24cbf0e2 --- /dev/null +++ b/src/decoder/h265/session.rs @@ -0,0 +1,495 @@ +//! The H.265 Vulkan decode session: lazy stream-driven setup and per-picture +//! record/submit. + +use std::collections::HashMap; + +use ash::vk; +use ash::vk::TaggedStructure; +use tracing::debug; + +use super::dpb::{DecodeDpb, PictureDecision, PictureState, RefPicSet, SkipReason}; +use super::parser::{self, NalHeader, NalType, NalUnit, Vps}; +use super::pps::{self, Pps}; +use super::record::H265Record; +use super::slice::{self, SliceHeader}; +use super::sps::{self, Sps}; +use super::std_params::StdParameterSets; +use crate::decoder::DecodeConfig; +use crate::decoder::annexb::START_CODE; +use crate::decoder::common::DecoderCommon; +use crate::decoder::driver::{CodecDecoder, Decoded}; +use crate::decoder::pipeline::DECODE_PIPELINE_DEPTH; +use crate::decoder::plan::StreamNeeds; +use crate::decoder::profile::DecodeProfile; +use crate::decoder::record::{DecodeRecord, LinkParams}; +use crate::encoder::{BitDepth, ChromaFormat}; +use crate::error::{PixelForgeError, Result}; +use crate::vulkan::VideoContext; + +/// H.265 decoder. +/// +/// Owns only what is specific to H.265: the parameter sets seen so far, and the +/// POC / reference picture set state. Everything Vulkan lives in +/// [`DecoderCommon`], and the flow around a picture (framing, keyframes, +/// reordering) is [`StreamDecoder`](crate::decoder::driver::StreamDecoder)'s. +pub(crate) struct H265Decoder { + common: DecoderCommon, + + /// How many DPB slots to reserve beyond the stream's needs, so the caller + /// can hold decoded frames while decoding continues. + output_depth: usize, + + /// Parameter sets seen so far, by id. + vps_map: HashMap, + sps_map: HashMap, + pps_map: HashMap, + + /// The parameter sets the current session parameters were built from, kept + /// whole rather than by id: a stream may resend an id with new content, + /// which is how a resolution change is signalled. + active: Option<(Vps, Sps, Pps)>, + + /// POC and reference picture set state, recreated alongside the session. + dpb: Option, + /// An end of sequence NAL unit arrived after the last picture, so the next + /// one starts a new coded video sequence. + pending_eos: bool, + + /// What each pipeline slot's decode command was recorded from, indexed like + /// the pipeline's slots. + records: Box<[DecodeRecord]>, + + /// `sps_max_num_reorder_pics` of the active SPS's highest sub-layer. + reorder_depth: usize, +} + +/// The slice segments making up one picture, gathered from the input. +pub(crate) struct Picture<'a> { + nal: NalHeader, + /// The header of the picture's first slice segment. + header: SliceHeader, + segments: Vec<&'a [u8]>, + /// An end of sequence NAL unit came before this picture. + after_eos: bool, + /// The parameter sets in effect when this picture was grouped. Captured + /// here for the same reason as in the H.264 decoder: a later parameter set + /// in the same chunk must not reach back. + vps: Vps, + sps: Sps, + pps: Pps, +} + +impl H265Decoder { + pub(crate) fn create(context: VideoContext, config: &DecodeConfig) -> Result { + Ok(Self { + common: DecoderCommon::new(context, config.consumer_queue_family)?, + output_depth: config.output_depth, + vps_map: HashMap::new(), + sps_map: HashMap::new(), + pps_map: HashMap::new(), + active: None, + dpb: None, + pending_eos: false, + // SAFETY: the picture info is plain C data, for which all-zero is + // a valid (if meaningless) value. + records: (0..DECODE_PIPELINE_DEPTH) + .map(|_| DecodeRecord::new(unsafe { std::mem::zeroed() })) + .collect(), + reorder_depth: 0, + }) + } + + /// The decode profile for a parsed SPS. + fn profile_for(sps: &Sps) -> Result<(DecodeProfile, ChromaFormat, BitDepth)> { + let chroma_format = match (sps.chroma_format_idc, sps.separate_colour_plane_flag) { + (1, _) => ChromaFormat::Yuv420, + (2, _) => ChromaFormat::Yuv422, + (3, false) => ChromaFormat::Yuv444, + (idc, separate) => { + return Err(PixelForgeError::InvalidInput(format!( + "H.265 decode: unsupported chroma_format_idc {idc} \ + (separate colour planes: {separate})" + ))); + } + }; + let bit_depth = match (sps.bit_depth_luma_minus8, sps.bit_depth_chroma_minus8) { + (0, 0) => BitDepth::Eight, + (2, 2) => BitDepth::Ten, + (luma, chroma) => { + return Err(PixelForgeError::InvalidInput(format!( + "H.265 decode: unsupported bit depth {}/{} (luma/chroma)", + luma + 8, + chroma + 8 + ))); + } + }; + if sps.sps_scc_extension_flag || sps.sps_multilayer_extension_flag { + return Err(PixelForgeError::InvalidInput( + "H.265 decode: screen content and multi-layer extensions are not supported" + .to_string(), + )); + } + // The Std profile enum uses the coded values. + let profile_idc = sps.profile_tier_level.general_profile_idc as u32; + let profile = DecodeProfile::h265(profile_idc, chroma_format, bit_depth); + Ok((profile, chroma_format, bit_depth)) + } + + /// Create (or recreate) the Vulkan session for the given parameter sets. + fn activate(&mut self, vps: &Vps, sps: &Sps, pps: &Pps) -> Result<()> { + if self.common.session.is_some() + && self + .active + .as_ref() + .is_some_and(|(v, s, p)| v == vps && s == sps && p == pps) + { + return Ok(()); + } + + let (profile, chroma_format, bit_depth) = Self::profile_for(sps)?; + // `sps_max_dec_pic_buffering` counts the current picture too. + let references = sps.max_dec_pic_buffering().saturating_sub(1) as u32; + let reorder_depth = sps.reorder_depth(); + let needs = StreamNeeds { + codec: "H.265", + width: sps.pic_width_in_luma_samples, + height: sps.pic_height_in_luma_samples, + chroma_format, + bit_depth, + references, + active_references: references, + slot_limit: super::dpb::MAX_DPB_SLOTS as u32, + reorder_depth, + output_depth: self.output_depth, + }; + let mut h265_caps = vk::VideoDecodeH265CapabilitiesKHR::default(); + let plan = self + .common + .plan_session(profile.info(), &mut h265_caps, &needs)?; + let slot_count = plan.slot_count; + self.common + .create_session(&plan, profile, |common, session| { + Self::build_session_params(common, session, vps, sps, pps) + })?; + + self.active = Some((vps.clone(), sps.clone(), pps.clone())); + self.reorder_depth = reorder_depth; + let pins = self.common.session()?.slot_pins.clone(); + self.dpb = Some(DecodeDpb::new(slot_count, pins)); + Ok(()) + } + + /// Translate the parsed parameter sets into StdVideo structs and create + /// session parameters. + fn build_session_params( + common: &DecoderCommon, + session: vk::VideoSessionKHR, + vps: &Vps, + sps: &Sps, + pps: &Pps, + ) -> Result { + let sets = StdParameterSets::new(vps, sps, pps); + let vps_array = [sets.vps]; + let sps_array = [sets.sps]; + let pps_array = [sets.pps]; + let add_info = vk::VideoDecodeH265SessionParametersAddInfoKHR::default() + .std_vp_ss(&vps_array) + .std_sp_ss(&sps_array) + .std_pp_ss(&pps_array); + let mut h265_create = vk::VideoDecodeH265SessionParametersCreateInfoKHR::default() + .max_std_vps_count(1) + .max_std_sps_count(1) + .max_std_pps_count(1) + .parameters_add_info(&add_info); + let create_info = vk::VideoSessionParametersCreateInfoKHR::default() + .video_session(session) + .push(&mut h265_create); + + unsafe { + common + .video_queue_fn + .create_video_session_parameters(&create_info, None) + } + .map_err(PixelForgeError::vk("vkCreateVideoSessionParametersKHR")) + } + + /// Group NAL units into pictures, absorbing parameter sets. Returns the + /// pictures plus, if any slice was skipped for missing parameter sets, the + /// reason a keyframe is needed. + fn split_pictures<'a>(&mut self, data: &'a [u8]) -> Result<(Vec>, Option)> { + let mut pictures: Vec> = Vec::new(); + let mut awaiting: Option = None; + // Whether the slices of the picture in progress are being dropped, + // because its first segment could not be used. + let mut dropping = false; + + for nal in parser::iter_nal_units(data) { + // Only the base layer is decoded. + if nal.header.layer_id != 0 { + continue; + } + match nal.nal_type() { + NalType::Vps => { + let vps = parser::parse_vps(nal.payload())?; + self.vps_map.insert(vps.vps_video_parameter_set_id, vps); + } + NalType::Sps => { + let sps = sps::parse_sps(nal.payload())?; + debug!( + "H.265 SPS id={} {}x{} profile={}", + sps.sps_seq_parameter_set_id, + sps.pic_width_in_luma_samples, + sps.pic_height_in_luma_samples, + sps.profile_tier_level.general_profile_idc + ); + self.sps_map.insert(sps.sps_seq_parameter_set_id, sps); + } + NalType::Pps => { + let pps = pps::parse_pps(nal.payload())?; + self.pps_map.insert(pps.pps_pic_parameter_set_id, pps); + } + NalType::Eos => self.pending_eos = true, + t if t.is_vcl() && !matches!(t, NalType::Other(_)) => { + if slice::starts_picture(&nal) { + dropping = false; + match self.begin_picture(&nal)? { + Ok(picture) => pictures.push(picture), + Err(reason) => { + awaiting.get_or_insert(reason); + dropping = true; + } + } + } else if !dropping && let Some(picture) = pictures.last_mut() { + picture.segments.push(nal.data); + } + } + // SEI, AUD, and everything else is not needed by the driver. + _ => {} + } + } + Ok((pictures, awaiting)) + } + + /// Start a picture at its first slice segment. The inner `Err` is the + /// reason it cannot be decoded: parameter sets that have not been seen. + fn begin_picture<'a>( + &mut self, + nal: &NalUnit<'a>, + ) -> Result, String>> { + let pps_id = slice::slice_pps_id(nal)?; + let Some(pps) = self.pps_map.get(&pps_id) else { + return Ok(Err(format!("PPS {pps_id} not yet received"))); + }; + let Some(sps) = self.sps_map.get(&pps.pps_seq_parameter_set_id) else { + return Ok(Err(format!( + "SPS {} not yet received", + pps.pps_seq_parameter_set_id + ))); + }; + let Some(vps) = self.vps_map.get(&sps.sps_video_parameter_set_id) else { + return Ok(Err(format!( + "VPS {} not yet received", + sps.sps_video_parameter_set_id + ))); + }; + let header = slice::parse_slice_header(nal, sps, pps)?; + let after_eos = std::mem::take(&mut self.pending_eos); + Ok(Ok(Picture { + nal: nal.header, + header, + segments: vec![nal.data], + after_eos, + vps: vps.clone(), + sps: sps.clone(), + pps: pps.clone(), + })) + } + + /// Record and submit the decode of a single picture, without waiting for + /// it. + fn decode_picture(&mut self, picture: &Picture, pts: u64) -> Result { + let sps = &picture.sps; + let dpb = self.dpb.as_mut().expect("activated above"); + if picture.after_eos { + dpb.end_of_sequence(); + } + let state = match dpb.begin_picture(&picture.nal, &picture.header, sps)? { + PictureDecision::Decode(state) => state, + PictureDecision::Skip(SkipReason::RaslWithoutReferences) => { + return Ok(Decoded::Skipped); + } + PictureDecision::Skip(SkipReason::AwaitingIrap) => { + debug!("H.265 decode: picture before the first IRAP picture skipped"); + return Ok(Decoded::Skipped); + } + }; + if state.missing_reference { + // A picture that predicts from one the decoder does not have + // would decode to garbage, and so would everything after it that + // predicts from it in turn. + debug!( + "H.265 decode: picture POC {} references a missing picture; skipped", + state.poc + ); + return Ok(Decoded::Skipped); + } + + // Every slot can be busy because a frame the caller still holds pins + // one; wait for them to drop it, as the encoder waits for a free slot. + let slot = loop { + let dpb = self.dpb.as_mut().expect("active"); + if let Some(slot) = dpb.try_allocate_slot() { + break slot; + } + if !dpb.has_pinned_slots() { + return Err(PixelForgeError::InvalidInput( + "H.265 decode: no free DPB slot (stream exceeds negotiated DPB size)" + .to_string(), + )); + } + self.common.session()?.slot_pins.wait_for_release(); + }; + + // Claim the pipeline slot before staging into its buffer; see the + // H.264 decoder for why the order matters. + self.common.begin_decode_commands()?; + let (buffer_range, offsets) = self.common.stage_slices(&picture.segments, &START_CODE)?; + self.common.record_barriers(slot); + self.record_decode(picture, &state, slot, buffer_range, &offsets)?; + self.common.submit_decode()?; + + let (width, height) = sps.display_dimensions(); + let frame = + self.common + .decoded_picture(slot, width, height, pts, state.poc, state.is_keyframe); + self.dpb.as_mut().expect("active").end_picture(slot, &state); + + Ok(if state.pic_output_flag { + Decoded::Shown(frame) + } else { + Decoded::Hidden + }) + } + + /// Fill and link this pipeline slot's record, and record the coding + /// commands from it. + fn record_decode( + &mut self, + picture: &Picture, + state: &PictureState, + slot: u8, + buffer_range: u64, + offsets: &[u32], + ) -> Result<()> { + let active = self.common.session.as_ref().expect("active"); + + // The slot's previous decode has finished (`begin_decode_commands` + // waited for it), so its record can be rewritten. + let record = &mut self.records[self.common.pipeline.current_index()]; + record.reset(offsets); + + // Every picture the DPB holds, so the driver keeps them all; the lists + // in the picture info say which the current picture predicts from. + for r in self.dpb.as_ref().expect("active").references() { + // SAFETY: plain C data, for which all-zero is valid. + let mut flags: ash::vk::native::StdVideoDecodeH265ReferenceInfoFlags = + unsafe { std::mem::zeroed() }; + flags.set_used_for_long_term_reference(r.long_term as u32); + record.push_reference( + r.slot, + active.dpb_entry(r.slot).2, + ash::vk::native::StdVideoDecodeH265ReferenceInfo { + flags, + PicOrderCntVal: r.poc, + }, + ); + } + let setup_std = ash::vk::native::StdVideoDecodeH265ReferenceInfo { + // SAFETY: plain C data, for which all-zero is valid. + flags: unsafe { std::mem::zeroed() }, + PicOrderCntVal: state.poc, + }; + + *record.picture_mut() = picture_info(picture, state)?; + record.link(&LinkParams { + extent: vk::Extent2D { + width: active.coded_width, + height: active.coded_height, + }, + session: active.session, + session_params: active.session_params, + bitstream: self.common.bitstream_buffer(), + bitstream_range: buffer_range, + setup_slot: slot, + setup_view: active.dpb_entry(slot).2, + output_view: active.output_image.map(|(_, _, view)| view), + setup_std, + }); + self.common.record_coding(record); + Ok(()) + } +} + +/// The Std picture info for a picture about to be decoded. +fn picture_info( + picture: &Picture, + state: &PictureState, +) -> Result { + let header = &picture.header; + // SAFETY: plain C data, for which all-zero is valid. + let mut flags: ash::vk::native::StdVideoDecodeH265PictureInfoFlags = + unsafe { std::mem::zeroed() }; + flags.set_IrapPicFlag(state.nal_type.is_irap() as u32); + flags.set_IdrPicFlag(state.nal_type.is_idr() as u32); + flags.set_IsReference(state.is_reference as u32); + flags.set_short_term_ref_pic_set_sps_flag(header.short_term_ref_pic_set_sps_flag as u32); + Ok(ash::vk::native::StdVideoDecodeH265PictureInfo { + flags, + sps_video_parameter_set_id: picture.sps.sps_video_parameter_set_id, + pps_seq_parameter_set_id: picture.pps.pps_seq_parameter_set_id, + pps_pic_parameter_set_id: picture.pps.pps_pic_parameter_set_id, + NumDeltaPocsOfRefRpsIdx: header.num_delta_pocs_of_ref_rps_idx(&picture.sps), + PicOrderCntVal: state.poc, + NumBitsForSTRefPicSetInSlice: header.num_bits_for_st_ref_pic_set as u16, + reserved: 0, + RefPicSetStCurrBefore: RefPicSet::vulkan_list(&state.rps.st_curr_before)?, + RefPicSetStCurrAfter: RefPicSet::vulkan_list(&state.rps.st_curr_after)?, + RefPicSetLtCurr: RefPicSet::vulkan_list(&state.rps.lt_curr)?, + }) +} + +impl CodecDecoder for H265Decoder { + type Unit<'a> = Picture<'a>; + + const NAME: &'static str = "H.265"; + + fn common(&self) -> &DecoderCommon { + &self.common + } + + fn common_mut(&mut self) -> &mut DecoderCommon { + &mut self.common + } + + fn complete_prefix(buffer: &[u8]) -> usize { + super::complete_prefix(buffer) + } + + fn split_units<'a>(&mut self, data: &'a [u8]) -> Result<(Vec>, Option)> { + self.split_pictures(data) + } + + fn is_keyframe(picture: &Picture<'_>) -> bool { + picture.nal.nal_type.is_irap() + } + + fn decode_unit(&mut self, picture: &Picture<'_>, pts: u64) -> Result { + // (Re)build the session if the stream's parameter sets changed. + self.activate(&picture.vps, &picture.sps, &picture.pps)?; + self.decode_picture(picture, pts) + } + + fn reorder_depth(&self) -> usize { + self.reorder_depth + } +} diff --git a/src/decoder/h265/sps.rs b/src/decoder/h265/sps.rs index ded10eea..5a7a0940 100644 --- a/src/decoder/h265/sps.rs +++ b/src/decoder/h265/sps.rs @@ -289,7 +289,9 @@ impl Sps { } /// `SpsMaxLatencyPictures` of the highest sub-layer, or `None` when - /// `sps_max_latency_increase_plus1` is 0 (no limit). + /// `sps_max_latency_increase_plus1` is 0 (no limit). The output process + /// uses only the reorder depth, so this is for tests. + #[cfg(test)] pub fn max_latency_pictures(&self) -> Option { let tid = self.highest_tid(); let plus1 = self.dec_pic_buf_mgr.max_latency_increase_plus1[tid] as usize; diff --git a/src/decoder/h265/std_params.rs b/src/decoder/h265/std_params.rs new file mode 100644 index 00000000..b7298625 --- /dev/null +++ b/src/decoder/h265/std_params.rs @@ -0,0 +1,407 @@ +//! The `StdVideoH265*` parameter sets a decode session is created with, built +//! from the parsed VPS, SPS and PPS. + +use ash::vk::native::{ + StdVideoH265DecPicBufMgr, StdVideoH265LongTermRefPicsSps, StdVideoH265PictureParameterSet, + StdVideoH265ProfileTierLevel, StdVideoH265ScalingLists, StdVideoH265SequenceParameterSet, + StdVideoH265SequenceParameterSetVui, StdVideoH265ShortTermRefPicSet, + StdVideoH265VideoParameterSet, +}; + +use super::parser::{DecPicBufMgr, ProfileTierLevel, Vps}; +use super::pps::Pps; +use super::rps::StRefPicSetSyntax; +use super::scaling::ScalingLists; +use super::sps::{Sps, Vui}; + +/// The three parameter sets, and everything they point at. +/// +/// Each sub-structure is boxed, so the pointers to it stay valid when this +/// value moves. Keep it alive until the session parameters are created. +pub(super) struct StdParameterSets { + pub vps: StdVideoH265VideoParameterSet, + pub sps: StdVideoH265SequenceParameterSet, + pub pps: StdVideoH265PictureParameterSet, + _vps_ptl: Box, + _vps_dpbm: Box, + _sps_ptl: Box, + _sps_dpbm: Box, + _sps_scaling: Option>, + _st_rps: Vec, + _lt_rps: Box, + _vui: Option>, + _pps_scaling: Option>, +} + +// SAFETY: the raw pointers only point into the boxes this value owns. +unsafe impl Send for StdParameterSets {} + +fn ptr(value: &Option>) -> *const T { + value.as_deref().map_or(std::ptr::null(), |v| v as *const T) +} + +impl StdParameterSets { + pub(super) fn new(vps: &Vps, sps: &Sps, pps: &Pps) -> Self { + let vps_ptl = Box::new(profile_tier_level(&vps.profile_tier_level)); + let vps_dpbm = Box::new(dec_pic_buf_mgr(&vps.dec_pic_buf_mgr)); + let sps_ptl = Box::new(profile_tier_level(&sps.profile_tier_level)); + let sps_dpbm = Box::new(dec_pic_buf_mgr(&sps.dec_pic_buf_mgr)); + // With scaling lists enabled but none coded, the default lists apply, + // and the driver is handed them explicitly. + let sps_scaling = sps.scaling_list_enabled_flag.then(|| { + Box::new(scaling_lists( + sps.scaling_lists + .as_ref() + .unwrap_or(&ScalingLists::default()), + )) + }); + let st_rps: Vec<_> = sps + .short_term_ref_pic_sets + .iter() + .map(|set| short_term_ref_pic_set(&set.syntax)) + .collect(); + let lt_rps = Box::new(long_term_ref_pics(sps)); + let vui = sps.vui.as_ref().map(|v| Box::new(vui(v))); + let pps_scaling = pps + .scaling_lists + .as_ref() + .filter(|_| pps.pps_scaling_list_data_present_flag) + .map(|l| Box::new(scaling_lists(l))); + + let std_vps = video_parameter_set(vps, &vps_ptl, &vps_dpbm); + let mut std_sps = sequence_parameter_set(sps); + std_sps.pProfileTierLevel = &*sps_ptl; + std_sps.pDecPicBufMgr = &*sps_dpbm; + std_sps.pScalingLists = ptr(&sps_scaling); + std_sps.pShortTermRefPicSet = if st_rps.is_empty() { + std::ptr::null() + } else { + st_rps.as_ptr() + }; + std_sps.pLongTermRefPicsSps = &*lt_rps; + std_sps.pSequenceParameterSetVui = ptr(&vui); + let mut std_pps = picture_parameter_set(pps, sps.sps_video_parameter_set_id); + std_pps.pScalingLists = ptr(&pps_scaling); + + Self { + vps: std_vps, + sps: std_sps, + pps: std_pps, + _vps_ptl: vps_ptl, + _vps_dpbm: vps_dpbm, + _sps_ptl: sps_ptl, + _sps_dpbm: sps_dpbm, + _sps_scaling: sps_scaling, + _st_rps: st_rps, + _lt_rps: lt_rps, + _vui: vui, + _pps_scaling: pps_scaling, + } + } +} + +fn profile_tier_level(ptl: &ProfileTierLevel) -> StdVideoH265ProfileTierLevel { + // SAFETY: plain C data, for which all-zero is valid. + let mut out: StdVideoH265ProfileTierLevel = unsafe { std::mem::zeroed() }; + out.flags + .set_general_tier_flag(ptl.general_tier_flag as u32); + out.flags + .set_general_progressive_source_flag(ptl.general_progressive_source_flag as u32); + out.flags + .set_general_interlaced_source_flag(ptl.general_interlaced_source_flag as u32); + out.flags + .set_general_non_packed_constraint_flag(ptl.general_non_packed_constraint_flag as u32); + out.flags + .set_general_frame_only_constraint_flag(ptl.general_frame_only_constraint_flag as u32); + // The Std profile enum uses the coded values. + out.general_profile_idc = ptl.general_profile_idc as u32; + out.general_level_idc = level_idc(ptl.general_level_idc); + out +} + +/// `StdVideoH265LevelIdc` for a coded `general_level_idc`, which is thirty +/// times the level. +fn level_idc(general_level_idc: u8) -> ash::vk::native::StdVideoH265LevelIdc { + use ash::vk::native::*; + match general_level_idc { + 0..=30 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_1_0, + 31..=60 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_2_0, + 61..=63 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_2_1, + 64..=90 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_3_0, + 91..=93 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_3_1, + 94..=120 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_0, + 121..=123 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_4_1, + 124..=150 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_5_0, + 151..=153 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_5_1, + 154..=156 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_5_2, + 157..=180 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_0, + 181..=183 => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_1, + _ => StdVideoH265LevelIdc_STD_VIDEO_H265_LEVEL_IDC_6_2, + } +} + +fn dec_pic_buf_mgr(m: &DecPicBufMgr) -> StdVideoH265DecPicBufMgr { + StdVideoH265DecPicBufMgr { + max_latency_increase_plus1: m.max_latency_increase_plus1, + max_dec_pic_buffering_minus1: m.max_dec_pic_buffering_minus1, + max_num_reorder_pics: m.max_num_reorder_pics, + } +} + +fn scaling_lists(l: &ScalingLists) -> StdVideoH265ScalingLists { + StdVideoH265ScalingLists { + ScalingList4x4: l.scaling_list_4x4, + ScalingList8x8: l.scaling_list_8x8, + ScalingList16x16: l.scaling_list_16x16, + ScalingList32x32: l.scaling_list_32x32, + ScalingListDCCoef16x16: l.scaling_list_dc_coef_16x16, + ScalingListDCCoef32x32: l.scaling_list_dc_coef_32x32, + } +} + +fn short_term_ref_pic_set(s: &StRefPicSetSyntax) -> StdVideoH265ShortTermRefPicSet { + // SAFETY: plain C data, for which all-zero is valid. + let mut out: StdVideoH265ShortTermRefPicSet = unsafe { std::mem::zeroed() }; + out.flags + .set_inter_ref_pic_set_prediction_flag(s.inter_ref_pic_set_prediction_flag as u32); + out.flags.set_delta_rps_sign(s.delta_rps_sign as u32); + out.delta_idx_minus1 = s.delta_idx_minus1; + // Vulkan holds these flag arrays as 16 bits. Inter-RPS prediction can code + // a 17th entry, which only a reference set of 16 pictures reaches. + out.use_delta_flag = s.use_delta_flag as u16; + out.abs_delta_rps_minus1 = s.abs_delta_rps_minus1 as u16; + out.used_by_curr_pic_flag = s.used_by_curr_pic_flag as u16; + out.used_by_curr_pic_s0_flag = s.used_by_curr_pic_s0_flag; + out.used_by_curr_pic_s1_flag = s.used_by_curr_pic_s1_flag; + out.num_negative_pics = s.num_negative_pics; + out.num_positive_pics = s.num_positive_pics; + out.delta_poc_s0_minus1 = s.delta_poc_s0_minus1; + out.delta_poc_s1_minus1 = s.delta_poc_s1_minus1; + out +} + +fn long_term_ref_pics(sps: &Sps) -> StdVideoH265LongTermRefPicsSps { + let mut lsb = [0u32; 32]; + for (dst, src) in lsb + .iter_mut() + .zip(&sps.long_term_ref_pics.lt_ref_pic_poc_lsb_sps) + { + *dst = *src; + } + StdVideoH265LongTermRefPicsSps { + used_by_curr_pic_lt_sps_flag: sps.long_term_ref_pics.used_by_curr_pic_lt_sps_flag, + lt_ref_pic_poc_lsb_sps: lsb, + } +} + +fn vui(v: &Vui) -> StdVideoH265SequenceParameterSetVui { + // SAFETY: plain C data, for which all-zero is valid. + let mut out: StdVideoH265SequenceParameterSetVui = unsafe { std::mem::zeroed() }; + let f = &mut out.flags; + f.set_aspect_ratio_info_present_flag(v.aspect_ratio_info_present_flag as u32); + f.set_overscan_info_present_flag(v.overscan_info_present_flag as u32); + f.set_overscan_appropriate_flag(v.overscan_appropriate_flag as u32); + f.set_video_signal_type_present_flag(v.video_signal_type_present_flag as u32); + f.set_video_full_range_flag(v.video_full_range_flag as u32); + f.set_colour_description_present_flag(v.colour_description_present_flag as u32); + f.set_chroma_loc_info_present_flag(v.chroma_loc_info_present_flag as u32); + f.set_neutral_chroma_indication_flag(v.neutral_chroma_indication_flag as u32); + f.set_field_seq_flag(v.field_seq_flag as u32); + f.set_frame_field_info_present_flag(v.frame_field_info_present_flag as u32); + f.set_default_display_window_flag(v.default_display_window_flag as u32); + f.set_vui_timing_info_present_flag(v.vui_timing_info_present_flag as u32); + f.set_vui_poc_proportional_to_timing_flag(v.vui_poc_proportional_to_timing_flag as u32); + // The HRD parameters are skipped when parsing, so none are handed over; + // decoding does not use them. + f.set_vui_hrd_parameters_present_flag(0); + f.set_bitstream_restriction_flag(v.bitstream_restriction_flag as u32); + f.set_tiles_fixed_structure_flag(v.tiles_fixed_structure_flag as u32); + f.set_motion_vectors_over_pic_boundaries_flag(v.motion_vectors_over_pic_boundaries_flag as u32); + f.set_restricted_ref_pic_lists_flag(v.restricted_ref_pic_lists_flag as u32); + out.aspect_ratio_idc = v.aspect_ratio_idc as u32; + out.sar_width = v.sar_width; + out.sar_height = v.sar_height; + out.video_format = v.video_format; + out.colour_primaries = v.colour_primaries; + out.transfer_characteristics = v.transfer_characteristics; + out.matrix_coeffs = v.matrix_coeffs; + out.chroma_sample_loc_type_top_field = v.chroma_sample_loc_type_top_field; + out.chroma_sample_loc_type_bottom_field = v.chroma_sample_loc_type_bottom_field; + out.def_disp_win_left_offset = v.def_disp_win_left_offset; + out.def_disp_win_right_offset = v.def_disp_win_right_offset; + out.def_disp_win_top_offset = v.def_disp_win_top_offset; + out.def_disp_win_bottom_offset = v.def_disp_win_bottom_offset; + out.vui_num_units_in_tick = v.vui_num_units_in_tick; + out.vui_time_scale = v.vui_time_scale; + out.vui_num_ticks_poc_diff_one_minus1 = v.vui_num_ticks_poc_diff_one_minus1; + out.min_spatial_segmentation_idc = v.min_spatial_segmentation_idc; + out.max_bytes_per_pic_denom = v.max_bytes_per_pic_denom; + out.max_bits_per_min_cu_denom = v.max_bits_per_min_cu_denom; + out.log2_max_mv_length_horizontal = v.log2_max_mv_length_horizontal; + out.log2_max_mv_length_vertical = v.log2_max_mv_length_vertical; + out +} + +fn video_parameter_set( + vps: &Vps, + ptl: &StdVideoH265ProfileTierLevel, + dpbm: &StdVideoH265DecPicBufMgr, +) -> StdVideoH265VideoParameterSet { + // SAFETY: plain C data, for which all-zero is valid. + let mut out: StdVideoH265VideoParameterSet = unsafe { std::mem::zeroed() }; + out.flags + .set_vps_temporal_id_nesting_flag(vps.vps_temporal_id_nesting_flag as u32); + out.flags.set_vps_sub_layer_ordering_info_present_flag( + vps.vps_sub_layer_ordering_info_present_flag as u32, + ); + if let Some(t) = &vps.timing { + out.flags.set_vps_timing_info_present_flag(1); + out.flags + .set_vps_poc_proportional_to_timing_flag(t.poc_proportional_to_timing_flag as u32); + out.vps_num_units_in_tick = t.num_units_in_tick; + out.vps_time_scale = t.time_scale; + out.vps_num_ticks_poc_diff_one_minus1 = t.num_ticks_poc_diff_one_minus1; + } + out.vps_video_parameter_set_id = vps.vps_video_parameter_set_id; + out.vps_max_sub_layers_minus1 = vps.vps_max_sub_layers_minus1; + out.pDecPicBufMgr = dpbm; + out.pProfileTierLevel = ptl; + out +} + +fn sequence_parameter_set(sps: &Sps) -> StdVideoH265SequenceParameterSet { + // SAFETY: plain C data, for which all-zero is valid. + let mut out: StdVideoH265SequenceParameterSet = unsafe { std::mem::zeroed() }; + let f = &mut out.flags; + let r = &sps.range_extension; + f.set_sps_temporal_id_nesting_flag(sps.sps_temporal_id_nesting_flag as u32); + f.set_separate_colour_plane_flag(sps.separate_colour_plane_flag as u32); + f.set_conformance_window_flag(sps.conformance_window_flag as u32); + f.set_sps_sub_layer_ordering_info_present_flag( + sps.sps_sub_layer_ordering_info_present_flag as u32, + ); + f.set_scaling_list_enabled_flag(sps.scaling_list_enabled_flag as u32); + f.set_sps_scaling_list_data_present_flag(sps.sps_scaling_list_data_present_flag as u32); + f.set_amp_enabled_flag(sps.amp_enabled_flag as u32); + f.set_sample_adaptive_offset_enabled_flag(sps.sample_adaptive_offset_enabled_flag as u32); + f.set_pcm_enabled_flag(sps.pcm_enabled_flag as u32); + f.set_pcm_loop_filter_disabled_flag(sps.pcm_loop_filter_disabled_flag as u32); + f.set_long_term_ref_pics_present_flag(sps.long_term_ref_pics_present_flag as u32); + f.set_sps_temporal_mvp_enabled_flag(sps.sps_temporal_mvp_enabled_flag as u32); + f.set_strong_intra_smoothing_enabled_flag(sps.strong_intra_smoothing_enabled_flag as u32); + f.set_vui_parameters_present_flag(sps.vui.is_some() as u32); + f.set_sps_extension_present_flag(sps.sps_extension_present_flag as u32); + f.set_sps_range_extension_flag(sps.sps_range_extension_flag as u32); + f.set_transform_skip_rotation_enabled_flag(r.transform_skip_rotation_enabled_flag as u32); + f.set_transform_skip_context_enabled_flag(r.transform_skip_context_enabled_flag as u32); + f.set_implicit_rdpcm_enabled_flag(r.implicit_rdpcm_enabled_flag as u32); + f.set_explicit_rdpcm_enabled_flag(r.explicit_rdpcm_enabled_flag as u32); + f.set_extended_precision_processing_flag(r.extended_precision_processing_flag as u32); + f.set_intra_smoothing_disabled_flag(r.intra_smoothing_disabled_flag as u32); + f.set_high_precision_offsets_enabled_flag(r.high_precision_offsets_enabled_flag as u32); + f.set_persistent_rice_adaptation_enabled_flag(r.persistent_rice_adaptation_enabled_flag as u32); + f.set_cabac_bypass_alignment_enabled_flag(r.cabac_bypass_alignment_enabled_flag as u32); + f.set_sps_scc_extension_flag(sps.sps_scc_extension_flag as u32); + out.chroma_format_idc = sps.chroma_format_idc as u32; + out.pic_width_in_luma_samples = sps.pic_width_in_luma_samples; + out.pic_height_in_luma_samples = sps.pic_height_in_luma_samples; + out.sps_video_parameter_set_id = sps.sps_video_parameter_set_id; + out.sps_max_sub_layers_minus1 = sps.sps_max_sub_layers_minus1; + out.sps_seq_parameter_set_id = sps.sps_seq_parameter_set_id; + out.bit_depth_luma_minus8 = sps.bit_depth_luma_minus8; + out.bit_depth_chroma_minus8 = sps.bit_depth_chroma_minus8; + out.log2_max_pic_order_cnt_lsb_minus4 = sps.log2_max_pic_order_cnt_lsb_minus4; + out.log2_min_luma_coding_block_size_minus3 = sps.log2_min_luma_coding_block_size_minus3; + out.log2_diff_max_min_luma_coding_block_size = sps.log2_diff_max_min_luma_coding_block_size; + out.log2_min_luma_transform_block_size_minus2 = sps.log2_min_luma_transform_block_size_minus2; + out.log2_diff_max_min_luma_transform_block_size = + sps.log2_diff_max_min_luma_transform_block_size; + out.max_transform_hierarchy_depth_inter = sps.max_transform_hierarchy_depth_inter; + out.max_transform_hierarchy_depth_intra = sps.max_transform_hierarchy_depth_intra; + out.num_short_term_ref_pic_sets = sps.short_term_ref_pic_sets.len() as u8; + out.num_long_term_ref_pics_sps = sps.long_term_ref_pics.lt_ref_pic_poc_lsb_sps.len() as u8; + out.pcm_sample_bit_depth_luma_minus1 = sps.pcm_sample_bit_depth_luma_minus1; + out.pcm_sample_bit_depth_chroma_minus1 = sps.pcm_sample_bit_depth_chroma_minus1; + out.log2_min_pcm_luma_coding_block_size_minus3 = sps.log2_min_pcm_luma_coding_block_size_minus3; + out.log2_diff_max_min_pcm_luma_coding_block_size = + sps.log2_diff_max_min_pcm_luma_coding_block_size; + out.conf_win_left_offset = sps.conf_win_left_offset; + out.conf_win_right_offset = sps.conf_win_right_offset; + out.conf_win_top_offset = sps.conf_win_top_offset; + out.conf_win_bottom_offset = sps.conf_win_bottom_offset; + out +} + +fn picture_parameter_set(pps: &Pps, vps_id: u8) -> StdVideoH265PictureParameterSet { + // SAFETY: plain C data, for which all-zero is valid. + let mut out: StdVideoH265PictureParameterSet = unsafe { std::mem::zeroed() }; + let f = &mut out.flags; + let r = &pps.range_extension; + f.set_dependent_slice_segments_enabled_flag(pps.dependent_slice_segments_enabled_flag as u32); + f.set_output_flag_present_flag(pps.output_flag_present_flag as u32); + f.set_sign_data_hiding_enabled_flag(pps.sign_data_hiding_enabled_flag as u32); + f.set_cabac_init_present_flag(pps.cabac_init_present_flag as u32); + f.set_constrained_intra_pred_flag(pps.constrained_intra_pred_flag as u32); + f.set_transform_skip_enabled_flag(pps.transform_skip_enabled_flag as u32); + f.set_cu_qp_delta_enabled_flag(pps.cu_qp_delta_enabled_flag as u32); + f.set_pps_slice_chroma_qp_offsets_present_flag( + pps.pps_slice_chroma_qp_offsets_present_flag as u32, + ); + f.set_weighted_pred_flag(pps.weighted_pred_flag as u32); + f.set_weighted_bipred_flag(pps.weighted_bipred_flag as u32); + f.set_transquant_bypass_enabled_flag(pps.transquant_bypass_enabled_flag as u32); + f.set_tiles_enabled_flag(pps.tiles_enabled_flag as u32); + f.set_entropy_coding_sync_enabled_flag(pps.entropy_coding_sync_enabled_flag as u32); + f.set_uniform_spacing_flag(pps.uniform_spacing_flag as u32); + f.set_loop_filter_across_tiles_enabled_flag(pps.loop_filter_across_tiles_enabled_flag as u32); + f.set_pps_loop_filter_across_slices_enabled_flag( + pps.pps_loop_filter_across_slices_enabled_flag as u32, + ); + f.set_deblocking_filter_control_present_flag(pps.deblocking_filter_control_present_flag as u32); + f.set_deblocking_filter_override_enabled_flag( + pps.deblocking_filter_override_enabled_flag as u32, + ); + f.set_pps_deblocking_filter_disabled_flag(pps.pps_deblocking_filter_disabled_flag as u32); + f.set_pps_scaling_list_data_present_flag(pps.pps_scaling_list_data_present_flag as u32); + f.set_lists_modification_present_flag(pps.lists_modification_present_flag as u32); + f.set_slice_segment_header_extension_present_flag( + pps.slice_segment_header_extension_present_flag as u32, + ); + f.set_pps_extension_present_flag(pps.pps_extension_present_flag as u32); + f.set_cross_component_prediction_enabled_flag(r.cross_component_prediction_enabled_flag as u32); + f.set_chroma_qp_offset_list_enabled_flag(r.chroma_qp_offset_list_enabled_flag as u32); + f.set_pps_range_extension_flag(pps.pps_range_extension_flag as u32); + out.pps_pic_parameter_set_id = pps.pps_pic_parameter_set_id; + out.pps_seq_parameter_set_id = pps.pps_seq_parameter_set_id; + out.sps_video_parameter_set_id = vps_id; + out.num_extra_slice_header_bits = pps.num_extra_slice_header_bits; + out.num_ref_idx_l0_default_active_minus1 = pps.num_ref_idx_l0_default_active_minus1; + out.num_ref_idx_l1_default_active_minus1 = pps.num_ref_idx_l1_default_active_minus1; + out.init_qp_minus26 = pps.init_qp_minus26; + out.diff_cu_qp_delta_depth = pps.diff_cu_qp_delta_depth; + out.pps_cb_qp_offset = pps.pps_cb_qp_offset; + out.pps_cr_qp_offset = pps.pps_cr_qp_offset; + out.pps_beta_offset_div2 = pps.pps_beta_offset_div2; + out.pps_tc_offset_div2 = pps.pps_tc_offset_div2; + out.log2_parallel_merge_level_minus2 = pps.log2_parallel_merge_level_minus2; + out.log2_max_transform_skip_block_size_minus2 = r.log2_max_transform_skip_block_size_minus2; + out.diff_cu_chroma_qp_offset_depth = r.diff_cu_chroma_qp_offset_depth; + out.chroma_qp_offset_list_len_minus1 = r.chroma_qp_offset_list_len_minus1; + out.cb_qp_offset_list = r.cb_qp_offset_list; + out.cr_qp_offset_list = r.cr_qp_offset_list; + out.log2_sao_offset_scale_luma = r.log2_sao_offset_scale_luma; + out.log2_sao_offset_scale_chroma = r.log2_sao_offset_scale_chroma; + out.num_tile_columns_minus1 = pps.num_tile_columns_minus1; + out.num_tile_rows_minus1 = pps.num_tile_rows_minus1; + for (dst, src) in out + .column_width_minus1 + .iter_mut() + .zip(&pps.column_width_minus1) + { + *dst = *src; + } + for (dst, src) in out.row_height_minus1.iter_mut().zip(&pps.row_height_minus1) { + *dst = *src; + } + out +} diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index e65e5040..2a7c3a5d 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -52,8 +52,7 @@ pub(crate) mod driver; pub(crate) mod frames; /// H.264 decoding. pub(crate) mod h264; -/// H.265 parsing and reference bookkeeping. Nothing uses it yet. -#[allow(dead_code)] +/// H.265 decoding. pub(crate) mod h265; pub(crate) mod pipeline; /// Turning what a stream needs into a session plan. @@ -417,6 +416,10 @@ impl Decoder { h264::H264Decoder::create(context, &config)?, &config, )), + Codec::H265 => Box::new(driver::StreamDecoder::new( + h265::H265Decoder::create(context, &config)?, + &config, + )), other => { return Err(PixelForgeError::InvalidInput(format!( "{:?} decoding is not implemented yet", diff --git a/src/vulkan.rs b/src/vulkan.rs index 58c53757..252ed5e7 100644 --- a/src/vulkan.rs +++ b/src/vulkan.rs @@ -581,7 +581,7 @@ fn decode_extension_name(codec: Codec) -> &'static std::ffi::CStr { } /// The codecs pixelforge can decode, on a device that supports them. -const DECODE_CODECS: &[Codec] = &[Codec::H264]; +const DECODE_CODECS: &[Codec] = &[Codec::H264, Codec::H265]; /// The decode codecs `physical_device` supports. fn query_decode_codecs( diff --git a/tests/decode_streams.rs b/tests/decode_streams.rs new file mode 100644 index 00000000..661537a8 --- /dev/null +++ b/tests/decode_streams.rs @@ -0,0 +1,86 @@ +//! Decode third-party streams and compare against ffmpeg's software decoder, +//! which must match byte for byte. +//! +//! The streams are the fixtures in `tests/data`; how each was made is in the +//! parser tests that use them. Each is fed as a byte stream that the decoder +//! frames itself, and takes whichever output path the device offers. +//! +//! `base.264` is left out: on NVIDIA (615.78.08) it differs from ffmpeg from +//! frame 17, the first after its `frame_num` wraps. +//! +//! Ignored by default: requires a Vulkan Video device and ffmpeg. Run with +//! `cargo test -- --ignored`. + +use std::fs::File; +use std::path::Path; + +#[allow(dead_code)] +mod common; +use common::{Readback, decode_stream, write_nv12}; + +use pixelforge::decoder::{DecodeConfig, Decoder}; +use pixelforge::{Codec, VideoContextBuilder}; + +/// Decode `name` from `tests/data` and compare it against ffmpeg. +fn check(codec: Codec, name: &str, pix_fmt: &str) -> Result<(), Box> { + common::init_logging(); + let input = Path::new("tests/data").join(name); + let dir = common::scratch_dir("decode_streams"); + let output_path = dir.join(format!("{name}.yuv")); + let reference_path = dir.join(format!("{name}.ref.yuv")); + + let status = std::process::Command::new("ffmpeg") + .args(["-hide_banner", "-loglevel", "error", "-y", "-i"]) + .arg(&input) + .args(["-pix_fmt", pix_fmt, "-f", "rawvideo"]) + .arg(&reference_path) + .status()?; + assert!(status.success(), "ffmpeg failed to decode {name}"); + + let context = VideoContextBuilder::new() + .app_name("pixelforge-decode-streams") + .require_decode(codec) + .enable_validation(std::env::var("PIXELFORGE_VALIDATION").is_ok()) + .build()?; + let mut readback = Some(Readback::new(&context)?); + let mut output = Some(File::create(&output_path)?); + let mut decoder = Decoder::new(context, DecodeConfig::new(codec).with_byte_stream())?; + let stream = std::fs::read(&input)?; + decode_stream(&mut decoder, &stream, |frame| { + write_nv12(&frame, &mut readback, &mut output) + })?; + drop(output); + + assert!( + std::fs::read(&reference_path)? == std::fs::read(&output_path)?, + "{name}: decoded frames differ from ffmpeg (see {})", + output_path.display() + ); + Ok(()) +} + +macro_rules! stream_tests { + ($($test:ident: $codec:expr, $name:literal, $pix_fmt:literal;)*) => { + $( + #[test] + #[ignore = "requires a Vulkan Video device and ffmpeg"] + fn $test() -> Result<(), Box> { + check($codec, $name, $pix_fmt) + } + )* + }; +} + +stream_tests! { + h264_bframes: Codec::H264, "bframes.264", "nv12"; + h264_multislice: Codec::H264, "multislice.264", "nv12"; + h264_zerolatency: Codec::H264, "zerolatency.264", "nv12"; + h265_closedgop: Codec::H265, "closedgop.265", "nv12"; + h265_cra_start: Codec::H265, "cra_start.265", "nv12"; + h265_main10: Codec::H265, "main10.265", "p010le"; + h265_nvenc: Codec::H265, "nvenc.265", "nv12"; + h265_opengop: Codec::H265, "opengop.265", "nv12"; + h265_scaling: Codec::H265, "scaling.265", "nv12"; + h265_slices: Codec::H265, "slices.265", "nv12"; + h265_temporal: Codec::H265, "temporal.265", "nv12"; +} diff --git a/tests/roundtrip.rs b/tests/roundtrip.rs index 71ec5f9c..a5ad4363 100644 --- a/tests/roundtrip.rs +++ b/tests/roundtrip.rs @@ -6,8 +6,9 @@ //! It is also where B-frames will be exercised once the encoder supports them. //! //! The input is planar YUV420 (I420). The decoded output is NV12 in display -//! order. The assertion here is that no frame is lost across the round trip; -//! the decoded file is left in the scratch dir for inspection. +//! order. The assertions are that no frame is lost across the round trip, and +//! that the output is byte-identical to ffmpeg's software decode of the same +//! stream. The files are left in the scratch dir for inspection. //! //! Ignored by default: requires a Vulkan Video device and ffmpeg. Run with //! `cargo test -- --ignored`. @@ -32,18 +33,28 @@ const GOP: u32 = 30; #[test] #[ignore = "requires a Vulkan Video device and ffmpeg"] fn roundtrip_h264() -> Result<(), Box> { - tracing_subscriber::fmt::init(); + roundtrip(Codec::H264, "h264") +} + +#[test] +#[ignore = "requires a Vulkan Video device and ffmpeg"] +fn roundtrip_h265() -> Result<(), Box> { + roundtrip(Codec::H265, "h265") +} + +fn roundtrip(codec: Codec, extension: &str) -> Result<(), Box> { + let _ = tracing_subscriber::fmt::try_init(); let input_path = format!("testdata/test_frames_{WIDTH}x{HEIGHT}_yuv420p.yuv"); common::ensure_test_data(WIDTH, HEIGHT, "yuv420p", &input_path)?; - let dir = common::scratch_dir("roundtrip"); - let bitstream_path = dir.join("out.h264"); + let dir = common::scratch_dir(&format!("roundtrip-{extension}")); + let bitstream_path = dir.join(format!("out.{extension}")); let context = VideoContextBuilder::new() .app_name("pixelforge-roundtrip") - .require_encode(Codec::H264) - .require_decode(Codec::H264) + .require_encode(codec) + .require_decode(codec) .enable_validation(std::env::var("PIXELFORGE_VALIDATION").is_ok()) .build()?; @@ -53,7 +64,7 @@ fn roundtrip_h264() -> Result<(), Box> { let frame_size = (WIDTH * HEIGHT * 3 / 2) as usize; let frame_count = yuv.len() / frame_size; - let config = EncodeConfig::h264(WIDTH, HEIGHT) + let config = EncodeConfig::new(codec, WIDTH, HEIGHT) .with_rate_control(RateControl::Cqp { qp: 26 }) .with_frame_rate(30, 1) .with_gop_size(GOP); @@ -89,7 +100,7 @@ fn roundtrip_h264() -> Result<(), Box> { let yuv_path = dir.join("out.yuv"); let mut out = Some(File::create(&yuv_path)?); let mut readback = Some(Readback::new(&context)?); - let mut decoder = Decoder::new(context, DecodeConfig::new(Codec::H264).with_byte_stream())?; + let mut decoder = Decoder::new(context, DecodeConfig::new(codec).with_byte_stream())?; let mut decoded_count = 0usize; decode_stream(&mut decoder, &bitstream, |frame| { @@ -104,6 +115,22 @@ fn roundtrip_h264() -> Result<(), Box> { "round trip lost frames: encoded {frame_count} but decoded {decoded_count}" ); println!("Wrote decoded frames to {}", yuv_path.display()); + drop(out); + + // --- Compare against ffmpeg's software decoder --- + let reference_path = dir.join("reference.yuv"); + let status = std::process::Command::new("ffmpeg") + .args(["-hide_banner", "-loglevel", "error", "-y", "-i"]) + .arg(&bitstream_path) + .args(["-pix_fmt", "nv12", "-f", "rawvideo"]) + .arg(&reference_path) + .status()?; + assert!(status.success(), "ffmpeg failed to decode the stream"); + assert!( + std::fs::read(&reference_path)? == std::fs::read(&yuv_path)?, + "decoded frames differ from ffmpeg's decode of {}", + bitstream_path.display() + ); Ok(()) } From 68139f8b783df00335ad128a6699c1729826e445 Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 12:59:22 +0100 Subject: [PATCH 15/17] feat(decoder): decode AV1 --- examples/common/mod.rs | 9 +- src/decoder/av1/consts.rs | 1 - src/decoder/av1/mod.rs | 5 +- src/decoder/av1/obu.rs | 5 - src/decoder/av1/parser.rs | 38 +-- src/decoder/av1/record.rs | 55 ++++ src/decoder/av1/session.rs | 484 ++++++++++++++++++++++++++++++++ src/decoder/av1/std_info.rs | 3 + src/decoder/av1/stream_tests.rs | 2 +- src/decoder/driver.rs | 8 + src/decoder/h264/session.rs | 1 + src/decoder/h265/session.rs | 8 +- src/decoder/mod.rs | 13 +- src/decoder/record.rs | 32 ++- src/vulkan.rs | 2 +- tests/common/mod.rs | 9 +- tests/decode_streams.rs | 26 +- tests/roundtrip.rs | 15 +- 18 files changed, 645 insertions(+), 71 deletions(-) create mode 100644 src/decoder/av1/record.rs create mode 100644 src/decoder/av1/session.rs diff --git a/examples/common/mod.rs b/examples/common/mod.rs index c79d2ce7..4be7a670 100644 --- a/examples/common/mod.rs +++ b/examples/common/mod.rs @@ -344,10 +344,13 @@ impl Readback { // Crop to the visible region on the way out: the image is coded-size, // which is padded up to the codec's macroblock granularity. + // An odd size rounds the chroma up: the last chroma sample covers + // the last luma column (and row) on its own. let visible_y_stride = frame.width as usize * bytes; - let visible_uv_stride = visible_y_stride; + let visible_uv_stride = frame.width.div_ceil(2) as usize * 2 * bytes; + let uv_rows = frame.height.div_ceil(2) as usize; let mut y = Vec::with_capacity(visible_y_stride * frame.height as usize); - let mut uv = Vec::with_capacity(visible_uv_stride * frame.height as usize / 2); + let mut uv = Vec::with_capacity(visible_uv_stride * uv_rows); unsafe { // Map the whole allocation: a sub-range map has to respect // nonCoherentAtomSize, and WHOLE_SIZE sidesteps that. @@ -361,7 +364,7 @@ impl Readback { visible_y_stride, )); } - for row in 0..frame.height as usize / 2 { + for row in 0..uv_rows { uv.extend_from_slice(std::slice::from_raw_parts( ptr.add(y_size + row * uv_stride), visible_uv_stride, diff --git a/src/decoder/av1/consts.rs b/src/decoder/av1/consts.rs index 54ccf31b..29ea997f 100644 --- a/src/decoder/av1/consts.rs +++ b/src/decoder/av1/consts.rs @@ -43,7 +43,6 @@ pub const AFFINE: u8 = 3; /// Reference frame names (section 6.10.24), which index `OrderHints`, /// `RefFrameSignBias`, `gm_params` and the loop filter reference deltas. -pub const INTRA_FRAME: usize = 0; pub const LAST_FRAME: usize = 1; pub const LAST2_FRAME: usize = 2; pub const LAST3_FRAME: usize = 3; diff --git a/src/decoder/av1/mod.rs b/src/decoder/av1/mod.rs index 26f3323c..925d4f88 100644 --- a/src/decoder/av1/mod.rs +++ b/src/decoder/av1/mod.rs @@ -32,8 +32,11 @@ pub(crate) mod sequence; pub(crate) mod std_info; pub(crate) mod tile; +mod record; +mod session; + /// Parser tests against real libaom and SVT-AV1 streams (host-side, no GPU). #[cfg(test)] mod stream_tests; -pub(crate) use parser::{Av1Event, Av1Frame, Av1Parser}; +pub(crate) use session::Av1Decoder; diff --git a/src/decoder/av1/obu.rs b/src/decoder/av1/obu.rs index c8e38f56..1fa809bb 100644 --- a/src/decoder/av1/obu.rs +++ b/src/decoder/av1/obu.rs @@ -74,11 +74,6 @@ impl Obu<'_> { pub fn payload_offset(&self) -> usize { self.offset + self.header_len } - - /// Total size of the OBU, header included. - pub fn total_len(&self) -> usize { - self.header_len + self.payload.len() - } } /// Whether an OBU belongs to the operating point with the given diff --git a/src/decoder/av1/parser.rs b/src/decoder/av1/parser.rs index 3d049fb5..a2a8e77b 100644 --- a/src/decoder/av1/parser.rs +++ b/src/decoder/av1/parser.rs @@ -22,21 +22,19 @@ pub struct Av1Frame { pub header: FrameHeader, /// The OBU that carried the header: `Frame`, `FrameHeader` or /// `RedundantFrameHeader` (when the original was lost). + /// + /// This and the two fields below only check the parse in tests: the + /// decoder hands the driver tile data alone. + #[cfg_attr(not(test), allow(dead_code))] pub header_obu_type: ObuType, - /// Offset of the first byte of that OBU (its `obu_header()`), which is - /// what Vulkan's `frameHeaderOffset` asks for: "the byte offset of the - /// AV1 frame header OBU". + /// Offset of the first byte of that OBU (its `obu_header()`). + #[cfg_attr(not(test), allow(dead_code))] pub frame_header_offset: usize, - /// Offset of the first byte of the header's payload, the start of - /// `uncompressed_header()`. - pub frame_header_payload_offset: usize, /// Bytes of payload the header takes: `uncompressed_header()` plus its /// byte alignment in a frame OBU, plus its trailing bits in a frame /// header OBU. + #[cfg_attr(not(test), allow(dead_code))] pub frame_header_bytes: usize, - /// `temporal_id` and `spatial_id` of the header OBU. - pub temporal_id: u8, - pub spatial_id: u8, /// The tile groups in order, which together cover every tile. Empty for /// `show_existing_frame`. pub tile_groups: Vec, @@ -54,9 +52,9 @@ impl Av1Frame { /// What one OBU produced. #[derive(Debug)] pub enum Av1Event { - /// A sequence header was parsed. `changed` is false when it repeats the - /// one in effect, which encoders do at every key frame. - SequenceHeader { changed: bool }, + /// A sequence header was parsed. Encoders repeat it at every key frame; + /// the decoder compares it with the one its session was built from. + SequenceHeader, /// A frame is complete: all of its tile groups have arrived, or it is a /// `show_existing_frame`. The caller decodes (or shows) it and then calls /// [`Av1Parser::commit_frame`] or [`Av1Parser::discard_frame`]. @@ -113,16 +111,6 @@ impl Av1Parser { &self.refs } - /// Forget every reference and any frame in flight, keeping the sequence - /// header. After this, only a key frame decodes. - pub fn reset(&mut self) { - self.refs = RefState::default(); - self.order_hints = [0; TOTAL_REFS_PER_FRAME]; - self.current = None; - self.pending = None; - self.skipping = false; - } - /// Apply the reference frame update process for the pending frame, now /// decoded into DPB slot `dpb_slot` (`None` if the caller does not track /// it). For `show_existing_frame` of a key frame the shown slot's DPB @@ -176,9 +164,8 @@ impl Av1Parser { } ObuType::SequenceHeader => { let seq = parse_sequence_header(obu.payload)?; - let changed = self.sequence.as_ref() != Some(&seq); self.sequence = Some(seq); - Ok(Some(Av1Event::SequenceHeader { changed })) + Ok(Some(Av1Event::SequenceHeader)) } ObuType::FrameHeader | ObuType::RedundantFrameHeader | ObuType::Frame => { if !self.selected(obu)? { @@ -267,10 +254,7 @@ impl Av1Parser { header: *header, header_obu_type: obu.header.obu_type, frame_header_offset: obu.offset, - frame_header_payload_offset: obu.payload_offset(), frame_header_bytes: header_bytes, - temporal_id: obu.header.temporal_id, - spatial_id: obu.header.spatial_id, tile_groups: Vec::new(), }; if frame.header.show_existing_frame { diff --git a/src/decoder/av1/record.rs b/src/decoder/av1/record.rs new file mode 100644 index 00000000..7a3737f5 --- /dev/null +++ b/src/decoder/av1/record.rs @@ -0,0 +1,55 @@ +//! The AV1 structures of a decode command. + +use ash::vk; +use ash::vk::native::StdVideoDecodeAV1ReferenceInfo; + +use super::consts::REFS_PER_FRAME; +use super::std_info::StdPictureInfo; +use crate::decoder::record::RecordCodec; + +/// AV1's part of a [`DecodeRecord`](crate::decoder::record::DecodeRecord). +pub(super) struct Av1Record; + +/// What the AV1 picture info points at, besides the tile offsets the record +/// keeps. +#[derive(Default)] +pub(super) struct Av1Picture { + /// The Std picture info and its sub-structures. `None` only before the + /// first frame. + pub std: Option, + /// The DPB slot of each of LAST_FRAME..=ALTREF_FRAME, or -1. + pub reference_name_slot_indices: [i32; REFS_PER_FRAME], + /// Where the frame header OBU starts in the bitstream buffer. Always 0: + /// the buffer holds tile data alone. + pub frame_header_offset: u32, + /// The size of each tile, in the order of the record's tile offsets. + pub tile_sizes: Vec, +} + +impl RecordCodec for Av1Record { + type RefStd = StdVideoDecodeAV1ReferenceInfo; + type DpbSlot = vk::VideoDecodeAV1DpbSlotInfoKHR<'static>; + type Picture = Av1Picture; + type PictureInfo = vk::VideoDecodeAV1PictureInfoKHR<'static>; + + fn dpb_slot(std: &'static StdVideoDecodeAV1ReferenceInfo) -> Self::DpbSlot { + vk::VideoDecodeAV1DpbSlotInfoKHR::default().std_reference_info(std) + } + + fn picture_info( + picture: &'static mut Av1Picture, + offsets: &'static [u32], + ) -> Self::PictureInfo { + let std = picture + .std + .as_ref() + .expect("the picture info is filled before linking") + .get(); + vk::VideoDecodeAV1PictureInfoKHR::default() + .std_picture_info(std) + .reference_name_slot_indices(picture.reference_name_slot_indices) + .frame_header_offset(picture.frame_header_offset) + .tile_offsets(offsets) + .tile_sizes(&picture.tile_sizes) + } +} diff --git a/src/decoder/av1/session.rs b/src/decoder/av1/session.rs new file mode 100644 index 00000000..497fc252 --- /dev/null +++ b/src/decoder/av1/session.rs @@ -0,0 +1,484 @@ +//! The AV1 Vulkan decode session: lazy stream-driven setup and per-frame +//! record/submit. +//! +//! One unit of input is a temporal unit, which shows exactly one frame but can +//! decode more than one: a hidden frame (`show_frame` 0) that a later +//! `show_existing_frame` shows, followed by the frame shown now. Each frame is +//! parsed only once the one before it is committed, since its header depends +//! on the reference state that frame leaves behind. + +use ash::vk; +use ash::vk::TaggedStructure; +use tracing::debug; + +use super::frame_header::FrameType; +use super::obu::{ObuType, iter_obus, temporal_unit_starts}; +use super::parser::{Av1Event, Av1Frame, Av1Parser}; +use super::record::{Av1Picture, Av1Record}; +use super::sequence::SequenceHeader; +use super::std_info::{ + StdPictureInfo, StdSequenceHeader, reference_name_slot_indices, std_reference_info, +}; +use crate::decoder::DecodeConfig; +use crate::decoder::common::DecoderCommon; +use crate::decoder::driver::{CodecDecoder, Decoded}; +use crate::decoder::frames::DecodedPicture; +use crate::decoder::pipeline::DECODE_PIPELINE_DEPTH; +use crate::decoder::plan::StreamNeeds; +use crate::decoder::profile::DecodeProfile; +use crate::decoder::record::{DecodeRecord, LinkParams}; +use crate::encoder::{BitDepth, ChromaFormat}; +use crate::error::{PixelForgeError, Result}; +use crate::vulkan::VideoContext; + +/// The most DPB slots the AV1 decoder uses: the 8 reference frames, the frame +/// being decoded, and spare slots for frames held for output. +const MAX_DPB_SLOTS: u32 = 17; + +/// AV1 decoder. +/// +/// Owns only what is specific to AV1: the OBU parser with its reference state. +/// Everything Vulkan lives in [`DecoderCommon`]. +pub(crate) struct Av1Decoder { + common: DecoderCommon, + + /// How many DPB slots to reserve beyond the stream's needs, so the caller + /// can hold decoded frames while decoding continues. + output_depth: usize, + + parser: Av1Parser, + /// The sequence header the current session was built from. + active: Option, + /// The Std sequence header the current session parameters were created + /// from, kept for as long as they exist. + /// + /// NVIDIA (615.78.08) does not copy it when the parameters are created: + /// it reads it again at every decode. Freed, its memory is reused, and the + /// decode reads whatever is there now. Nothing fails; the chroma of every + /// inter frame just comes out slightly wrong, which is how this was found. + /// Boxed, so moving the decoder does not move it. + std_sequence: Option>, + /// DPB slots in the current session. + slot_count: usize, + + /// Shown frames since the last key frame, which is the display order: AV1 + /// shows frames in the order the stream gives them. + shown_since_key: i32, + + /// What each pipeline slot's decode command was recorded from, indexed like + /// the pipeline's slots. + records: Box<[DecodeRecord]>, +} + +impl Av1Decoder { + pub(crate) fn create(context: VideoContext, config: &DecodeConfig) -> Result { + Ok(Self { + common: DecoderCommon::new(context, config.consumer_queue_family)?, + output_depth: config.output_depth, + parser: Av1Parser::new(), + active: None, + std_sequence: None, + slot_count: 0, + shown_since_key: 0, + records: (0..DECODE_PIPELINE_DEPTH) + .map(|_| DecodeRecord::new(Av1Picture::default())) + .collect(), + }) + } + + /// The decode profile for a sequence header. + fn profile_for(seq: &SequenceHeader) -> Result<(DecodeProfile, ChromaFormat, BitDepth)> { + let cc = &seq.color_config; + if cc.mono_chrome { + return Err(PixelForgeError::InvalidInput( + "AV1 decode: monochrome streams are not supported".to_string(), + )); + } + let chroma_format = match (cc.subsampling_x, cc.subsampling_y) { + (true, true) => ChromaFormat::Yuv420, + (true, false) => ChromaFormat::Yuv422, + (false, false) => ChromaFormat::Yuv444, + (false, true) => { + return Err(PixelForgeError::InvalidInput( + "AV1 decode: invalid chroma subsampling".to_string(), + )); + } + }; + let bit_depth = match cc.bit_depth { + 8 => BitDepth::Eight, + 10 => BitDepth::Ten, + other => { + return Err(PixelForgeError::InvalidInput(format!( + "AV1 decode: unsupported bit depth {other}" + ))); + } + }; + if seq.film_grain_params_present { + return Err(PixelForgeError::InvalidInput( + "AV1 decode: film grain is not supported yet".to_string(), + )); + } + let profile = DecodeProfile::av1( + seq.seq_profile as ash::vk::native::StdVideoAV1Profile, + false, + chroma_format, + bit_depth, + ); + Ok((profile, chroma_format, bit_depth)) + } + + /// Create (or recreate) the Vulkan session for the sequence header in + /// effect. + fn activate(&mut self) -> Result<()> { + let seq = self + .parser + .sequence_header() + .expect("a frame implies a sequence header") + .clone(); + if self.common.session.is_some() && self.active.as_ref() == Some(&seq) { + return Ok(()); + } + + let (profile, chroma_format, bit_depth) = Self::profile_for(&seq)?; + let needs = StreamNeeds { + codec: "AV1", + width: seq.max_frame_width(), + height: seq.max_frame_height(), + chroma_format, + bit_depth, + references: super::consts::NUM_REF_FRAMES as u32, + active_references: super::consts::REFS_PER_FRAME as u32, + slot_limit: MAX_DPB_SLOTS, + // Frames are shown in the order the stream gives them. + reorder_depth: 0, + output_depth: self.output_depth, + }; + let mut av1_caps = vk::VideoDecodeAV1CapabilitiesKHR::default(); + let plan = self + .common + .plan_session(profile.info(), &mut av1_caps, &needs)?; + if !plan.coincide { + // A frame shown again by `show_existing_frame` is read from its DPB + // slot, which only a coincident DPB hands out. + return Err(PixelForgeError::NoSuitableDevice( + "AV1 decode: needs a device whose DPB and output images coincide".to_string(), + )); + } + self.slot_count = plan.slot_count; + let std_seq = Box::new(StdSequenceHeader::new(&seq)); + self.common + .create_session(&plan, profile, |common, session| { + Self::build_session_params(common, session, &std_seq) + })?; + // The old session parameters are gone by now, so the header they + // pointed at can go too. + self.std_sequence = Some(std_seq); + self.active = Some(seq); + Ok(()) + } + + fn build_session_params( + common: &DecoderCommon, + session: vk::VideoSessionKHR, + std_seq: &StdSequenceHeader, + ) -> Result { + let mut av1_create = vk::VideoDecodeAV1SessionParametersCreateInfoKHR::default() + .std_sequence_header(std_seq.get()); + let create_info = vk::VideoSessionParametersCreateInfoKHR::default() + .video_session(session) + .push(&mut av1_create); + unsafe { + common + .video_queue_fn + .create_video_session_parameters(&create_info, None) + } + .map_err(PixelForgeError::vk("vkCreateVideoSessionParametersKHR")) + } + + /// Decode every frame of one temporal unit, returning the one it shows. + fn decode_temporal_unit(&mut self, tu: &[u8], pts: u64) -> Result { + let mut outcome = Decoded::Hidden; + let mut missing: Option = None; + for obu in iter_obus(tu) { + let obu = obu?; + // Frame data from before the first sequence header belongs to a + // stream joined mid-way. + let is_frame_data = matches!( + obu.header.obu_type, + ObuType::Frame + | ObuType::FrameHeader + | ObuType::RedundantFrameHeader + | ObuType::TileGroup + ); + if is_frame_data && self.parser.sequence_header().is_none() { + missing.get_or_insert_with(|| "no sequence header received yet".to_string()); + continue; + } + match self.parser.parse_obu(&obu)? { + None | Some(Av1Event::SequenceHeader) => {} + Some(Av1Event::MissingReferences(slots)) => { + missing.get_or_insert_with(|| { + format!("frame references empty slots (mask {slots:#04x})") + }); + } + Some(Av1Event::Frame(frame)) => { + // The frame shown by the previous one has already been + // handed over; close off its pipeline slot first. + if let Decoded::Shown(_) = outcome { + return Err(PixelForgeError::InvalidInput( + "AV1 decode: temporal unit shows more than one frame".to_string(), + )); + } + let result = self.decode_frame(tu, &frame, pts); + match result { + Ok(Some(picture)) => outcome = Decoded::Shown(picture), + Ok(None) => { + // A hidden frame: its work is complete as far as + // recording goes. + self.common.end_picture(Vec::new()); + } + Err(e) => { + self.parser.discard_frame(); + return Err(e); + } + } + } + } + } + match (outcome, missing) { + (Decoded::Hidden, Some(reason)) => Ok(Decoded::NeedsKeyframe(reason)), + (outcome, _) => Ok(outcome), + } + } + + /// Decode (or show) one frame and commit it to the reference state. + /// Returns the picture to output when the frame is shown. + fn decode_frame( + &mut self, + tu: &[u8], + frame: &Av1Frame, + pts: u64, + ) -> Result> { + let header = &frame.header; + self.activate()?; + + if header.show_existing_frame { + let shown = self.parser.refs().slots[header.frame_to_show_map_idx as usize].clone(); + let slot = shown.dpb_slot.ok_or_else(|| { + PixelForgeError::InvalidInput( + "AV1 decode: show_existing_frame of a slot with no picture".to_string(), + ) + })?; + let is_key = shown.frame_type == FrameType::Key; + if is_key { + self.shown_since_key = 0; + } + let picture = self.common.decoded_picture( + slot, + shown.upscaled_width, + shown.frame_height, + pts, + self.shown_since_key, + is_key, + ); + self.shown_since_key += 1; + self.parser.commit_frame(None)?; + return Ok(Some(picture)); + } + + let slot = self.allocate_slot()?; + self.common.begin_decode_commands()?; + // Only the tile data goes to the driver, each tile at the offset the + // staging gives it. + let tile_data: Vec<&[u8]> = frame + .tiles() + .map(|t| &tu[t.offset..t.offset + t.size]) + .collect(); + let (buffer_range, offsets) = self.common.stage_slices(&tile_data, &[])?; + self.common.record_barriers(slot); + self.record_decode(frame, slot, buffer_range, &offsets)?; + self.common.submit_decode()?; + + let size = &header.frame_size; + let is_key = header.frame_type == FrameType::Key; + if is_key && header.show_frame { + self.shown_since_key = 0; + } + let picture = self.common.decoded_picture( + slot, + size.upscaled_width, + size.frame_height, + pts, + self.shown_since_key, + is_key && header.show_frame, + ); + self.parser.commit_frame(Some(slot))?; + if !header.show_frame { + return Ok(None); + } + self.shown_since_key += 1; + Ok(Some(picture)) + } + + /// A DPB slot no reference frame holds and no caller's frame pins. + fn allocate_slot(&self) -> Result { + loop { + let pins = &self.common.session()?.slot_pins; + let refs = self.parser.refs(); + if let Some(slot) = + (0..self.slot_count as u8).find(|&s| !refs.dpb_slot_in_use(s) && !pins.is_pinned(s)) + { + return Ok(slot); + } + if !pins.any_pinned() { + return Err(PixelForgeError::InvalidInput( + "AV1 decode: no free DPB slot".to_string(), + )); + } + pins.wait_for_release(); + } + } + + /// Fill and link this pipeline slot's record, and record the coding + /// commands from it. + fn record_decode( + &mut self, + frame: &Av1Frame, + slot: u8, + buffer_range: u64, + offsets: &[u32], + ) -> Result<()> { + let header = &frame.header; + let active = self.common.session.as_ref().expect("active"); + let refs = self.parser.refs(); + + let tiles: Vec<_> = frame.tiles().collect(); + let record = &mut self.records[self.common.pipeline.current_index()]; + record.reset(offsets); + + // Every picture the frame references, once each: several reference + // names can point at the same picture. + let mut pushed = [false; MAX_DPB_SLOTS as usize]; + let named: Vec = if header.frame_is_intra() { + Vec::new() + } else { + header.ref_frame_idx.iter().map(|&i| i as usize).collect() + }; + for r in named.iter().map(|&i| &refs.slots[i]).filter(|r| r.valid) { + let Some(dpb_slot) = r.dpb_slot else { continue }; + if std::mem::replace(&mut pushed[dpb_slot as usize], true) { + continue; + } + record.push_reference( + dpb_slot, + active.dpb_entry(dpb_slot).2, + Some(vk::Extent2D { + width: r.upscaled_width, + height: r.frame_height, + }), + std_reference_info(r), + ); + } + + // What the slot set up here will hold once this frame is a reference. + // SAFETY: plain C data, for which all-zero is valid. + let mut setup_std: ash::vk::native::StdVideoDecodeAV1ReferenceInfo = + unsafe { std::mem::zeroed() }; + setup_std + .flags + .set_disable_frame_end_update_cdf(header.disable_frame_end_update_cdf as u32); + setup_std + .flags + .set_segmentation_enabled(header.segmentation.segmentation_enabled as u32); + setup_std.frame_type = header.frame_type as u8; + setup_std.RefFrameSignBias = header.ref_frame_sign_bias_mask(); + setup_std.OrderHint = header.order_hint; + setup_std.SavedOrderHints = header.order_hints; + + let picture = record.picture_mut(); + picture.std = Some(StdPictureInfo::new(header)); + picture.reference_name_slot_indices = reference_name_slot_indices(header, &refs.slots); + // The buffer holds tile data alone, as ffmpeg hands it over, so there is + // no header in it to point at. + picture.frame_header_offset = 0; + picture.tile_sizes.clear(); + picture + .tile_sizes + .extend(tiles.iter().map(|t| t.size as u32)); + + let size = &header.frame_size; + record.link(&LinkParams { + extent: vk::Extent2D { + width: size.upscaled_width, + height: size.frame_height, + }, + session: active.session, + session_params: active.session_params, + bitstream: self.common.bitstream_buffer(), + bitstream_range: buffer_range, + setup_slot: slot, + setup_view: active.dpb_entry(slot).2, + output_view: active.output_image.map(|(_, _, view)| view), + setup_std, + }); + self.common.record_coding(record); + Ok(()) + } +} + +impl CodecDecoder for Av1Decoder { + type Unit<'a> = &'a [u8]; + + const NAME: &'static str = "AV1"; + + fn common(&self) -> &DecoderCommon { + &self.common + } + + fn common_mut(&mut self) -> &mut DecoderCommon { + &mut self.common + } + + fn complete_prefix(buffer: &[u8]) -> usize { + // An error here is input that is not low-overhead OBUs; decoding the + // buffer reports it. + temporal_unit_starts(buffer) + .ok() + .and_then(|starts| starts.last().copied()) + .unwrap_or(0) + } + + fn split_units<'a>(&mut self, data: &'a [u8]) -> Result<(Vec<&'a [u8]>, Option)> { + let mut starts = temporal_unit_starts(data)?; + // Data before the first temporal delimiter is a temporal unit too, + // when the caller strips delimiters (as some containers do). + if starts.first() != Some(&0) { + starts.insert(0, 0); + } + let units = starts + .iter() + .zip(starts.iter().skip(1).chain([&data.len()])) + .map(|(&start, &end)| &data[start..end]) + .filter(|unit| !unit.is_empty()) + .collect(); + Ok((units, None)) + } + + /// AV1 tracks which reference slots hold what itself and reports a frame + /// it cannot decode, so every temporal unit is offered. + fn is_keyframe(_unit: &&[u8]) -> bool { + true + } + + fn decode_unit(&mut self, unit: &&[u8], pts: u64) -> Result { + let outcome = self.decode_temporal_unit(unit, pts)?; + if let Decoded::NeedsKeyframe(reason) = &outcome { + debug!("AV1 decode: {reason}"); + } + Ok(outcome) + } + + fn reorder_depth(&self) -> usize { + 0 + } +} diff --git a/src/decoder/av1/std_info.rs b/src/decoder/av1/std_info.rs index a4abc766..64523347 100644 --- a/src/decoder/av1/std_info.rs +++ b/src/decoder/av1/std_info.rs @@ -26,6 +26,9 @@ pub struct StdSequenceHeader { _timing: Option>, } +// SAFETY: the raw pointers only point into the boxes this value owns. +unsafe impl Send for StdSequenceHeader {} + impl StdSequenceHeader { pub fn new(seq: &SequenceHeader) -> Self { let cc = &seq.color_config; diff --git a/src/decoder/av1/stream_tests.rs b/src/decoder/av1/stream_tests.rs index e9134969..515f113f 100644 --- a/src/decoder/av1/stream_tests.rs +++ b/src/decoder/av1/stream_tests.rs @@ -115,7 +115,7 @@ fn parse_stream(name: &str, data: &[u8]) -> Parsed { Some(Av1Event::MissingReferences(slots)) => { panic!("{name}: missing references {slots:#010b}") } - Some(Av1Event::SequenceHeader { .. }) | None => {} + Some(Av1Event::SequenceHeader) | None => {} } } Parsed { diff --git a/src/decoder/driver.rs b/src/decoder/driver.rs index a1cfbf0e..fbd48081 100644 --- a/src/decoder/driver.rs +++ b/src/decoder/driver.rs @@ -63,6 +63,10 @@ pub(crate) enum Decoded { /// Not decoded, and that is normal: an H.265 RASL picture after the CRA /// that started decoding, which references pictures from before it. Skipped, + /// Not decoded, because it references state the decoder does not have: + /// pictures or parameter sets from before the stream was joined, or lost + /// since. The reason names what is missing. + NeedsKeyframe(String), } /// Drives a [`CodecDecoder`]. @@ -172,6 +176,10 @@ impl StreamDecoder { } Decoded::Hidden => Vec::new(), Decoded::Skipped => continue, + Decoded::NeedsKeyframe(reason) => { + awaiting.get_or_insert(reason); + continue; + } }; decoded_any = true; // A decoded keyframe re-establishes a valid decode point. diff --git a/src/decoder/h264/session.rs b/src/decoder/h264/session.rs index ce7c71a2..660cb41b 100644 --- a/src/decoder/h264/session.rs +++ b/src/decoder/h264/session.rs @@ -473,6 +473,7 @@ impl H264Decoder { record.push_reference( r.slot, active.dpb_entry(r.slot).2, + None, ash::vk::native::StdVideoDecodeH264ReferenceInfo { flags, FrameNum: frame_num, diff --git a/src/decoder/h265/session.rs b/src/decoder/h265/session.rs index 24cbf0e2..f5a9b5d8 100644 --- a/src/decoder/h265/session.rs +++ b/src/decoder/h265/session.rs @@ -327,11 +327,10 @@ impl H265Decoder { // A picture that predicts from one the decoder does not have // would decode to garbage, and so would everything after it that // predicts from it in turn. - debug!( - "H.265 decode: picture POC {} references a missing picture; skipped", + return Ok(Decoded::NeedsKeyframe(format!( + "picture POC {} references a picture that is not in the DPB", state.poc - ); - return Ok(Decoded::Skipped); + ))); } // Every slot can be busy because a frame the caller still holds pins @@ -398,6 +397,7 @@ impl H265Decoder { record.push_reference( r.slot, active.dpb_entry(r.slot).2, + None, ash::vk::native::StdVideoDecodeH265ReferenceInfo { flags, PicOrderCntVal: r.poc, diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 2a7c3a5d..64daca3c 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -40,8 +40,7 @@ /// Annex B start-code framing, shared by H.264 and H.265. pub(crate) mod annexb; -/// AV1 OBU parsing and reference state (host side; not wired up yet). -#[allow(dead_code, unused_imports)] +/// AV1 decoding. pub(crate) mod av1; pub(crate) mod bitreader; /// The Vulkan video session and the per-decoder state every codec shares. @@ -420,12 +419,10 @@ impl Decoder { h265::H265Decoder::create(context, &config)?, &config, )), - other => { - return Err(PixelForgeError::InvalidInput(format!( - "{:?} decoding is not implemented yet", - other - ))); - } + Codec::AV1 => Box::new(driver::StreamDecoder::new( + av1::Av1Decoder::create(context, &config)?, + &config, + )), }; let rx = inner .take_frame_receiver() diff --git a/src/decoder/record.rs b/src/decoder/record.rs index 3734d9c4..485f7882 100644 --- a/src/decoder/record.rs +++ b/src/decoder/record.rs @@ -68,8 +68,9 @@ pub(crate) struct DecodeRecord { // Plain data. offsets: Vec, ref_count: usize, - /// DPB slot and image view of each reference. - refs: [(u8, vk::ImageView); MAX_REFS], + /// DPB slot, image view and coded extent of each reference. A `None` + /// extent is the picture's own. + refs: [(u8, vk::ImageView, Option); MAX_REFS], ref_std: [C::RefStd; MAX_REFS], setup_std: C::RefStd, picture: C::Picture, @@ -142,7 +143,7 @@ impl DecodeRecord { Self { offsets: Vec::new(), ref_count: 0, - refs: [(0, vk::ImageView::null()); MAX_REFS], + refs: [(0, vk::ImageView::null(), None); MAX_REFS], ref_std: [zeroed_ref; MAX_REFS], setup_std: zeroed_ref, picture, @@ -178,9 +179,19 @@ impl DecodeRecord { } /// Add a reference picture, in the order the driver is to see it. - pub(crate) fn push_reference(&mut self, slot: u8, view: vk::ImageView, std: C::RefStd) { + /// + /// `extent` is the reference's coded extent when it differs from the + /// picture's own, which only AV1 allows (a frame size change between key + /// frames). + pub(crate) fn push_reference( + &mut self, + slot: u8, + view: vk::ImageView, + extent: Option, + std: C::RefStd, + ) { assert!(self.ref_count < MAX_REFS, "too many references"); - self.refs[self.ref_count] = (slot, view); + self.refs[self.ref_count] = (slot, view, extent); self.ref_std[self.ref_count] = std; self.ref_count += 1; } @@ -193,20 +204,21 @@ impl DecodeRecord { let n = self.ref_count; self.setup_std = p.setup_std; - let resource = |view: vk::ImageView| { + let resource = |view: vk::ImageView, extent: vk::Extent2D| { vk::VideoPictureResourceInfoKHR::default() .coded_offset(vk::Offset2D { x: 0, y: 0 }) - .coded_extent(p.extent) + .coded_extent(extent) .base_array_layer(0) .image_view_binding(view) }; for i in 0..n { - self.ref_resources[i] = resource(self.refs[i].1); + let (_, view, extent) = self.refs[i]; + self.ref_resources[i] = resource(view, extent.unwrap_or(p.extent)); } - self.setup_resource = resource(p.setup_view); + self.setup_resource = resource(p.setup_view, p.extent); self.output_resource = match p.output_view { None => self.setup_resource, - Some(view) => resource(view), + Some(view) => resource(view, p.extent), }; // SAFETY (for every `stable`/`stable_mut` below): the target is a field diff --git a/src/vulkan.rs b/src/vulkan.rs index 252ed5e7..f97d385e 100644 --- a/src/vulkan.rs +++ b/src/vulkan.rs @@ -581,7 +581,7 @@ fn decode_extension_name(codec: Codec) -> &'static std::ffi::CStr { } /// The codecs pixelforge can decode, on a device that supports them. -const DECODE_CODECS: &[Codec] = &[Codec::H264, Codec::H265]; +const DECODE_CODECS: &[Codec] = &[Codec::H264, Codec::H265, Codec::AV1]; /// The decode codecs `physical_device` supports. fn query_decode_codecs( diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 62dbbc98..5d8e85ca 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -309,10 +309,13 @@ impl Readback { // Crop to the visible region on the way out: the image is coded-size, // which is padded up to the codec's macroblock granularity. + // An odd size rounds the chroma up: the last chroma sample covers + // the last luma column (and row) on its own. let visible_y_stride = frame.width as usize * bytes; - let visible_uv_stride = visible_y_stride; + let visible_uv_stride = frame.width.div_ceil(2) as usize * 2 * bytes; + let uv_rows = frame.height.div_ceil(2) as usize; let mut y = Vec::with_capacity(visible_y_stride * frame.height as usize); - let mut uv = Vec::with_capacity(visible_uv_stride * frame.height as usize / 2); + let mut uv = Vec::with_capacity(visible_uv_stride * uv_rows); unsafe { // Map the whole allocation: a sub-range map has to respect // nonCoherentAtomSize, and WHOLE_SIZE sidesteps that. @@ -326,7 +329,7 @@ impl Readback { visible_y_stride, )); } - for row in 0..frame.height as usize / 2 { + for row in 0..uv_rows { uv.extend_from_slice(std::slice::from_raw_parts( ptr.add(y_size + row * uv_stride), visible_uv_stride, diff --git a/tests/decode_streams.rs b/tests/decode_streams.rs index 661537a8..2bd46be4 100644 --- a/tests/decode_streams.rs +++ b/tests/decode_streams.rs @@ -26,13 +26,22 @@ fn check(codec: Codec, name: &str, pix_fmt: &str) -> Result<(), Box Result<(), Box> { roundtrip(Codec::H265, "h265") } +#[test] +#[ignore = "requires a Vulkan Video device and ffmpeg"] +fn roundtrip_av1() -> Result<(), Box> { + roundtrip(Codec::AV1, "obu") +} + fn roundtrip(codec: Codec, extension: &str) -> Result<(), Box> { let _ = tracing_subscriber::fmt::try_init(); @@ -119,8 +125,13 @@ fn roundtrip(codec: Codec, extension: &str) -> Result<(), Box Date: Thu, 5 Nov 2026 13:01:37 +0100 Subject: [PATCH 16/17] feat(decoder): apply AV1 film grain --- src/decoder/av1/session.rs | 40 ++++++++++++---- src/decoder/common.rs | 91 +++++++++++++++++++++++++++++++++++++ src/decoder/frames.rs | 8 +++- src/decoder/h264/session.rs | 1 + src/decoder/h265/session.rs | 1 + src/decoder/plan.rs | 6 +++ tests/decode_streams.rs | 2 + 7 files changed, 138 insertions(+), 11 deletions(-) diff --git a/src/decoder/av1/session.rs b/src/decoder/av1/session.rs index 497fc252..5a5e271c 100644 --- a/src/decoder/av1/session.rs +++ b/src/decoder/av1/session.rs @@ -60,6 +60,10 @@ pub(crate) struct Av1Decoder { std_sequence: Option>, /// DPB slots in the current session. slot_count: usize, + /// Which DPB slots hold a frame decoded with film grain, whose output is + /// in the slot's grain image. A `show_existing_frame` of it shows that + /// image again. + grain_applied: [bool; MAX_DPB_SLOTS as usize], /// Shown frames since the last key frame, which is the display order: AV1 /// shows frames in the order the stream gives them. @@ -79,6 +83,7 @@ impl Av1Decoder { active: None, std_sequence: None, slot_count: 0, + grain_applied: [false; MAX_DPB_SLOTS as usize], shown_since_key: 0, records: (0..DECODE_PIPELINE_DEPTH) .map(|_| DecodeRecord::new(Av1Picture::default())) @@ -113,14 +118,10 @@ impl Av1Decoder { ))); } }; - if seq.film_grain_params_present { - return Err(PixelForgeError::InvalidInput( - "AV1 decode: film grain is not supported yet".to_string(), - )); - } + // A session that may apply film grain has to say so in its profile. let profile = DecodeProfile::av1( seq.seq_profile as ash::vk::native::StdVideoAV1Profile, - false, + seq.film_grain_params_present, chroma_format, bit_depth, ); @@ -152,6 +153,7 @@ impl Av1Decoder { // Frames are shown in the order the stream gives them. reorder_depth: 0, output_depth: self.output_depth, + film_grain: seq.film_grain_params_present, }; let mut av1_caps = vk::VideoDecodeAV1CapabilitiesKHR::default(); let plan = self @@ -273,7 +275,7 @@ impl Av1Decoder { if is_key { self.shown_since_key = 0; } - let picture = self.common.decoded_picture( + let mut picture = self.common.decoded_picture( slot, shown.upscaled_width, shown.frame_height, @@ -281,6 +283,9 @@ impl Av1Decoder { self.shown_since_key, is_key, ); + if self.grain_applied[slot as usize] { + self.common.use_grain_output(&mut picture); + } self.shown_since_key += 1; self.parser.commit_frame(None)?; return Ok(Some(picture)); @@ -295,16 +300,21 @@ impl Av1Decoder { .map(|t| &tu[t.offset..t.offset + t.size]) .collect(); let (buffer_range, offsets) = self.common.stage_slices(&tile_data, &[])?; + let grain = header.film_grain.apply_grain; self.common.record_barriers(slot); - self.record_decode(frame, slot, buffer_range, &offsets)?; + if grain { + self.common.record_grain_barrier(slot); + } + self.record_decode(frame, slot, buffer_range, &offsets, grain)?; self.common.submit_decode()?; + self.grain_applied[slot as usize] = grain; let size = &header.frame_size; let is_key = header.frame_type == FrameType::Key; if is_key && header.show_frame { self.shown_since_key = 0; } - let picture = self.common.decoded_picture( + let mut picture = self.common.decoded_picture( slot, size.upscaled_width, size.frame_height, @@ -312,6 +322,9 @@ impl Av1Decoder { self.shown_since_key, is_key && header.show_frame, ); + if grain { + self.common.use_grain_output(&mut picture); + } self.parser.commit_frame(Some(slot))?; if !header.show_frame { return Ok(None); @@ -347,6 +360,7 @@ impl Av1Decoder { slot: u8, buffer_range: u64, offsets: &[u32], + grain: bool, ) -> Result<()> { let header = &frame.header; let active = self.common.session.as_ref().expect("active"); @@ -418,7 +432,13 @@ impl Av1Decoder { bitstream_range: buffer_range, setup_slot: slot, setup_view: active.dpb_entry(slot).2, - output_view: active.output_image.map(|(_, _, view)| view), + // With grain, the output must not be the reference picture, which + // stays grain-free. + output_view: if grain { + Some(active.grain_view(slot)) + } else { + active.output_image.map(|(_, _, view)| view) + }, setup_std, }); self.common.record_coding(record); diff --git a/src/decoder/common.rs b/src/decoder/common.rs index 8d5ebf96..bfabd236 100644 --- a/src/decoder/common.rs +++ b/src/decoder/common.rs @@ -136,6 +136,15 @@ pub(crate) struct DecodeSession { pub plane_views: bool, /// Distinct decode output image, only when `!coincide`. pub output_image: Option<(vk::Image, vk::DeviceMemory, vk::ImageView)>, + /// One film grain output image per DPB slot, or none. + /// + /// Vulkan applies AV1 film grain only to a decode output that is not the + /// DPB slot, since the reference must stay grain-free. A frame with grain + /// is therefore decoded into its slot's grain image as well, and handed + /// out from there. One per slot rather than one in all, because + /// `show_existing_frame` can show a frame again long after its decode, and + /// it has to show the same grain. + pub grain_outputs: Vec>, /// The profile everything in this session was created with. pub profile: DecodeProfile, } @@ -148,6 +157,17 @@ impl DecodeSession { (entry.image, layer) } + /// The film grain output image of `slot`. Only for a session with film + /// grain. + pub fn grain_entry(&self, slot: u8) -> &Arc { + &self.grain_outputs[slot as usize] + } + + /// The view of `slot`'s film grain output image. + pub fn grain_view(&self, slot: u8) -> vk::ImageView { + self.grain_outputs[slot as usize].views[0] + } + /// The image backing `slot`, the array layer within it, and its view. pub fn dpb_entry(&self, slot: u8) -> (&Arc, u32, vk::ImageView) { if self.use_layered_dpb { @@ -403,6 +423,32 @@ impl DecoderCommon { )?) }; + let grain_outputs = if plan.film_grain { + let mut usage = + vk::ImageUsageFlags::VIDEO_DECODE_DST_KHR | vk::ImageUsageFlags::TRANSFER_SRC; + if plan.sampleable { + usage |= vk::ImageUsageFlags::SAMPLED; + } + let grain_params = VideoImageParams { + usage, + ..dpb_params + }; + (0..plan.slot_count) + .map(|_| { + let (image, memory, view) = + create_video_image(&self.context, &grain_params, profile_info)?; + Ok(Arc::new(DpbImage { + context: self.context.clone(), + image, + memory, + views: vec![view], + })) + }) + .collect::>>()? + } else { + Vec::new() + }; + let session_params = make_params(self, session)?; // A new set of images, so a new generation. Frames from the previous @@ -427,6 +473,7 @@ impl DecoderCommon { sampleable: plan.sampleable, plane_views: plan.plane_views, output_image, + grain_outputs, profile, }); @@ -584,6 +631,49 @@ impl DecoderCommon { unsafe { sync2.cmd_pipeline_barrier2(self.decode_command_buffer(), &dependency) }; } + /// Transition `slot`'s film grain output image into the layout + /// `vkCmdDecodeVideo` writes it in. Its old contents are discarded: the + /// slot is free, so nothing still shows them. + pub fn record_grain_barrier(&self, slot: u8) { + let session = self.session.as_ref().expect("active session"); + let barrier = vk::ImageMemoryBarrier2::default() + .src_stage_mask(vk::PipelineStageFlags2::ALL_COMMANDS) + .src_access_mask(vk::AccessFlags2::empty()) + .dst_stage_mask(vk::PipelineStageFlags2::VIDEO_DECODE_KHR) + .dst_access_mask(vk::AccessFlags2::VIDEO_DECODE_WRITE_KHR) + .old_layout(vk::ImageLayout::UNDEFINED) + .new_layout(self.output_layout()) + .src_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .dst_queue_family_index(vk::QUEUE_FAMILY_IGNORED) + .image(session.grain_entry(slot).image) + .subresource_range(vk::ImageSubresourceRange { + aspect_mask: vk::ImageAspectFlags::COLOR, + base_mip_level: 0, + level_count: 1, + base_array_layer: 0, + layer_count: 1, + }); + let barriers = [barrier]; + let dependency = vk::DependencyInfo::default().image_memory_barriers(&barriers); + unsafe { + self.context + .sync2() + .cmd_pipeline_barrier2(self.decode_command_buffer(), &dependency) + }; + } + + /// Point `picture` at its slot's film grain output image instead of the + /// slot itself. + pub fn use_grain_output(&self, picture: &mut DecodedPicture) { + let session = self.session.as_ref().expect("active session"); + let entry = session.grain_entry(picture.slot); + picture.image = entry.image; + picture.image_view = entry.views[0]; + picture.layout = self.output_layout(); + picture.array_layer = 0; + picture.film_grain = true; + } + /// The decode command buffer of the picture currently being recorded. pub fn decode_command_buffer(&self) -> vk::CommandBuffer { self.pipeline.current().decode_command_buffer @@ -696,6 +786,7 @@ impl DecoderCommon { pts, display_order, is_keyframe, + film_grain: false, } } diff --git a/src/decoder/frames.rs b/src/decoder/frames.rs index 1fa09d3f..4ab771db 100644 --- a/src/decoder/frames.rs +++ b/src/decoder/frames.rs @@ -42,6 +42,8 @@ pub(crate) struct DecodedPicture { pub pts: u64, pub display_order: i32, pub is_keyframe: bool, + /// The picture is in its slot's film grain output image, not in the slot. + pub film_grain: bool, } /// An image in the frame pool, by index. @@ -450,7 +452,11 @@ impl ReorderBuffer { // holds frames gets copies rather than a decoder that cannot proceed. if session.pinnable && session.slot_pins.count() < session.spare_slots { session.slot_pins.pin(picture.slot); - let (entry, _, _) = session.dpb_entry(picture.slot); + let entry = if picture.film_grain { + session.grain_entry(picture.slot) + } else { + session.dpb_entry(picture.slot).0 + }; return Ok(Retained::Slot { pin: FramePin::DpbSlot { slot: picture.slot, diff --git a/src/decoder/h264/session.rs b/src/decoder/h264/session.rs index 660cb41b..53c8b50a 100644 --- a/src/decoder/h264/session.rs +++ b/src/decoder/h264/session.rs @@ -202,6 +202,7 @@ impl H264Decoder { slot_limit: crate::decoder::h264::dpb::MAX_DPB_SLOTS as u32, reorder_depth, output_depth: self.output_depth, + film_grain: false, }; let mut h264_caps = vk::VideoDecodeH264CapabilitiesKHR::default(); let plan = self diff --git a/src/decoder/h265/session.rs b/src/decoder/h265/session.rs index f5a9b5d8..db29a547 100644 --- a/src/decoder/h265/session.rs +++ b/src/decoder/h265/session.rs @@ -161,6 +161,7 @@ impl H265Decoder { slot_limit: super::dpb::MAX_DPB_SLOTS as u32, reorder_depth, output_depth: self.output_depth, + film_grain: false, }; let mut h265_caps = vk::VideoDecodeH265CapabilitiesKHR::default(); let plan = self diff --git a/src/decoder/plan.rs b/src/decoder/plan.rs index 5206ccf2..a1d733f9 100644 --- a/src/decoder/plan.rs +++ b/src/decoder/plan.rs @@ -35,6 +35,9 @@ pub(crate) struct StreamNeeds { pub reorder_depth: usize, /// Frames the caller may hold at once (`DecodeConfig::output_depth`). pub output_depth: usize, + /// The stream applies film grain (AV1), which needs an output image apart + /// from the DPB slot for every slot. + pub film_grain: bool, } /// What a codec decided the session should look like. @@ -79,6 +82,8 @@ pub(crate) struct SessionPlan { /// One layered DPB image rather than one image per slot. pub use_layered_dpb: bool, pub dpb_usage: vk::ImageUsageFlags, + /// Create a film grain output image for every DPB slot. + pub film_grain: bool, /// The Std header version the device reports for this profile. pub std_header_version: vk::ExtensionProperties, /// Bitstream buffer alignments the device requires, each at least one. @@ -291,6 +296,7 @@ impl DecoderCommon { view_formats, use_layered_dpb, dpb_usage, + film_grain: needs.film_grain, std_header_version: caps.std_header_version, bitstream_offset_alignment: caps.min_bitstream_buffer_offset_alignment.max(1), bitstream_size_alignment: caps.min_bitstream_buffer_size_alignment.max(1), diff --git a/tests/decode_streams.rs b/tests/decode_streams.rs index 2bd46be4..e51feefb 100644 --- a/tests/decode_streams.rs +++ b/tests/decode_streams.rs @@ -99,4 +99,6 @@ stream_tests! { av1_superres: Codec::AV1, "av1/aom_superres.obu", "nv12"; av1_resize: Codec::AV1, "av1/aom_resize.obu", "nv12"; av1_10bit: Codec::AV1, "av1/aom_10bit.obu", "p010le"; + av1_grain: Codec::AV1, "av1/aom_grain.obu", "nv12"; + av1_svt_grain: Codec::AV1, "av1/svt_grain.obu", "nv12"; } From 18f04919b3fd6da310c7fe64df9ddfd1986b2a0f Mon Sep 17 00:00:00 2001 From: Hans Gaiser Date: Thu, 5 Nov 2026 13:02:30 +0100 Subject: [PATCH 17/17] docs: describe H.265 and AV1 decoding --- AGENTS.md | 30 ++++++++++++++++++++++++++++-- CHANGELOG.md | 5 +++++ README.md | 11 ++++++----- src/decoder/mod.rs | 22 +++++++++++++++------- src/lib.rs | 11 ++++++----- 5 files changed, 60 insertions(+), 19 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index eefd553a..f2dab8d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -46,6 +46,30 @@ cargo run --example decode -- tests/data/bframes.264 out.yuv \ && cmp ref.yuv out.yuv && echo "decode matches ffmpeg" ``` +The `decode` example picks the codec from the extension (`.264`, `.265`, +`.obu`, `.ivf`). For AV1 the reference is dav1d, and `-noautoscale` keeps a +stream that changes size from being scaled to its first size (it applies to +any codec; H.264 and H.265 streams in `tests/data` do not change size): + +```bash +cargo run --example decode -- tests/data/av1/aom_altref.obu out.yuv \ + && ffmpeg -hide_banner -loglevel error -y -c:v libdav1d -i tests/data/av1/aom_altref.obu \ + -noautoscale -pix_fmt nv12 ref.yuv \ + && cmp ref.yuv out.yuv && echo "decode matches dav1d" +``` + +`tests/decode_streams.rs` does this for every fixture in `tests/data` (use +`p010le` for the 10-bit ones), and `tests/roundtrip.rs` for each codec's own +encoder output. A run with `PIXELFORGE_NO_UNIFIED_LAYOUTS=1` set takes the +copying path below. + +On NVIDIA (615.78.08) the AV1 decoder does not copy the `StdVideoAV1SequenceHeader` +handed to `vkCreateVideoSessionParametersKHR`: it reads it again at every +decode. The decoder keeps it alive for as long as the session parameters. Freed, +nothing fails and only the chroma of inter frames comes out slightly wrong, so +if AV1 inter frames ever differ from dav1d in chroma alone, look for a Std +structure that does not live long enough. + Decoding has two paths and both need checking. Where the device supports `VK_KHR_unified_image_layouts`, frames are the decoder's own images, handed over with no copy. Where it does not, they are copied into private images. Force the @@ -57,7 +81,8 @@ PIXELFORGE_NO_UNIFIED_LAYOUTS=1 cargo run --example decode -- \ ``` Which path a run took is in the debug log (`RUST_LOG=debug`), on the -`H.264 decode session:` line as `pinnable=`. +` decode session:` line (for example `H.264 decode session:`) as +`pinnable=`. The render path has its own example. `sample_frame` reads decoded frames in a compute shader through a sampler-YCbCr conversion, which is the path that never @@ -199,7 +224,8 @@ view by image handle, and only RADV without validation showed it. - `benches/` - The encode timing/throughput benchmark (`cargo bench`) - `testdata/` - Test input files (note: `test_frames.yuv` is a git-LFS pointer; generate frames locally if LFS content is unavailable) -- `tests/data/` - Small H.264 streams used by the decoder tests +- `tests/data/` - Small H.264, H.265 and AV1 (`tests/data/av1/`) streams used + by the decoder tests ## Key Dependencies - Vulkan Video API diff --git a/CHANGELOG.md b/CHANGELOG.md index 15928b3d..31d2b6dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add H.265 decoding: `DecodeConfig::new(Codec::H265)`. Main and Main 10, open + and closed GOPs, joining a stream at a CRA, scaling lists, temporal layers. +- Add AV1 decoding: `DecodeConfig::new(Codec::AV1)`, for low-overhead OBU + streams. Hidden frames and `show_existing_frame`, tiles, superres, frame + size changes, 10-bit and film grain. - Add `EncodeConfig::new(codec, width, height)`, for a codec chosen at runtime. - Add RGB input: `EncodeConfig::with_source(Source::Rgb(..))`. The encoder diff --git a/README.md b/README.md index b0c5e5fe..ad33025c 100644 --- a/README.md +++ b/README.md @@ -5,12 +5,12 @@ [![Documentation](https://docs.rs/pixelforge/badge.svg)](https://docs.rs/pixelforge) A Vulkan-based video encoding and decoding library for Rust, supporting H.264, -H.265 and AV1 encode, and H.264 decode. +H.265 and AV1, for both encode and decode. ## Features - **Hardware-accelerated** video encoding and decoding using Vulkan Video extensions. -- **Multiple codec support**: H.264/AVC, H.265/HEVC, AV1 encode; H.264 decode. +- **Multiple codec support**: H.264/AVC, H.265/HEVC and AV1, encode and decode. - **Asynchronous pipelines**: both directions submit without waiting. Encoding hands back an [`EncodeFuture`]; decoding delivers frames through a [`DecodeSource`] as the GPU finishes with them. @@ -37,11 +37,12 @@ H.265 and AV1 encode, and H.264 decode. | Codec | Encode | Decode | |-------|--------|--------| | H.264/AVC | ✓ | ✓ | -| H.265/HEVC | ✓ | | -| AV1 | ✓ | | +| H.265/HEVC | ✓ | ✓ | +| AV1 | ✓ | ✓ | H.264 decoding is verified byte-identical to `ffmpeg -pix_fmt nv12` on AMD -(RADV), NVIDIA and Intel (ANV). +(RADV), NVIDIA and Intel (ANV). H.265 decoding is verified byte-identical to +ffmpeg, and AV1 decoding (film grain included) to dav1d, on NVIDIA so far. ## Requirements diff --git a/src/decoder/mod.rs b/src/decoder/mod.rs index 64daca3c..4129d536 100644 --- a/src/decoder/mod.rs +++ b/src/decoder/mod.rs @@ -29,14 +29,22 @@ //! image is valid for exactly as long as the frame is; see [`DecodedFrame`] //! for what holding one costs. //! -//! # Limitations (H.264) +//! # Limitations //! -//! - Progressive streams only (no interlaced/field coding). -//! - Streams using explicit scaling matrices are rejected. -//! -//! Reference management is complete (IDR, sliding-window, and MMCO), so any -//! stream this crate's encoder produces — and typical output from x264, NVENC -//! and friends, including B-pyramid — decodes correctly. +//! - **H.264**: progressive streams only (no interlaced/field coding), and no +//! explicit scaling matrices. Reference management is complete (IDR, +//! sliding-window, and MMCO), so any stream this crate's encoder produces, +//! and typical output from x264, NVENC and friends, including B-pyramid, +//! decodes correctly. +//! - **H.265**: the base layer only; Main and Main 10 (and 4:2:2 or 4:4:4 where +//! the device has the profile), progressive. The screen content and +//! multi-layer extensions are rejected. RASL pictures after the CRA that +//! decoding starts at are skipped, as the standard allows. +//! - **AV1**: low-overhead OBU streams (the format of `.obu` files, IVF and +//! MP4), not Annex B; operating point 0. Monochrome streams and 12-bit are +//! rejected. Film grain is applied, which takes a second image for every DPB +//! slot. A device whose DPB and output images do not coincide is not +//! supported. /// Annex B start-code framing, shared by H.264 and H.265. pub(crate) mod annexb; diff --git a/src/lib.rs b/src/lib.rs index e37b436a..ec1f8a80 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,10 @@ //! A Vulkan-based video encoding and decoding library for Rust, supporting H.264, -//! H.265 and AV1 encode, and H.264 decode. +//! H.265 and AV1, for both encode and decode. //! //! # Features //! //! - **Hardware-accelerated** video encoding and decoding using Vulkan Video extensions. -//! - **Multiple codec support**: H.264/AVC, H.265/HEVC, AV1 encode; H.264 decode. +//! - **Multiple codec support**: H.264/AVC, H.265/HEVC and AV1, encode and decode. //! - **Asynchronous pipelines**: both directions submit without waiting. //! Encoding hands back an [`EncodeFuture`]; decoding delivers frames through a //! [`DecodeSource`] as the GPU finishes with them. @@ -31,11 +31,12 @@ //! | Codec | Encode | Decode | //! |-------|--------|--------| //! | H.264/AVC | ✓ | ✓ | -//! | H.265/HEVC | ✓ | | -//! | AV1 | ✓ | | +//! | H.265/HEVC | ✓ | ✓ | +//! | AV1 | ✓ | ✓ | //! //! H.264 decoding is verified byte-identical to `ffmpeg -pix_fmt nv12` on AMD -//! (RADV), NVIDIA and Intel (ANV). +//! (RADV), NVIDIA and Intel (ANV). H.265 decoding is verified byte-identical to +//! ffmpeg, and AV1 decoding (film grain included) to dav1d, on NVIDIA so far. //! //! # Requirements //!