diff --git a/crates/pf-bitstream/src/av1.rs b/crates/pf-bitstream/src/av1.rs index 2cde14fa..2a59d881 100644 --- a/crates/pf-bitstream/src/av1.rs +++ b/crates/pf-bitstream/src/av1.rs @@ -80,14 +80,22 @@ pub struct RefPic { /// What one picture's own frame header said, kept for as long as that picture can /// serve as a reference. /// -/// Every backend has a per-REFERENCE structure — Vulkan's -/// `StdVideoDecodeAV1ReferenceInfo`, DXVA's `DXVA_PicEntry_AV1`, libva's -/// `VAReferenceFrameAV1` — and each of them asks questions about the reference -/// picture, not about the frame being decoded. Answering them from the CURRENT -/// header is the shape of a whole bug class: it compiles, it looks like the fields -/// are filled, and the hardware predicts from a picture it has been told the wrong -/// things about. So the answers are recorded once, where they are unambiguous — -/// when the picture is STORED into its slots — and travel on the slot. +/// Two of the three backends have a per-REFERENCE structure — Vulkan's +/// `StdVideoDecodeAV1ReferenceInfo` and DXVA's `DXVA_PicEntry_AV1` — and each of them +/// asks questions about the reference picture, not about the frame being decoded. +/// Answering them from the CURRENT header is the shape of a whole bug class: it +/// compiles, it looks like the fields are filled, and the hardware predicts from a +/// picture it has been told the wrong things about. So the answers are recorded once, +/// where they are unambiguous — when the picture is STORED into its slots — and travel +/// on the slot. +/// +/// ⚠ **VA-API has no such structure at all.** An earlier revision of this comment +/// named a `VAReferenceFrameAV1`; libva 2.23.0 does not declare one (measured, `grep +/// -c` is 0). Its `ref_frame_map` is a bare array of `VASurfaceID`, and a driver reads +/// every per-reference answer off the surface — which is why the same revision's claim +/// about [`Self::upscaled_width`] below was wrong too, and why an invented type name +/// is worth correcting rather than leaving as harmless prose: it is what sent somebody +/// looking for a field to fill. /// /// [`Av1Planner::refresh_slots`] is the only writer, and [`RefState::of`] the only /// way to build one. @@ -101,10 +109,15 @@ pub struct RefState { /// AV1 lets every frame pick its own size up to the sequence maximum without a /// key frame, and a decoder predicting from a differently-sized reference /// SCALES the motion (7.11.3.3 derives `xStep` from `RefUpscaledWidth[refIdx]`). - /// So the per-reference structures ask for it: DXVA's `DXVA_PicEntry_AV1` has - /// `width`/`height` fields, VA-API's `VADecPictureParameterBufferAV1` has - /// `ref_frame_width`/`height`. Answering from the CURRENT header makes every + /// So **DXVA** asks for it per reference: `DXVA_PicEntry_AV1` has `width` and + /// `height` fields, and answering them from the CURRENT header makes every /// scaled prediction read as unscaled. + /// + /// ⚠ **VA-API does not.** `VADecPictureParameterBufferAV1` carries no + /// `ref_frame_width`/`ref_frame_height` at all (measured against libva 2.23.0's + /// `va_dec_av1.h`: `grep -c ref_frame_width` is 0), because its `ref_frame_map` + /// holds `VASurfaceID`s and a driver reads each reference's dimensions off the + /// surface. An earlier revision of this comment claimed otherwise. pub upscaled_width: u32, /// The picture's own `FrameHeight`, on the same terms as /// [`Self::upscaled_width`]. (There is no superres in the vertical direction, diff --git a/crates/pf-client-core/src/video.rs b/crates/pf-client-core/src/video.rs index bc7f7b20..c3235489 100644 --- a/crates/pf-client-core/src/video.rs +++ b/crates/pf-client-core/src/video.rs @@ -897,6 +897,10 @@ fn native_vaapi_codec(codec_id: ffmpeg::codec::Id) -> Option { match codec_id { ffmpeg::codec::Id::H264 => Some(pf_vaadec::Codec::H264), ffmpeg::codec::Id::HEVC => Some(pf_vaadec::Codec::H265), + // AV1 (M7). Not a widening of what this client can decode — the FFmpeg VAAPI + // rung already decodes AV1 Profile 0 through the same libva profile — but the + // native rung has to cover it, or dropping FFmpeg would drop a codec. + ffmpeg::codec::Id::AV1 => Some(pf_vaadec::Codec::Av1), _ => None, } } @@ -1329,8 +1333,8 @@ impl Decoder { } None => tracing::warn!( ?codec_id, - "PUNKTFUNK_DECODER=native-vaapi refused (needs an H.264 or HEVC \ - session) — standard ladder" + "PUNKTFUNK_DECODER=native-vaapi refused (needs an H.264, HEVC or \ + AV1 session) — standard ladder" ), } choice = "auto".to_string(); diff --git a/crates/pf-client-core/src/video_vaapi_native.rs b/crates/pf-client-core/src/video_vaapi_native.rs index 9118aeb6..bfcd544e 100644 --- a/crates/pf-client-core/src/video_vaapi_native.rs +++ b/crates/pf-client-core/src/video_vaapi_native.rs @@ -467,24 +467,32 @@ impl Display { /// `vaCreateBuffer` with the data copied in — libva's documented behaviour for a /// non-null `data` pointer, and what makes the caller's structs free to die /// straight after. + /// + /// `size` is ONE element's size and `count` is how many follow, because that is + /// how `vaCreateBuffer` is declared and the two are not interchangeable. Every + /// H.264 and H.265 buffer here passes `count = 1`; **AV1's tile-parameter buffer + /// is the one exception** — libavcodec's `vaapi_av1.c` sends a whole tile group's + /// records in a single buffer beside that group's one data buffer, and a driver + /// reads `num_elements` records out of it. fn create_buffer( &self, context: VaContextId, kind: u32, size: usize, + count: usize, data: *const c_void, ) -> Result { let mut id: VaBufferId = VA_INVALID_ID; - // SAFETY: a live display and context; `data` points at `size` readable bytes - // for the duration of the call (the caller's live struct or slice), and `id` - // is a local written through. libva copies the payload before returning. + // SAFETY: a live display and context; `data` points at `size * count` readable + // bytes for the duration of the call (the caller's live struct or slice), and + // `id` is a local written through. libva copies the payload before returning. self.va.check("vaCreateBuffer", unsafe { (self.va.create_buffer)( self.display, context, kind as c_uint, size as c_uint, - 1, + count as c_uint, data.cast_mut(), &mut id, ) @@ -547,6 +555,7 @@ struct StreamShape { enum Planner { H264(Box), H265(Box), + Av1(Box), } impl Planner { @@ -554,6 +563,7 @@ impl Planner { match self { Planner::H264(_) => "native-vaapi h264", Planner::H265(_) => "native-vaapi h265", + Planner::Av1(_) => "native-vaapi av1", } } } @@ -885,6 +895,7 @@ impl NativeVaapiDecoder { let planner = match codec { pf_vaadec::Codec::H264 => Planner::H264(Box::new(pf_vaadec::H264Planner::new())), pf_vaadec::Codec::H265 => Planner::H265(Box::new(pf_vaadec::H265Planner::new())), + pf_vaadec::Codec::Av1 => Planner::Av1(Box::new(pf_vaadec::Av1Planner::new())), }; let (release_tx, release_rx) = mpsc::channel(); Ok(NativeVaapiDecoder { @@ -949,10 +960,12 @@ impl NativeVaapiDecoder { /// for a keyframe it has no reason to send. pub(crate) fn decode(&mut self, au: &[u8]) -> Result> { self.drain_releases(); - let result = if matches!(self.planner, Planner::H264(_)) { - self.decode_h264(au) - } else { - self.decode_h265(au) + let result = match self.planner { + Planner::H264(_) => self.decode_h264(au), + Planner::H265(_) => self.decode_h265(au), + // ⚠ An AV1 "access unit" is a TEMPORAL UNIT and may carry several + // frames; this arm is the only one whose planner returns a `Vec`. + Planner::Av1(_) => self.decode_av1(au), }; // ONE verdict per access unit, folded here and nowhere else. Damage is // reported by the codec arm rather than counted inside it, so a failure @@ -969,7 +982,7 @@ impl NativeVaapiDecoder { fn decode_h264(&mut self, au: &[u8]) -> Result<(Option, bool)> { let plan = match &mut self.planner { Planner::H264(p) => p.plan_au(au).map_err(|e| anyhow!("{e:?}"))?, - Planner::H265(_) => unreachable!("dispatched on the planner's own arm"), + _ => unreachable!("dispatched on the planner's own arm"), }; let shape = shape_of( plan.picture.coded_width, @@ -1002,21 +1015,21 @@ impl NativeVaapiDecoder { let converted = pf_vaadec::plan_to_va(&plan, au, &mut s.slots, &table, target) .map_err(|e| anyhow!("{e}"))?; - bind_setup(s, plan.dpb.stored, free); + bind_setup(s, plan.dpb.stored, Some(free)); let iq = Some(as_ptr(&converted.iq_matrix)); - let slice_ptrs: Vec<(*const c_void, usize)> = converted.slices.iter().map(as_ptr).collect(); + let slices = one_record_each(&converted.slices, &converted.slice_data)?; submit( display, s, target, as_ptr(&converted.pic_params), iq, - &slice_ptrs, - &converted.slice_data, + &slices, au, )?; + let display_size = (s.shape.display_width, s.shape.display_height); let frame = finish( display, s, @@ -1025,6 +1038,7 @@ impl NativeVaapiDecoder { damaged, plan.picture.is_idr, colour_of(&plan.picture.colour), + display_size, &mut self.recovery_request, &self.release_tx, )?; @@ -1041,7 +1055,7 @@ impl NativeVaapiDecoder { Err(pf_vaadec::PlanErrorH265::RaslSkipped { .. }) => return Ok((None, false)), Err(e) => return Err(anyhow!("{e:?}")), }, - Planner::H264(_) => unreachable!("dispatched on the planner's own arm"), + _ => unreachable!("dispatched on the planner's own arm"), }; let shape = shape_of( plan.picture.coded_width, @@ -1077,7 +1091,7 @@ impl NativeVaapiDecoder { let converted = pf_vaadec::plan_to_va_h265(&plan, au, &mut s.slots, &table, target) .map_err(|e| anyhow!("{e}"))?; - bind_setup(s, plan.dpb.stored, free); + bind_setup(s, plan.dpb.stored, Some(free)); // The IQ matrix is submitted ONLY where the sequence codes scaling lists. // Handing the driver an all-zero matrix on a "use the defaults" stream is @@ -1086,18 +1100,18 @@ impl NativeVaapiDecoder { // (M5's review caught exactly this on the DXVA rung, where the buffer was // unconditional. The conversion answers `None` here so the rung cannot.) let iq = converted.iq_matrix.as_ref().map(as_ptr); - let slice_ptrs: Vec<(*const c_void, usize)> = converted.slices.iter().map(as_ptr).collect(); + let slices = one_record_each(&converted.slices, &converted.slice_data)?; submit( display, s, target, as_ptr(&converted.pic_params), iq, - &slice_ptrs, - &converted.slice_data, + &slices, au, )?; + let display_size = (s.shape.display_width, s.shape.display_height); let frame = finish( display, s, @@ -1106,11 +1120,309 @@ impl NativeVaapiDecoder { damaged, plan.picture.is_idr, colour_of(&plan.picture.colour), + display_size, &mut self.recovery_request, &self.release_tx, )?; Ok((frame, damaged)) } + + /// One AV1 **temporal unit**: decode every frame in it, present at most one. + /// + /// This is the whole of what AV1 adds to this rung's contract, and it is the + /// SPEC's shape rather than an assumption about punktfunk hosts. A temporal unit + /// may carry several frame headers; the vendored 250-packet conformance vector + /// decodes **274 frames** and shows 250, so 24 of its units carry a hidden + /// picture (an alt-ref later frames predict from) ahead of the one that + /// displays. Those hidden frames must be DECODED — they are references — and + /// must never reach the presenter, which would show each of them for a frame and + /// stutter every time. + /// + /// AV1 admits at most one shown frame per temporal unit, so "the last shown + /// frame wins" cannot silently drop a picture. + /// + /// # Concealment is per UNIT here, per picture on the other two codecs + /// + /// A damaged frame is still CONVERTED and still SUBMITTED, exactly as the H.264 + /// and H.265 arms above do it. Converting is what assigns its ledger slot, and + /// skipping that would desynchronise this rung's slot map from the planner's store + /// and turn every later reference to it into a hard `Err` — a demotion streak + /// earned by one lost packet. Submitting is what puts a decoded picture in the + /// surface, which matters because a hidden frame's surface is a REFERENCE for + /// later frames and can still be exported by a later `show_existing_frame`; a + /// surface the driver never wrote is uninitialised video memory, not a stale + /// picture. + /// + /// What concealment does instead is withhold the DISPLAY: nothing from the unit + /// is presented, because a shown frame that predicts from a concealed reference in + /// the same unit is not fit to display either, and the unit is the smallest thing + /// this rung can honestly drop. + /// + /// ⚠ Submitting a frame whose references were lost needs one thing from the + /// conversion, and `va_dec_av1.h:352` is where it comes from: *"Driver is not + /// responsible to validate reference frames' id … If missing frame is identified, + /// application may choose to perform error recovery by pointing problematic index + /// to an alternative frame buffer."* So `plan_to_va_av1` points every empty + /// `ref_frame_map` entry at a live surface and reports which + /// (`DecodePlanVaAv1::substituted_refs`); no `VA_INVALID_ID` reaches a driver that + /// says it will not check. + /// + /// The one frame that is NOT submitted is the one with nothing to submit: an + /// access unit whose tile groups were lost. That refusal is handled in + /// [`Self::frame_av1`] and binds no surface at all, so its picture can be neither + /// exported nor predicted from. + fn decode_av1(&mut self, au: &[u8]) -> Result<(Option, bool)> { + let plans = match &mut self.planner { + Planner::Av1(p) => p.plan_au(au).map_err(|e| anyhow!("{e}"))?, + _ => unreachable!("dispatched on the planner's own arm"), + }; + let mut shown = None; + let mut damaged_unit = false; + for plan in &plans { + let damaged = plan + .warnings + .iter() + .any(pf_vaadec::is_integrity_warning_av1); + damaged_unit |= damaged; + if !plan.warnings.is_empty() { + tracing::debug!(warnings = ?plan.warnings, damaged, "native VAAPI AV1 plan warnings"); + } + if let Some(frame) = self.frame_av1(au, plan, damaged)? { + shown = Some(frame); + } + } + if damaged_unit { + // A frame may already have been exported before a LATER frame of the + // same unit turned out to be damaged. Dropping it here is safe rather + // than merely tolerable: `DmabufFrame`'s guard closes its fds and returns + // the surface to the free list, which is exactly what an unshown picture + // should do. + drop(shown); + return Ok((None, true)); + } + Ok((shown, false)) + } + + /// One frame of a temporal unit: converted, submitted, and exported only if it is + /// the frame the unit displays and the unit is clean. + /// + /// `damaged` changes two things and neither of them is the submission. It decides + /// whether the picture may be SHOWN (through [`finish`]), and it decides how a + /// conversion refusal is answered: a lost tile group on an already-damaged plan is + /// concealed, the same refusal on a plan that arrived whole is a defect and stays + /// an error. + fn frame_av1( + &mut self, + au: &[u8], + plan: &pf_vaadec::AuPlanAv1, + damaged: bool, + ) -> Result> { + // `show_existing_frame` decodes nothing at all: it re-displays a picture some + // earlier hidden frame put in a reference slot. + if plan.dpb.stored.is_none() { + return self.show_existing_av1(plan, damaged); + } + let shape = shape_of_av1(plan); + let Self { + display, session, .. + } = self; + let s = ensure_session( + display, + session, + pf_vaadec::Codec::Av1, + shape, + &mut self.generation, + )?; + let free = s + .free_surface() + .ok_or_else(|| anyhow!("surface pool exhausted ({} surfaces)", s.surfaces.len()))?; + let target = s.surfaces[free]; + let table = s.surface_table(); + let converted = match pf_vaadec::plan_to_va_av1(plan, au, &mut s.slots, &table, target) { + Ok(converted) => converted, + Err(e) => { + // ⚠ The ledger has already been mutated — the conversion assigns the + // setup slot before its tile walk, so that a refusal here does not + // desynchronise it from the planner's store — and the caller's half of + // that contract is to bind NOTHING (see [`bind_setup`]). Unconditional, + // because it is also correct for the refusals that fire before any + // mutation: there is no slot to clear and no surface to bind either + // way. + bind_setup(s, plan.dpb.stored, None); + // A lost tile group on a plan the planner ALREADY called damaged is + // concealment, not a defect: the access unit simply did not carry the + // tiles its frame header announced, which is what one dropped packet + // looks like. Answering with an error instead would burn the demotion + // streak on exactly the lossy links this rung exists to diagnose. The + // frame is not submitted (there is nothing to submit), its surface is + // bound to nothing, and the unit is dropped by the caller. + if damaged && e.lost_tiles() { + tracing::debug!( + error = %e, + id = plan.dpb.stored, + "native VAAPI AV1: concealed a truncated access unit" + ); + finish( + display, + s, + &plan.dpb.outputs, + &plan.dpb.removed, + true, + plan.picture.is_key, + colour_of(&plan.picture.colour), + // Unread — `finish` returns before it looks at the display + // region when `damaged` — but written the same way as the + // submitting path below, so the two cannot drift apart. + ( + plan.picture.render_width.min(plan.picture.upscaled_width), + plan.picture.render_height.min(plan.picture.frame_height), + ), + &mut self.recovery_request, + &self.release_tx, + )?; + return Ok(None); + } + return Err(anyhow!("{e}")); + } + }; + + // `bind_setup` asks the LEDGER where the picture landed rather than being + // told, which is what makes it right for the AV1 frame that refreshes no + // slot: the conversion has already handed that slot back, so nothing binds + // the surface and only the pending-output claim keeps it out of the free + // list. (`DecodePlanVaAv1::setup_slot` is `None` there; it is not consulted + // here for exactly that reason.) + bind_setup(s, plan.dpb.stored, Some(free)); + + if converted.substituted_refs != 0 { + tracing::debug!( + slots = format_args!("{:#010b}", converted.substituted_refs), + "native VAAPI AV1: concealed reference slot(s) with a live surface" + ); + } + let mut slices: Vec = Vec::with_capacity(converted.tile_groups.len()); + for group in &converted.tile_groups { + slices.push(SlicePair { + params: group.tiles.as_ptr().cast::(), + record_size: size_of::(), + // ⚠ Several records in ONE buffer — the only place this rung does + // that, and what libavcodec's `vaapi_av1.c` does per tile group. + records: group.tiles.len(), + data: group.data.clone(), + }); + } + submit( + display, + s, + target, + as_ptr(&converted.pic_params), + // AV1 transmits no quantisation matrix: its matrices are SELECTED by + // index out of tables the decoder already holds. + None, + &slices, + au, + )?; + + // AV1's display region is the RENDER size, not the coded size — and it is a + // per-FRAME value, so it cannot live in the session shape the way a + // conformance window does. + // + // ⚠ CLAMPED to the decoded picture. AV1 5.9.6 puts no upper bound on the + // render size — a stream may legally ask to be shown at more than it coded — + // and an unclamped crop would hand the presenter a region larger than the + // surface. The same clamp is in the Vulkan and D3D11 rungs. + // + // ⚠ Treated as a CROP, which is what both other native rungs do. libavcodec + // instead keeps the frame at `upscaled_width` x `frame_height` and expresses + // the render size as a sample aspect RATIO, so on a stream where the two + // differ this rung shows less picture than the FFmpeg rung would. No + // punktfunk host emits such a stream; the choice is here so the three native + // rungs answer alike, not because it is settled. + let display_size = ( + plan.picture.render_width.min(plan.picture.upscaled_width), + plan.picture.render_height.min(plan.picture.frame_height), + ); + let frame = finish( + display, + s, + &plan.dpb.outputs, + &plan.dpb.removed, + damaged, + plan.picture.is_key, + colour_of(&plan.picture.colour), + display_size, + &mut self.recovery_request, + &self.release_tx, + )?; + + // A frame that enters no reference slot AND displays nothing is dead the + // moment it is decoded, and it is the one picture nothing else ever retires: + // the planner cannot report it removed (it was never stored) and cannot + // output it, so its pending entry — and therefore its surface — is held for + // the session's whole life. Seventeen of them exhaust the pool. + // + // Legal AV1 syntax that no encoder emits, which is exactly why it is worth a + // line: a damaged or truncated header can parse to it, and the symptom would + // be a session that dies of "pool exhausted" some minutes later with nothing + // pointing back here. + if converted.setup_slot.is_none() && !plan.dpb.outputs.contains(&converted.setup_id) { + s.pending.retain(|(id, _)| *id != converted.setup_id); + } + Ok(frame) + } + + /// A `show_existing_frame` access unit: export a surface the pool already holds. + /// + /// No conversion and no submission — the picture was decoded by an earlier frame + /// of an earlier temporal unit and is still in [`Session::pending`], because a + /// hidden frame is never output when it decodes. [`finish`] resolves the output + /// id to its surface exactly as it does for any other picture, so this path needs + /// no per-surface facts table: the plan's own `picture` carries the SHOWN frame's + /// geometry and type, which the vendored parser restores from the reference + /// (`load_reference_frame` copies `ref_upscaled_width` / `ref_frame_height` / + /// `ref_render_*` / `ref_frame_type` into the display-only header). + /// + /// ⚠ **Untested.** The vendored conformance vector uses `show_existing_frame` + /// zero times — pf-bitstream's planner test asserts that count stays 0 — so + /// nothing in any gate reaches this function. + fn show_existing_av1( + &mut self, + plan: &pf_vaadec::AuPlanAv1, + damaged: bool, + ) -> Result> { + let Self { + display, session, .. + } = self; + // Nothing has decoded yet: the unit is already concealed (the planner + // reported `MissingShowExisting`) and there is no session to look in. + let Some(s) = session.as_mut() else { + return Ok(None); + }; + // Showing a KEY frame this way resets the whole reference store (AV1 7.20), + // so the plan's removals are real and this rung's ledger has to follow them — + // or the map fills up and the next assignment fails. No conversion runs on + // this path, so this is the only place they can be applied. + for &id in &plan.dpb.removed { + s.slots.release(id); + } + s.sync_slot_bindings(); + let display_size = ( + plan.picture.render_width.min(plan.picture.upscaled_width), + plan.picture.render_height.min(plan.picture.frame_height), + ); + finish( + display, + s, + &plan.dpb.outputs, + &plan.dpb.removed, + damaged, + plan.picture.is_key, + colour_of(&plan.picture.colour), + display_size, + &mut self.recovery_request, + &self.release_tx, + ) + } } impl Drop for NativeVaapiDecoder { @@ -1217,6 +1529,39 @@ fn shape_of( }) } +/// The session shape one AV1 plan implies. +/// +/// ⚠ The pool is sized from the SEQUENCE header's **maximum** frame size, not this +/// frame's. AV1 lets every frame pick its own size up to that maximum without a key +/// frame, and sizing the session from the frame would rebuild the config, the +/// surface pool and the ledger — dropping every reference — the first time a stream +/// resized downward. libavcodec does the same (`set_context_with_sequence` calls +/// `ff_set_dimensions(avctx, seq->max_frame_width_minus_1 + 1, …)`). +/// +/// The display fields carry the same maximum rather than the render region, for the +/// same reason: the render size is a per-FRAME value and putting it here would make +/// every render-size change a renegotiation. What actually reaches the presenter is +/// [`finish`]'s `display` parameter. +/// +/// The DPB depth is a constant of the codec — `NUM_REF_FRAMES` — never anything a +/// sequence header says. There is no conformance window to refuse, so unlike +/// [`shape_of`] this cannot fail. +fn shape_of_av1(plan: &pf_vaadec::AuPlanAv1) -> StreamShape { + let coded_width = u32::from(plan.sequence.max_frame_width_minus_1) + 1; + let coded_height = u32::from(plan.sequence.max_frame_height_minus_1) + 1; + StreamShape { + coded_width, + coded_height, + display_width: coded_width, + display_height: coded_height, + max_dpb_frames: pf_vaadec::AV1_MAX_DPB_FRAMES, + chroma_format_idc: plan.picture.chroma_format_idc, + // AV1 codes ONE bit depth for all three planes, so there is no luma/chroma + // pair to reconcile the way H.264 and H.265 need. + bit_depth: plan.picture.bit_depth, + } +} + /// The session for this shape, rebuilt whole if the stream renegotiated. fn ensure_session<'a>( d: &Display, @@ -1254,7 +1599,7 @@ fn ensure_session<'a>( Ok(slot.insert(built)) } -/// Record which surface holds the picture just planned. +/// Record which surface holds the picture just planned — or that NOTHING does. /// /// The slot bindings are re-derived from the ledger FIRST — the conversion has /// already applied this AU's removals, so a slot the planner released binds nothing @@ -1263,38 +1608,92 @@ fn ensure_session<'a>( /// picture with no free frame buffer is stored and evicted inside a single plan, so /// it holds NO slot when the conversion returns. Its surface is kept out of the free /// list by `pending` instead, until it has been output. -fn bind_setup(s: &mut Session, stored: Option, surface: usize) { +/// +/// ⚠ `surface` is `None` on the AV1 refusal path, and that call is not optional. The +/// AV1 conversion assigns the ledger slot BEFORE its tile walk — deliberately, so a +/// lost tile group does not desynchronise the ledger from the planner's store forever +/// (`pf_vaadec::plan_to_va_av1`'s docs) — which means a refusal can leave a slot +/// re-assigned to a picture that never decoded while `slot_surface` still holds the +/// surface of whatever occupied that slot BEFORE. That is not a missing reference, it +/// is a WRONG one, and nothing downstream could tell. Binding `None` makes the slot +/// read back as `VA_INVALID_ID`, which the conversion then substitutes with a live +/// surface. Nothing is pushed to `pending` either: an undecoded surface must never be +/// exportable. +fn bind_setup(s: &mut Session, stored: Option, surface: Option) { s.sync_slot_bindings(); - if let Some(id) = stored { - if let Some(slot) = s.slots.slot_of(id) { - s.slot_surface[usize::from(slot)] = Some(surface); - } + let Some(id) = stored else { return }; + if let Some(slot) = s.slots.slot_of(id) { + s.slot_surface[usize::from(slot)] = surface; + } + if let Some(surface) = surface { s.pending.push((id, surface)); } } +/// One slice-parameter (AV1: tile-parameter) buffer and the bitstream region its +/// records address. +/// +/// The two travel together because `vaRenderPicture` is what establishes which data +/// buffer a record's `slice_data_offset` is relative to — it is handed the pair. +struct SlicePair { + /// The record array. Borrowed from the caller's converted plan, which outlives + /// the `submit` call that reads it. + params: *const c_void, + /// ONE record's size. `vaCreateBuffer` takes the element size and the element + /// count separately and they are not interchangeable. + record_size: usize, + /// How many records this buffer carries: **1** for H.264 and H.265 — one slice, + /// one buffer, exactly as libavcodec sends them — and a whole tile group's worth + /// for AV1, which is the one codec libavcodec packs several records into a single + /// buffer for. + records: usize, + /// The bitstream those records address, in ACCESS-UNIT coordinates. + data: std::ops::Range, +} + +/// The H.264/H.265 shape of the above: one record per buffer, parallel to its data +/// range. +/// +/// ⚠ The length check is not ceremony, even though both conversions build the two +/// vectors in one loop today and cannot produce a mismatch. `zip` would answer a +/// future divergence by SILENTLY TRUNCATING — a picture submitted with some of its +/// slices, which decodes to a partial frame rather than to an error, and which no +/// gate here has hardware to catch. A refusal is the honest answer and costs one +/// comparison per access unit. +fn one_record_each(records: &[T], data: &[std::ops::Range]) -> Result> { + if records.len() != data.len() { + bail!( + "{} slice record(s) for {} data range(s) — the conversion's two halves \ + disagree", + records.len(), + data.len() + ); + } + Ok(records + .iter() + .zip(data) + .map(|(record, range)| SlicePair { + params: (record as *const T).cast::(), + record_size: size_of::(), + records: 1, + data: range.clone(), + }) + .collect()) +} + /// One picture's buffers, in the order libavcodec's VAAPI path submits them: the /// parameter buffers in one `vaRenderPicture`, then the interleaved /// slice-parameter/slice-data pairs in another. Matching the path drivers are /// validated against is worth more than any tidier arrangement. -#[allow(clippy::too_many_arguments)] fn submit( d: &Display, s: &Session, target: VaSurfaceId, pic: (*const c_void, usize), iq: Option<(*const c_void, usize)>, - slices: &[(*const c_void, usize)], - slice_data: &[std::ops::Range], + slices: &[SlicePair], au: &[u8], ) -> Result<()> { - if slices.len() != slice_data.len() { - bail!( - "{} slice records for {} data ranges", - slices.len(), - slice_data.len() - ); - } let mut params: Vec = Vec::with_capacity(2); let mut slice_buffers: Vec = Vec::with_capacity(slices.len() * 2); // A picture that was BEGUN must be ended even if a step in between failed, or @@ -1310,6 +1709,7 @@ fn submit( s.context, pf_vaadec::va::VA_PICTURE_PARAMETER_BUFFER_TYPE, pic.1, + 1, pic.0, ) .context("picture parameter buffer")?, @@ -1320,24 +1720,30 @@ fn submit( s.context, pf_vaadec::va::VA_IQ_MATRIX_BUFFER_TYPE, size, + 1, ptr, ) .context("IQ matrix buffer")?, ); } - for (n, ((ptr, size), range)) in slices.iter().zip(slice_data).enumerate() { + for (n, pair) in slices.iter().enumerate() { + let range = pair.data.clone(); let data = au.get(range.clone()).ok_or_else(|| { anyhow!( "slice {n}: range {range:?} is outside a {}-byte access unit", au.len() ) })?; + if pair.records == 0 { + bail!("slice {n}: a parameter buffer with no records"); + } slice_buffers.push( d.create_buffer( s.context, pf_vaadec::va::VA_SLICE_PARAMETER_BUFFER_TYPE, - *size, - *ptr, + pair.record_size, + pair.records, + pair.params, ) .with_context(|| format!("slice {n} parameter buffer"))?, ); @@ -1346,6 +1752,7 @@ fn submit( s.context, pf_vaadec::va::VA_SLICE_DATA_BUFFER_TYPE, data.len(), + 1, data.as_ptr().cast::(), ) .with_context(|| format!("slice {n} data buffer"))?, @@ -1427,6 +1834,10 @@ fn finish( damaged: bool, keyframe: bool, color: ColorDesc, + // The DISPLAY region for this picture. A parameter rather than a read of + // `s.shape` because AV1's is per-FRAME: its render size may change without a key + // frame, so it cannot live in the shape that rebuilds the session. + display: (u32, u32), recovery_request: &mut bool, tx: &mpsc::Sender, ) -> Result> { @@ -1504,8 +1915,8 @@ fn finish( // The DISPLAY region. The surface is allocated at the coded size and is // taller/wider than the picture; handing over the coded size would show the // codec's granule padding. - width: s.shape.display_width, - height: s.shape.display_height, + width: display.0, + height: display.1, fourcc: exported.fourcc, modifier: exported.modifier, planes, @@ -1714,6 +2125,56 @@ mod tests { ); } + /// A picture the conversion REFUSED binds no surface — so nothing can show it and + /// nothing can predict from it. + /// + /// Both halves matter and they fail differently. The AV1 conversion assigns its + /// ledger slot before the tile walk, so a refusal leaves a slot re-assigned to a + /// picture that never decoded; leaving the slot's PREVIOUS binding in place would + /// hand the next frame a real, decoded, WRONG picture, which no later check could + /// notice. And a `pending` entry for it would let a later `show_existing_frame` + /// claim the surface and ship it — a surface the driver never wrote, which is + /// uninitialised video memory rather than a stale frame. + #[test] + fn a_refused_picture_binds_nothing_and_can_never_be_exported() { + let mut s = session(4, 3); + + // Picture 11 decoded into surface 0 and took slot 0. + s.slots.assign(11).expect("a free slot"); + bind_setup(&mut s, Some(11), Some(0)); + assert_eq!(s.slot_surface[0], Some(0)); + assert_eq!(s.surface_table()[0], s.surfaces[0]); + assert_eq!(s.pending, vec![(11, 0)]); + + // Picture 12's access unit lost its tile groups. The conversion released 11, + // handed 12 the slot it just gave back — the routine case, not a contrived one + // — and then refused. + s.slots.release(11); + assert_eq!(s.slots.assign(12).expect("the slot 11 gave back"), 0); + bind_setup(&mut s, Some(12), None); + + assert_eq!( + s.slot_surface[0], None, + "the slot must not keep picture 11's surface: picture 12 never decoded, \ + and a reference to 12 that reads 11 is a wrong picture, not a missing one" + ); + assert_eq!( + s.surface_table()[0], + VA_INVALID_ID, + "and the table the conversion reads must say so, so it can substitute" + ); + assert!( + !s.pending.iter().any(|(id, _)| *id == 12), + "an undecoded picture owes no output — a pending entry is what would let \ + a later show_existing_frame export a surface the driver never wrote" + ); + + // The slot is still LIVE in the ledger, which is the whole point of the + // conversion mutating before it refuses: the next frame resolves picture 12 + // rather than hard-erroring on it. + assert_eq!(s.slots.slot_of(12), Some(0)); + } + /// A conformance window with a non-zero ORIGIN is refused, not cropped from the /// wrong corner: nothing downstream carries an origin. #[test] @@ -1751,6 +2212,127 @@ mod tests { .is_err()); } + /// One synthetic AV1 plan: a sequence that permits `max` and a frame that codes + /// `frame`, so the two can be told apart. + fn av1_plan(max: (u16, u16), frame: (u32, u32), render: (u32, u32)) -> pf_vaadec::AuPlanAv1 { + let sequence = pf_vaadec::ParsedSequenceHeaderAv1 { + max_frame_width_minus_1: max.0 - 1, + max_frame_height_minus_1: max.1 - 1, + ..Default::default() + }; + pf_vaadec::AuPlanAv1 { + picture: pf_vaadec::PicturePlanAv1 { + frame_type: pf_vaadec::FrameTypeAv1::KeyFrame, + is_key: true, + show_frame: true, + showable_frame: false, + order_hint: 0, + upscaled_width: frame.0, + frame_width: frame.0, + frame_height: frame.1, + render_width: render.0, + render_height: render.1, + bit_depth: 8, + chroma_format_idc: 1, + colour: pf_vaadec::ColourDescription { + colour_primaries: 1, + transfer_characteristics: 1, + matrix_coefficients: 1, + video_full_range: false, + }, + }, + tiles: Vec::new(), + refs: [None; 7], + dpb: pf_vaadec::DpbUpdateAv1::default(), + dpb_refs: Vec::new(), + warnings: Vec::new(), + sequence: std::rc::Rc::new(sequence), + header: std::rc::Rc::new(pf_vaadec::ParsedFrameHeaderAv1::default()), + } + } + + /// An AV1 session is sized from the SEQUENCE, never from the frame — and its DPB + /// depth is the codec's constant. + /// + /// Both halves are the difference between a stream that survives a mid-GOP resize + /// and one that rebuilds its pool, drops every reference and conceals its way back + /// to a keyframe. AV1 permits a frame to code any size up to the sequence maximum + /// with no key frame in sight, so a shape derived from the frame changes when + /// nothing renegotiated. + #[test] + fn an_av1_session_is_sized_from_the_sequence_maximum_not_the_frame() { + let big = shape_of_av1(&av1_plan((1920, 1080), (1920, 1080), (1920, 1080))); + assert_eq!((big.coded_width, big.coded_height), (1920, 1080)); + assert_eq!( + big.max_dpb_frames, 8, + "NUM_REF_FRAMES, not a stream property" + ); + + // The same sequence, a frame coded smaller and shown smaller still. Neither + // may move the shape, or this is a renegotiation. + let small = shape_of_av1(&av1_plan((1920, 1080), (1280, 720), (960, 540))); + assert_eq!( + small, big, + "a frame that resized itself must not rebuild the session" + ); + + // A genuinely different sequence does move it. + let other = shape_of_av1(&av1_plan((1280, 720), (1280, 720), (1280, 720))); + assert_ne!(other, big); + } + + /// The render size reaches the presenter CLAMPED to the decoded picture. + /// + /// AV1 5.9.6 puts no upper bound on the render size, so a stream may legally ask + /// to be shown at more than it coded; handing that to the presenter as a crop + /// would address rows the surface does not have. This restates the clamp in + /// `frame_av1`, which cannot itself be reached without a device. + #[test] + fn an_oversized_render_region_is_clamped_to_the_decoded_picture() { + let plan = av1_plan((1920, 1080), (1280, 720), (4096, 4096)); + let display = ( + plan.picture.render_width.min(plan.picture.upscaled_width), + plan.picture.render_height.min(plan.picture.frame_height), + ); + assert_eq!(display, (1280, 720)); + + let ordinary = av1_plan((1920, 1080), (1920, 1088), (1920, 1080)); + let display = ( + ordinary + .picture + .render_width + .min(ordinary.picture.upscaled_width), + ordinary + .picture + .render_height + .min(ordinary.picture.frame_height), + ); + assert_eq!(display, (1920, 1080), "the ordinary crop still crops"); + } + + /// H.264 and H.265 send ONE record per parameter buffer; AV1 is the exception. + /// + /// `vaCreateBuffer` takes an element size and an element count, and getting the + /// pair backwards is a driver reading `size` records of `count` bytes. This pins + /// the side every codec but AV1 is on. + #[test] + fn a_slice_pair_carries_one_record_unless_av1_says_otherwise() { + let records = [7u32, 8, 9]; + let ranges = vec![0..4, 4..8, 8..12]; + let pairs = one_record_each(&records, &ranges).expect("parallel lengths"); + assert_eq!(pairs.len(), 3); + for pair in &pairs { + assert_eq!(pair.records, 1); + assert_eq!(pair.record_size, size_of::()); + } + assert_eq!(pairs[1].data, 4..8); + + // A record without its data range REFUSES. `zip` would drop it silently and + // submit a picture missing a slice, which decodes rather than fails. + assert!(one_record_each(&records, &ranges[..2]).is_err()); + assert!(one_record_each(&records[..1], &ranges).is_err()); + } + /// A shape this rung cannot decode is refused BEFORE libva is even loaded. /// /// The ordering is the point, not the refusal. M3 WP-2's review caught the diff --git a/crates/pf-vaadec/Cargo.toml b/crates/pf-vaadec/Cargo.toml index 3b487c26..a527fb07 100644 --- a/crates/pf-vaadec/Cargo.toml +++ b/crates/pf-vaadec/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pf-vaadec" -description = "Native VAAPI H.264/HEVC decode for the Linux clients (M6): the hand-declared libva decode buffer layouts plus the profile/format/surface decisions — the CPU-testable half; the libva plumbing lives in pf-client-core (design/client-native-decode.md §3.4)" +description = "Native VAAPI H.264/HEVC/AV1 decode for the Linux clients (M6, M7): the hand-declared libva decode buffer layouts plus the profile/format/surface decisions — the CPU-testable half; the libva plumbing lives in pf-client-core (design/client-native-decode.md §3.4)" version.workspace = true edition.workspace = true rust-version.workspace = true diff --git a/crates/pf-vaadec/layout-probe.c b/crates/pf-vaadec/layout-probe.c index 27357f75..c09b975a 100644 --- a/crates/pf-vaadec/layout-probe.c +++ b/crates/pf-vaadec/layout-probe.c @@ -1,7 +1,8 @@ /* - * Layout probe for the hand-declared libva structures in `src/va.rs`. + * Layout probe for the hand-declared libva structures in `src/va.rs`, + * `src/va_h265.rs` and `src/va_av1.rs`. * - * `src/va.rs` declares VAAPI's decode buffers as `#[repr(C)]` Rust structs, because + * Those modules declare VAAPI's decode buffers as `#[repr(C)]` Rust structs, because * this crate must build on macOS and in the Linux container where libva headers need * not exist. This file is how those declarations were CHECKED rather than eyeballed, * and it is committed so the check is reproducible instead of a claim in a commit @@ -24,6 +25,7 @@ #include #include #include +#include #include #define S(t) printf("size %-34s %zu align %zu\n", #t, sizeof(t), _Alignof(t)) @@ -221,6 +223,225 @@ int main(void) { printf("bits hevc LongSliceFlags.slice_loop_filter_across=1 -> 0x%08x\n", s.LongSliceFlags.value); } + /* ---- AV1 (va_dec_av1.h) ---- + * + * Three things make this codec's layout worth measuring rather than counting: + * a POINTER member (`anchor_frames_list`) that forces eight-byte alignment and + * therefore padding nothing in the field list suggests; two bit-field unions + * that are NOT 32 bits wide (`loop_filter_info_fields` is a uint8_t, + * `qmatrix_fields` and `loop_restoration_fields` are uint16_t), so a u32 `pack` + * would write over the neighbouring field; and three nested structs whose own + * VA_PADDING_LOW tails sit inside the picture-parameter buffer. + */ + S(VASegmentationStructAV1); + O(VASegmentationStructAV1, segment_info_fields); + O(VASegmentationStructAV1, feature_data); + O(VASegmentationStructAV1, feature_mask); + O(VASegmentationStructAV1, va_reserved); + + S(VAFilmGrainStructAV1); + O(VAFilmGrainStructAV1, film_grain_info_fields); + O(VAFilmGrainStructAV1, grain_seed); + O(VAFilmGrainStructAV1, num_y_points); + O(VAFilmGrainStructAV1, point_y_value); + O(VAFilmGrainStructAV1, point_y_scaling); + O(VAFilmGrainStructAV1, num_cb_points); + O(VAFilmGrainStructAV1, point_cb_value); + O(VAFilmGrainStructAV1, point_cb_scaling); + O(VAFilmGrainStructAV1, num_cr_points); + O(VAFilmGrainStructAV1, point_cr_value); + O(VAFilmGrainStructAV1, point_cr_scaling); + O(VAFilmGrainStructAV1, ar_coeffs_y); + O(VAFilmGrainStructAV1, ar_coeffs_cb); + O(VAFilmGrainStructAV1, ar_coeffs_cr); + O(VAFilmGrainStructAV1, cb_mult); + O(VAFilmGrainStructAV1, cb_luma_mult); + O(VAFilmGrainStructAV1, cb_offset); + O(VAFilmGrainStructAV1, cr_mult); + O(VAFilmGrainStructAV1, cr_luma_mult); + O(VAFilmGrainStructAV1, cr_offset); + O(VAFilmGrainStructAV1, va_reserved); + + S(VAWarpedMotionParamsAV1); + O(VAWarpedMotionParamsAV1, wmtype); + O(VAWarpedMotionParamsAV1, wmmat); + O(VAWarpedMotionParamsAV1, invalid); + O(VAWarpedMotionParamsAV1, va_reserved); + + S(VADecPictureParameterBufferAV1); + O(VADecPictureParameterBufferAV1, profile); + O(VADecPictureParameterBufferAV1, order_hint_bits_minus_1); + O(VADecPictureParameterBufferAV1, bit_depth_idx); + O(VADecPictureParameterBufferAV1, matrix_coefficients); + O(VADecPictureParameterBufferAV1, seq_info_fields); + O(VADecPictureParameterBufferAV1, current_frame); + O(VADecPictureParameterBufferAV1, current_display_picture); + O(VADecPictureParameterBufferAV1, anchor_frames_num); + O(VADecPictureParameterBufferAV1, anchor_frames_list); + O(VADecPictureParameterBufferAV1, frame_width_minus1); + O(VADecPictureParameterBufferAV1, frame_height_minus1); + O(VADecPictureParameterBufferAV1, output_frame_width_in_tiles_minus_1); + O(VADecPictureParameterBufferAV1, output_frame_height_in_tiles_minus_1); + O(VADecPictureParameterBufferAV1, ref_frame_map); + O(VADecPictureParameterBufferAV1, ref_frame_idx); + O(VADecPictureParameterBufferAV1, primary_ref_frame); + O(VADecPictureParameterBufferAV1, order_hint); + O(VADecPictureParameterBufferAV1, seg_info); + O(VADecPictureParameterBufferAV1, film_grain_info); + O(VADecPictureParameterBufferAV1, tile_cols); + O(VADecPictureParameterBufferAV1, tile_rows); + O(VADecPictureParameterBufferAV1, width_in_sbs_minus_1); + O(VADecPictureParameterBufferAV1, height_in_sbs_minus_1); + O(VADecPictureParameterBufferAV1, tile_count_minus_1); + O(VADecPictureParameterBufferAV1, context_update_tile_id); + O(VADecPictureParameterBufferAV1, pic_info_fields); + O(VADecPictureParameterBufferAV1, superres_scale_denominator); + O(VADecPictureParameterBufferAV1, interp_filter); + O(VADecPictureParameterBufferAV1, filter_level); + O(VADecPictureParameterBufferAV1, filter_level_u); + O(VADecPictureParameterBufferAV1, filter_level_v); + O(VADecPictureParameterBufferAV1, loop_filter_info_fields); + O(VADecPictureParameterBufferAV1, ref_deltas); + O(VADecPictureParameterBufferAV1, mode_deltas); + O(VADecPictureParameterBufferAV1, base_qindex); + O(VADecPictureParameterBufferAV1, y_dc_delta_q); + O(VADecPictureParameterBufferAV1, u_dc_delta_q); + O(VADecPictureParameterBufferAV1, u_ac_delta_q); + O(VADecPictureParameterBufferAV1, v_dc_delta_q); + O(VADecPictureParameterBufferAV1, v_ac_delta_q); + O(VADecPictureParameterBufferAV1, qmatrix_fields); + O(VADecPictureParameterBufferAV1, mode_control_fields); + O(VADecPictureParameterBufferAV1, cdef_damping_minus_3); + O(VADecPictureParameterBufferAV1, cdef_bits); + O(VADecPictureParameterBufferAV1, cdef_y_strengths); + O(VADecPictureParameterBufferAV1, cdef_uv_strengths); + O(VADecPictureParameterBufferAV1, loop_restoration_fields); + O(VADecPictureParameterBufferAV1, wm); + O(VADecPictureParameterBufferAV1, va_reserved); + printf("count VADecPictureParameterBufferAV1 wm %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->wm) / + sizeof(((VADecPictureParameterBufferAV1 *)0)->wm[0])); + printf("size union seq_info_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->seq_info_fields)); + printf("size union pic_info_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->pic_info_fields)); + printf("size union loop_filter_info_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->loop_filter_info_fields)); + printf("size union qmatrix_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->qmatrix_fields)); + printf("size union mode_control_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->mode_control_fields)); + printf("size union loop_restoration_fields %zu\n", + sizeof(((VADecPictureParameterBufferAV1 *)0)->loop_restoration_fields)); + + S(VASliceParameterBufferAV1); + O(VASliceParameterBufferAV1, slice_data_size); + O(VASliceParameterBufferAV1, slice_data_offset); + O(VASliceParameterBufferAV1, slice_data_flag); + O(VASliceParameterBufferAV1, tile_row); + O(VASliceParameterBufferAV1, tile_column); + O(VASliceParameterBufferAV1, tg_start); + O(VASliceParameterBufferAV1, tg_end); + O(VASliceParameterBufferAV1, anchor_frame_idx); + O(VASliceParameterBufferAV1, tile_idx_in_tile_list); + O(VASliceParameterBufferAV1, va_reserved); + + /* + * Bit-field allocation order for AV1's six unions — one field at a time, the + * same proof the H.264 block makes, repeated here because three of these + * unions are NARROWER than a word and a mistake there is invisible in a u32. + */ + { + VADecPictureParameterBufferAV1 a; + a.seq_info_fields.value = 0; + a.seq_info_fields.fields.still_picture = 1; + printf("bits av1 seq_info.still_picture=1 -> 0x%08x\n", a.seq_info_fields.value); + a.seq_info_fields.value = 0; + a.seq_info_fields.fields.film_grain_params_present = 1; + printf("bits av1 seq_info.film_grain_params_present=1 -> 0x%08x\n", + a.seq_info_fields.value); + a.seq_info_fields.value = 0; + a.seq_info_fields.fields.mono_chrome = 1; + printf("bits av1 seq_info.mono_chrome=1 -> 0x%08x\n", a.seq_info_fields.value); + + a.pic_info_fields.value = 0; + a.pic_info_fields.bits.frame_type = 3; + printf("bits av1 pic_info.frame_type=3 -> 0x%08x\n", a.pic_info_fields.value); + a.pic_info_fields.value = 0; + a.pic_info_fields.bits.large_scale_tile = 1; + printf("bits av1 pic_info.large_scale_tile=1 -> 0x%08x\n", a.pic_info_fields.value); + a.pic_info_fields.value = 0; + a.pic_info_fields.bits.use_ref_frame_mvs = 1; + printf("bits av1 pic_info.use_ref_frame_mvs=1 -> 0x%08x\n", a.pic_info_fields.value); + + a.loop_filter_info_fields.value = 0; + a.loop_filter_info_fields.bits.sharpness_level = 7; + printf("bits av1 loop_filter_info.sharpness_level=7 -> 0x%02x\n", + a.loop_filter_info_fields.value); + a.loop_filter_info_fields.value = 0; + a.loop_filter_info_fields.bits.mode_ref_delta_update = 1; + printf("bits av1 loop_filter_info.mode_ref_delta_update=1 -> 0x%02x\n", + a.loop_filter_info_fields.value); + + a.qmatrix_fields.value = 0; + a.qmatrix_fields.bits.using_qmatrix = 1; + printf("bits av1 qmatrix.using_qmatrix=1 -> 0x%04x\n", a.qmatrix_fields.value); + a.qmatrix_fields.value = 0; + a.qmatrix_fields.bits.qm_v = 0xf; + printf("bits av1 qmatrix.qm_v=0xf -> 0x%04x\n", a.qmatrix_fields.value); + + a.mode_control_fields.value = 0; + a.mode_control_fields.bits.delta_q_present_flag = 1; + printf("bits av1 mode_control.delta_q_present_flag=1 -> 0x%08x\n", + a.mode_control_fields.value); + a.mode_control_fields.value = 0; + a.mode_control_fields.bits.skip_mode_present = 1; + printf("bits av1 mode_control.skip_mode_present=1 -> 0x%08x\n", + a.mode_control_fields.value); + a.mode_control_fields.value = 0; + a.mode_control_fields.bits.tx_mode = 3; + printf("bits av1 mode_control.tx_mode=3 -> 0x%08x\n", a.mode_control_fields.value); + + a.loop_restoration_fields.value = 0; + a.loop_restoration_fields.bits.yframe_restoration_type = 3; + printf("bits av1 loop_restoration.yframe_restoration_type=3 -> 0x%04x\n", + a.loop_restoration_fields.value); + a.loop_restoration_fields.value = 0; + a.loop_restoration_fields.bits.lr_uv_shift = 1; + printf("bits av1 loop_restoration.lr_uv_shift=1 -> 0x%04x\n", + a.loop_restoration_fields.value); + + VASegmentationStructAV1 s; + s.segment_info_fields.value = 0; + s.segment_info_fields.bits.enabled = 1; + printf("bits av1 segment_info.enabled=1 -> 0x%08x\n", s.segment_info_fields.value); + s.segment_info_fields.value = 0; + s.segment_info_fields.bits.update_data = 1; + printf("bits av1 segment_info.update_data=1 -> 0x%08x\n", s.segment_info_fields.value); + + VAFilmGrainStructAV1 g; + g.film_grain_info_fields.value = 0; + g.film_grain_info_fields.bits.apply_grain = 1; + printf("bits av1 film_grain.apply_grain=1 -> 0x%08x\n", g.film_grain_info_fields.value); + g.film_grain_info_fields.value = 0; + g.film_grain_info_fields.bits.clip_to_restricted_range = 1; + printf("bits av1 film_grain.clip_to_restricted_range=1 -> 0x%08x\n", + g.film_grain_info_fields.value); + g.film_grain_info_fields.value = 0; + g.film_grain_info_fields.bits.grain_scale_shift = 3; + printf("bits av1 film_grain.grain_scale_shift=3 -> 0x%08x\n", + g.film_grain_info_fields.value); + } + + printf("enum VAProfileAV1Profile0 %d\n", VAProfileAV1Profile0); + printf("enum VAProfileAV1Profile1 %d\n", VAProfileAV1Profile1); + printf("enum VAAV1TransformationIdentity %d\n", VAAV1TransformationIdentity); + printf("enum VAAV1TransformationTranslation %d\n", VAAV1TransformationTranslation); + printf("enum VAAV1TransformationRotzoom %d\n", VAAV1TransformationRotzoom); + printf("enum VAAV1TransformationAffine %d\n", VAAV1TransformationAffine); + printf("enum VA_RT_FORMAT_YUV420_10 0x%08x\n", VA_RT_FORMAT_YUV420_10); + printf("enum VA_RT_FORMAT_YUV420 0x%08x\n", VA_RT_FORMAT_YUV420); + printf("VA_PADDING_LOW=%d VA_PADDING_MEDIUM=%d\n", VA_PADDING_LOW, VA_PADDING_MEDIUM); /* diff --git a/crates/pf-vaadec/src/config.rs b/crates/pf-vaadec/src/config.rs index 7aa09678..615670d2 100644 --- a/crates/pf-vaadec/src/config.rs +++ b/crates/pf-vaadec/src/config.rs @@ -17,6 +17,10 @@ pub const VA_PROFILE_H264_HIGH: i32 = 7; pub const VA_PROFILE_H264_CONSTRAINED_BASELINE: i32 = 13; pub const VA_PROFILE_HEVC_MAIN: i32 = 17; pub const VA_PROFILE_HEVC_MAIN10: i32 = 18; +/// Measured, not counted from the top of the enum: `VAProfileAV1Profile0` is **32** +/// and `VAProfileAV1Profile1` is 33, with ten VP9/HEVC enumerators in between. +pub const VA_PROFILE_AV1_PROFILE0: i32 = 32; +pub const VA_PROFILE_AV1_PROFILE1: i32 = 33; /// `VA_RT_FORMAT_*` — the surface render-target format. pub const VA_RT_FORMAT_YUV420: u32 = 0x0000_0001; @@ -29,6 +33,7 @@ pub const VA_RT_FORMAT_YUV420_10: u32 = 0x0000_0100; pub enum Codec { H264, H265, + Av1, } /// A profile choice, with the name the logs print. @@ -74,6 +79,14 @@ impl std::error::Error for ConfigError {} /// 4:4:4 is refused rather than mapped: `VAProfileH264High444` exists in the header /// but no driver in this fleet advertises it, and the Vulkan rung is where this /// program's 4:4:4 support actually lives. +/// +/// **AV1 Profile 0 covers 8 AND 10 bits under one enumerator**, so the pair differs +/// only in the render-target format — which is the one thing that must not be shared, +/// since it is what the surface pool is allocated with. Profile 1 (4:4:4) and +/// Profile 2 (4:2:2 / 12-bit) are refused: `va_dec_av1.h` opens by saying *"This AV1 +/// decoding API supports 8-bit/10bit 420 format only"*, so this is the API's +/// envelope and not merely ours. Monochrome reaches here as `chroma_format_idc` 0 +/// and lands in the same refusal rather than being mistaken for 4:2:0. pub fn profile_for( codec: Codec, chroma_format_idc: u8, @@ -84,6 +97,14 @@ pub fn profile_for( value: VA_PROFILE_H264_HIGH, name: "H.264 High", }), + (Codec::Av1, 1, 8) => Ok(VaProfile { + value: VA_PROFILE_AV1_PROFILE0, + name: "AV1 Profile 0", + }), + (Codec::Av1, 1, 10) => Ok(VaProfile { + value: VA_PROFILE_AV1_PROFILE0, + name: "AV1 Profile 0 (10-bit)", + }), (Codec::H265, 1, 8) => Ok(VaProfile { value: VA_PROFILE_HEVC_MAIN, name: "HEVC Main", @@ -137,10 +158,21 @@ pub const PRESENTER_HEADROOM: usize = 8; /// /// VAAPI reports no driver minimum to honour (DXVA's /// `ConfigMinRenderTargetBuffCount` has no counterpart), so this is the whole rule. +/// +/// AV1 passes [`AV1_MAX_DPB_FRAMES`] here — the codec's constant, not a stream +/// property. pub fn surface_count(max_dpb_frames: usize) -> usize { max_dpb_frames + 1 + PRESENTER_HEADROOM } +/// AV1's DPB depth: `NUM_REF_FRAMES`, and a constant of the codec rather than +/// anything a sequence header says. +/// +/// The slot ledger adds the picture being decoded, so an AV1 session's ledger holds +/// nine — libavcodec's own `num_surfaces = 1 + 8` for this codec, before the +/// presenter headroom this rung adds on top. +pub const AV1_MAX_DPB_FRAMES: usize = 8; + #[cfg(test)] mod tests { use super::*; @@ -163,6 +195,27 @@ mod tests { ); } + /// AV1 Profile 0 is one enumerator for two depths, and the depth still has to + /// reach the surface pool through `rt_format` rather than through the profile. + #[test] + fn av1_profile0_covers_both_depths_and_the_format_is_what_differs() { + assert_eq!( + profile_for(Codec::Av1, 1, 8).unwrap().value, + VA_PROFILE_AV1_PROFILE0 + ); + assert_eq!( + profile_for(Codec::Av1, 1, 10).unwrap().value, + VA_PROFILE_AV1_PROFILE0 + ); + assert_ne!( + profile_for(Codec::Av1, 1, 8).unwrap().name, + profile_for(Codec::Av1, 1, 10).unwrap().name, + "the log must still say which depth the session was built for" + ); + assert_eq!(rt_format(1, 8).unwrap(), VA_RT_FORMAT_YUV420); + assert_eq!(rt_format(1, 10).unwrap(), VA_RT_FORMAT_YUV420_10); + } + #[test] fn shapes_outside_the_envelope_are_refused_not_guessed() { // 10-bit H.264 (High10) and 4:4:4 both have header enumerators; neither is @@ -172,6 +225,26 @@ mod tests { assert!(profile_for(Codec::H264, 3, 8).is_err()); assert!(profile_for(Codec::H265, 3, 10).is_err()); assert!(rt_format(1, 12).is_err()); + // AV1 Profile 1 (4:4:4) and Profile 2 (4:2:2 / 12-bit) have no rung here — + // `va_dec_av1.h` says the API itself is 8/10-bit 4:2:0 only — and + // monochrome, which the AV1 planner reports as chroma_format_idc 0, is + // refused rather than treated as 4:2:0's neighbour. + assert!(profile_for(Codec::Av1, 3, 8).is_err()); + assert!(profile_for(Codec::Av1, 3, 10).is_err()); + assert!(profile_for(Codec::Av1, 1, 12).is_err()); + assert!(profile_for(Codec::Av1, 0, 8).is_err()); + assert!(profile_for(Codec::Av1, 2, 8).is_err()); + } + + /// AV1's pool is sized from the codec's constant, and the ledger it implies is + /// the nine slots [`crate::pic_av1::plan_to_va_av1`] insists on. + #[test] + fn the_av1_pool_is_the_codecs_eight_slots_plus_the_current_picture() { + assert_eq!(AV1_MAX_DPB_FRAMES, pf_bitstream::av1::NUM_REF_SLOTS); + assert_eq!( + surface_count(AV1_MAX_DPB_FRAMES), + 8 + 1 + PRESENTER_HEADROOM + ); } #[test] diff --git a/crates/pf-vaadec/src/lib.rs b/crates/pf-vaadec/src/lib.rs index e57ec530..10e424b5 100644 --- a/crates/pf-vaadec/src/lib.rs +++ b/crates/pf-vaadec/src/lib.rs @@ -1,5 +1,6 @@ -//! Native VAAPI decode for the Linux clients — M6 of the native-decode program, and -//! the VAAPI counterpart of [`pf_vkdecode`] and `pf-dxvadec`. +//! Native VAAPI decode for the Linux clients — M6 (H.264/HEVC) and M7 (AV1) of the +//! native-decode program, and the VAAPI counterpart of [`pf_vkdecode`] and +//! `pf-dxvadec`. //! //! Like `pf-dxvadec`, this crate is the **CPU-testable half**: everything between //! pf-bitstream's per-AU plan and the buffers a `vaRenderPicture` call delivers. It @@ -8,19 +9,26 @@ //! `cfg(target_os = "linux")` code that only a box can build, so anything left inside //! that boundary is verified by a remote `cargo check` and nothing more. //! -//! - [`va`]: the libva decode buffer layouts, **hand-declared**, with every size and -//! offset measured off the real headers and pinned as compile-time assertions. +//! - [`va`] / [`va_h265`] / [`va_av1`]: the libva decode buffer layouts, +//! **hand-declared**, with every size and offset measured off the real headers and +//! pinned as compile-time assertions. //! - [`config`]: profile, render-target format and surface-count decisions. -//! - [`pic`]: one `AuPlan` into picture parameters, IQ matrices and slice records. +//! - [`pic`] / [`pic_h265`] / [`pic_av1`]: one `AuPlan` into picture parameters, IQ +//! matrices and slice records (AV1: tile records, and no IQ matrix at all). //! //! # Status //! -//! **Both codecs converted, and the rung is wired.** `pf-client-core`'s +//! **All three codecs converted, and the rung is wired.** `pf-client-core`'s //! `video_vaapi_native` dlopens libva and drives these buffers; this crate holds //! everything decidable without a device — including [`drm`], the export //! descriptor the driver writes back and the plane walk that reads it. //! -//! Four things this crate settled that a reader would otherwise have to re-derive: +//! ⚠ **Nothing here has decoded a frame.** The rung is pin-only +//! (`PUNKTFUNK_DECODER=native-vaapi`) and no VAAPI hardware has been reachable +//! during M7, so everything below is a CPU-side conversion checked against +//! libavcodec and against measured layouts, not against a picture. +//! +//! Five things this crate settled that a reader would otherwise have to re-derive: //! //! * **`slice_data_bit_offset` costs no new parsing.** VAAPI is the only one of the //! three backends that wants a bit position — DXVA takes a byte offset, Vulkan @@ -45,6 +53,12 @@ //! vendored `PredWeightTable` stores `luma_offset_l0` as `[i8; 32]` but //! `luma_offset_l1` as `[i16; 32]`, an upstream inconsistency, while libva wants //! `i16` for both. +//! * **AV1's reference plumbing is a FIFTH convention**, and libva's AV1 buffers +//! break three of this rung's other habits: the "slice" parameter buffer is a TILE +//! parameter buffer, several of its records share ONE data buffer (the only place +//! `vaCreateBuffer`'s `num_elements` is not 1), and there is no IQ matrix buffer at +//! all. [`va_av1`] states the convention and what it was established from; +//! [`pic_av1`] is where it is applied. //! //! # Why the slot ledger is borrowed //! @@ -59,14 +73,30 @@ pub mod config; pub mod drm; pub mod pic; +pub mod pic_av1; pub mod pic_h265; pub mod va; +pub mod va_av1; pub mod va_h265; /// The DPB slot ledger — borrowed, not redefined (crate docs). pub use pf_vkdecode::SlotError; pub use pf_vkdecode::SlotMap; +/// The AV1 planner and its plan. ⚠ Its `plan_au` returns a **`Vec`**: an AV1 access +/// unit is a TEMPORAL UNIT and may carry several frames, of which at most one +/// displays. +pub use pf_bitstream::av1::AuPlan as AuPlanAv1; +pub use pf_bitstream::av1::Av1Planner; +pub use pf_bitstream::av1::DpbUpdate as DpbUpdateAv1; +pub use pf_bitstream::av1::FrameType as FrameTypeAv1; +pub use pf_bitstream::av1::ParsedFrameHeader as ParsedFrameHeaderAv1; +pub use pf_bitstream::av1::ParsedSequenceHeader as ParsedSequenceHeaderAv1; +pub use pf_bitstream::av1::PicId as PicIdAv1; +pub use pf_bitstream::av1::PicturePlan as PicturePlanAv1; +pub use pf_bitstream::av1::PlanError as PlanErrorAv1; +pub use pf_bitstream::av1::PlanWarning as PlanWarningAv1; +pub use pf_bitstream::av1::NUM_REF_SLOTS; /// The planners and plans this crate converts, re-exported so the Linux layer names /// every type it touches through `pf_vaadec` — the same courtesy `pf-dxvadec` does /// for the Windows layer. @@ -83,6 +113,7 @@ pub use pf_bitstream::h265::PlanWarning as PlanWarningH265; /// Which warnings mean the PICTURE is damaged — pf-vkdecode's one list, so all three /// native rungs conceal on exactly the same predicate. pub use pf_vkdecode::is_integrity_warning; +pub use pf_vkdecode::is_integrity_warning_av1; pub use pf_vkdecode::is_integrity_warning_h265; pub use drm::flatten; @@ -101,10 +132,15 @@ pub use config::surface_count; pub use config::Codec; pub use config::ConfigError; pub use config::VaProfile; +pub use config::AV1_MAX_DPB_FRAMES; pub use config::VA_ENTRYPOINT_VLD; pub use pic::plan_to_va; pub use pic::DecodePlanVa; pub use pic::PlanToVaError; +pub use pic_av1::plan_to_va_av1; +pub use pic_av1::DecodePlanVaAv1; +pub use pic_av1::PlanToVaAv1Error; +pub use pic_av1::TileGroupVa; pub use pic_h265::plan_to_va_h265; pub use pic_h265::DecodePlanVaH265; pub use pic_h265::PlanToVaH265Error; diff --git a/crates/pf-vaadec/src/pic_av1.rs b/crates/pf-vaadec/src/pic_av1.rs new file mode 100644 index 00000000..d86a2cd3 --- /dev/null +++ b/crates/pf-vaadec/src/pic_av1.rs @@ -0,0 +1,1591 @@ +//! One AV1 [`AuPlanAv1`] into libva's buffers — M7's VAAPI conversion, and the +//! third and last hardware rung for this codec. +//! +//! The layouts it fills are measured against the real `va_dec_av1.h` +//! ([`crate::va_av1`]); this module is where the AV1 frame header's meaning is +//! mapped onto them, and where the places VAAPI disagrees with the other two +//! backends are handled. +//! +//! # The reference convention, and what it is established from +//! +//! [`crate::va_av1`]'s module docs state it in full. In one line: **`ref_frame_map` +//! is indexed by AV1 reference SLOT and holds a `VASurfaceID`; `ref_frame_idx` is +//! indexed by reference NAME and holds a SLOT (an index into `ref_frame_map`); +//! global motion is a picture-level array indexed by NAME with `wm[0]` = +//! `LAST_FRAME`; and there is no per-reference size field at all.** That comes from +//! `va_dec_av1.h`'s own comments and from libavcodec's `vaapi_av1.c`, which is what +//! every VAAPI driver is validated against. +//! +//! The last clause is the one that differs from DXVA and is easy to get backwards. +//! `DXVA_PicEntry_AV1` carries each reference's own `width`/`height` because a +//! decoder scales motion out of a differently-sized reference (7.11.3.3 derives +//! `xStep` from `RefUpscaledWidth[refIdx]`). libva 2.23.0 has **no** +//! `ref_frame_width`/`ref_frame_height` — measured, `grep -c` is 0 — so a VAAPI +//! driver reads each reference's dimensions off the SURFACE. Nothing here needs +//! `RefState::upscaled_width`, and looking for a field to put it in would end in +//! writing it somewhere it does not belong. +//! +//! # What this conversion refuses, and why refusing is the honest answer +//! +//! **Film grain synthesis.** libva's picture buffer carries two surfaces — +//! `current_frame` (the decode target, which is also what later frames PREDICT +//! from) and `current_display_picture` (the grained output) — and libavcodec +//! allocates a second frame (`ctx->tmp_frame`) precisely so the two can differ. +//! With one surface there are only wrong answers: grain in the reference chain, +//! which drifts every later frame, or an ungrained picture on screen, and +//! `va_dec_av1.h` does not say which a driver would pick. So a frame with +//! `apply_grain` set is [`PlanToVaAv1Error::FilmGrain`] rather than a submission +//! that decodes to something. The film-grain STRUCTURE is declared and its layout +//! pinned ([`crate::va_av1::VaFilmGrainStructAV1`]); it is left zero, which libva +//! documents as "ignore all of this" when `apply_grain` is 0. The fill and the +//! second surface belong to the same future change and neither is written here. +//! +//! No punktfunk host emits film grain (no AV1 hardware encoder in the fleet does) +//! and neither vendored conformance vector codes it, so this refusal is reachable +//! only by a stream from elsewhere — where it costs the session this rung and gets +//! the FFmpeg rung, which synthesises grain correctly. +//! +//! The gate is per FRAME rather than per SEQUENCE on purpose: `film_grain_params_present` +//! only says the tool is coded, and a sequence that declares it while every frame +//! leaves `apply_grain` at 0 decodes here perfectly. Refusing on the sequence flag +//! would be a whole-session demotion bought with nothing. What the per-frame gate must +//! NOT do is poison the ledger on its way out, which is why it sits after the mutation +//! block — see "A refusal after the mutations is deliberate" below. +//! +//! # A lost reference gets a LIVE surface, not `VA_INVALID_ID` +//! +//! A slot the planner reports empty, and a slot whose picture this rung never decoded +//! into a surface, both arrive here as [`VA_INVALID_SURFACE`] in `ref_frame_map`. +//! Sending that is what `va_dec_av1.h:352` warns about — *"Driver is not responsible +//! to validate reference frames' id"* — and the sentence CONTINUES: *"If missing frame +//! is identified, application may choose to perform error recovery by pointing +//! problematic index to an alternative frame buffer."* That is what +//! [`DecodePlanVaAv1::substituted_refs`] records: every empty entry is pointed at a +//! live surface (a resolved reference where there is one, the decode target otherwise) +//! so a concealed frame is a driver predicting from the WRONG picture rather than a +//! driver dereferencing a handle that names nothing. +//! +//! ⚠ Only where the store is PUBLISHED. A shown key frame publishes an all-invalid map +//! deliberately (libavcodec does the same) and substituting there would depart from the +//! one path every driver is exercised on, for a frame that reads no references at all. +//! +//! # A refusal after the mutations is deliberate +//! +//! [`Av1Planner::plan_au`](pf_bitstream::av1::Av1Planner::plan_au) has already stored +//! this picture in its own reference store by the time the plan arrives, so a refusal +//! that skipped this rung's `slots.assign` would leave the ledger one picture short of +//! the planner's store FOREVER: the next access unit's `dpb_refs` names the picture, +//! [`SlotMap::slot_of`] answers `None`, and [`PlanToVaAv1Error::UnresolvedReference`] +//! fires — which is itself a refusal, so it never repairs. One lost tile group would +//! cost every frame until the next shown key frame. +//! +//! So the removals and the assignment run BEFORE the tile walk and before the film +//! grain gate, and every refusal past that point leaves the ledger in step with the +//! planner. The caller's side of the contract is in [`plan_to_va_av1`]'s docs: on a +//! refusal it must bind NOTHING to the assigned slot, which is what turns the next +//! frame's reference to this picture into the substitution above. +//! +//! # Tiles: one record per TILE, several records per BUFFER +//! +//! `VASliceParameterBufferAV1` is a tile parameter buffer under a misleading name +//! (the header says so). libavcodec sends, per tile-group OBU, **one parameter +//! buffer holding that group's records** beside **one data buffer holding the +//! group's whole `tile_data` region** — `tile_size_minus_1` fields and all — with +//! each record's `slice_data_offset` relative to that buffer. That is the DXVA +//! upload layout, not the Vulkan one, so [`Av1Bitstream::groups`] is the half of the +//! shared walk this rung reads, and [`DecodePlanVaAv1::tile_groups`] is grouped +//! accordingly rather than being a flat list. + +use std::ops::Range; + +use pf_bitstream::av1::coded_cdef_sec_strength; +use pf_bitstream::av1::AuPlan as AuPlanAv1; +use pf_bitstream::av1::FrameType; +use pf_bitstream::av1::PicId; +use pf_bitstream::av1::NUM_REF_SLOTS; +use pf_bitstream::av1::REFS_PER_FRAME; + +use crate::va::VA_INVALID_SURFACE; +use crate::va::VA_SLICE_DATA_FLAG_ALL; +use crate::va_av1::FilmGrainInfoFieldsAV1; +use crate::va_av1::LoopFilterInfoFieldsAV1; +use crate::va_av1::LoopRestorationFieldsAV1; +use crate::va_av1::ModeControlFieldsAV1; +use crate::va_av1::PicInfoFieldsAV1; +use crate::va_av1::QmatrixFieldsAV1; +use crate::va_av1::SegmentInfoFieldsAV1; +use crate::va_av1::SeqInfoFieldsAV1; +use crate::va_av1::VaDecPictureParameterBufferAV1; +use crate::va_av1::VaSegmentationStructAV1; +use crate::va_av1::VaSliceParameterBufferAV1; +use crate::va_av1::VaWarpedMotionParamsAV1; +use crate::va_av1::ANCHOR_FRAME_UNUSED; +use crate::va_av1::LAST_FRAME; +use crate::va_av1::SUPERRES_NUM; +use crate::va_av1::TILE_SBS_LEN; +use crate::SlotError; +use crate::SlotMap; +use pf_vkdecode::plan_bitstream; +use pf_vkdecode::Av1Bitstream; +use pf_vkdecode::Av1TileError; + +/// AV1's tile ceiling in one frame — `MAX_TILE_COLS` × `MAX_TILE_ROWS` is 4096, +/// which no AV1 level defines; libavcodec refuses past 256 ("exceeding all defined +/// levels in the AV1 spec") and so does the shared walk. +pub const MAX_TILES: usize = 256; + +/// AV1's `MAX_TILE_COLS` / `MAX_TILE_ROWS`, and the bound the parser's own +/// `TileInfo` arrays are sized to. +pub const MAX_TILE_DIM: usize = 64; + +/// One tile-group OBU's submission: the records for its tiles, and the byte range +/// (ACCESS-UNIT coordinates) of the `tile_data` region they address. +/// +/// The pairing is the point. `vaRenderPicture` establishes which data buffer a +/// parameter buffer's `slice_data_offset` is relative to by being handed the two +/// together, so the records and their region must travel as one thing. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TileGroupVa { + pub tiles: Vec, + pub data: Range, +} + +/// Everything one AV1 `vaRenderPicture` sequence needs. +#[derive(Debug, Clone)] +pub struct DecodePlanVaAv1 { + pub pic_params: VaDecPictureParameterBufferAV1, + /// One entry per tile-group (or frame) OBU, in decode order. + pub tile_groups: Vec, + /// The ledger slot this picture took — or `None` when the picture refreshes no + /// reference slot and the conversion gave the slot straight back (see the + /// `refresh_frame_flags == 0` note in [`plan_to_va_av1`]). + pub setup_slot: Option, + pub setup_id: PicId, + /// Which `ref_frame_map` entries were empty and got a live surface instead — bit + /// `i` for AV1 reference slot `i` (module docs, "A lost reference gets a LIVE + /// surface"). + /// + /// Non-zero means this frame is being concealed: it decodes from at least one + /// substitute. Reported rather than silent because it is the one thing about a + /// submission that a log cannot otherwise tell from a clean decode, and because a + /// clean stream must never produce it — `pf-vaadec`'s vector test asserts 0 across + /// all 274 frames. + pub substituted_refs: u8, +} + +/// Why an AV1 plan cannot be expressed as VAAPI buffers. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum PlanToVaAv1Error { + /// A `show_existing_frame` plan decodes nothing and has no submission. Not a + /// failure: the caller displays a surface it already holds. + NoDecode, + NoTiles, + /// The access unit's tile OBUs could not be walked into per-tile payloads. + Tiles(Av1TileError), + /// The frame header's tile GRID and the tiles the access unit actually carried + /// disagree — a dropped tile group, most likely, which nothing else reports. + TileCountMismatch { + records: usize, + walked: usize, + grid: usize, + }, + /// More tile columns or rows than AV1 defines. + TooManyTiles { + cols: u32, + rows: u32, + }, + /// A tile's payload is not inside the tile-group region the records address. + TileOutsideGroup { + tile: usize, + }, + /// The frame applies film grain, which needs a second surface this rung does not + /// allocate (module docs). + FilmGrain, + CapacityMismatch { + required: usize, + capacity: usize, + }, + /// A picture the marked store holds has no ledger slot, so no surface can be put + /// in `ref_frame_map` for it. + UnresolvedReference(PicId), + SurfaceOutOfRange { + slot: u8, + surfaces: usize, + }, + /// A header value wider than the libva field that carries it. + FieldOverflow { + field: &'static str, + value: u32, + }, + Slot(SlotError), +} + +impl From for PlanToVaAv1Error { + fn from(e: SlotError) -> Self { + PlanToVaAv1Error::Slot(e) + } +} + +impl PlanToVaAv1Error { + /// This refusal is the shape a LOST TILE GROUP makes. + /// + /// The distinction the caller needs, and the reason it is decided here rather than + /// by matching an enum at the call site: on a plan that already carries an + /// integrity warning these five are damage, not a defect — the access unit simply + /// did not carry the tiles its frame header announced — and the rung's answer to + /// damage is concealment, exactly as it is for every warning the planner raises. + /// On an UNDAMAGED plan the same five mean this conversion or the shared tile walk + /// disagrees with a stream that arrived whole, which is a defect and must surface + /// as one. + /// + /// Everything else stays a refusal either way: a capacity mismatch, an unresolved + /// reference or a field overflow says something about this rung's own state that + /// concealing would bury. + pub fn lost_tiles(&self) -> bool { + matches!( + self, + PlanToVaAv1Error::NoTiles + | PlanToVaAv1Error::Tiles(_) + | PlanToVaAv1Error::TileCountMismatch { .. } + | PlanToVaAv1Error::TooManyTiles { .. } + | PlanToVaAv1Error::TileOutsideGroup { .. } + ) + } +} + +impl std::fmt::Display for PlanToVaAv1Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PlanToVaAv1Error::NoDecode => { + write!(f, "a show_existing_frame access unit decodes nothing") + } + PlanToVaAv1Error::NoTiles => write!(f, "the access unit planned no tiles"), + PlanToVaAv1Error::Tiles(e) => write!(f, "tile walk: {e}"), + PlanToVaAv1Error::TileCountMismatch { + records, + walked, + grid, + } => write!( + f, + "{records} tile records and {walked} walked tiles for a {grid}-tile \ + grid — a tile group was lost" + ), + PlanToVaAv1Error::TooManyTiles { cols, rows } => { + write!(f, "a {cols}x{rows} tile grid is outside AV1's limits") + } + PlanToVaAv1Error::TileOutsideGroup { tile } => write!( + f, + "tile {tile}'s payload is not inside its tile group's data region" + ), + PlanToVaAv1Error::FilmGrain => write!( + f, + "this frame applies film grain, which needs a separate display \ + surface this rung does not allocate" + ), + PlanToVaAv1Error::CapacityMismatch { required, capacity } => write!( + f, + "the slot map holds {capacity} slots, AV1 needs {required}" + ), + PlanToVaAv1Error::UnresolvedReference(id) => { + write!(f, "picture {id} holds a reference slot but no surface") + } + PlanToVaAv1Error::SurfaceOutOfRange { slot, surfaces } => { + write!( + f, + "ledger slot {slot} has no surface in a table of {surfaces}" + ) + } + PlanToVaAv1Error::FieldOverflow { field, value } => { + write!(f, "{field} = {value} does not fit its libva field") + } + PlanToVaAv1Error::Slot(e) => write!(f, "DPB slot map: {e:?}"), + } + } +} + +impl std::error::Error for PlanToVaAv1Error {} + +fn narrow(field: &'static str, value: u32) -> Result { + u8::try_from(value).map_err(|_| PlanToVaAv1Error::FieldOverflow { field, value }) +} + +fn narrow16(field: &'static str, value: u32) -> Result { + u16::try_from(value).map_err(|_| PlanToVaAv1Error::FieldOverflow { field, value }) +} + +fn narrow32(field: &'static str, value: usize) -> Result { + u32::try_from(value).map_err(|_| PlanToVaAv1Error::FieldOverflow { + field, + value: u32::MAX, + }) +} + +/// Convert one planned AV1 frame. +/// +/// `au` is the access unit `plan` was planned from: the tile records need per-TILE +/// byte ranges, and finding those means walking each tile group's header and its +/// `tile_size_minus_1` fields — a walk over the bitstream, not over the plan. It is +/// [`plan_bitstream`], shared with the Vulkan and DXVA rungs. +/// +/// `surfaces` is the caller's ledger-slot → `VASurfaceID` table and `setup_surface` +/// is the surface this picture decodes INTO — the same parameter contract +/// [`crate::pic::plan_to_va`] documents, and for the same reason: the decode target +/// comes off the caller's free list at activation time and is bound to its slot +/// afterwards, because a slot freed by this access unit's own removals is free +/// again by the time the ledger is asked. +/// +/// # The frame that refreshes nothing +/// +/// A frame with `refresh_frame_flags == 0` is legal AV1 — shown once, referenced +/// never — and it enters the planner's store NOWHERE, so the planner can never +/// report it removed. It still needs a ledger slot while it is converted (that is +/// how a later frame would resolve its surface), so this function assigns one and +/// gives it straight back, exactly as [`crate::pic_h265::plan_to_va_h265`] does for +/// a picture its own access unit evicts. [`DecodePlanVaAv1::setup_slot`] is then +/// `None`, which is the caller's signal that nothing binds the surface and only its +/// pending-output claim keeps it off the free list. +/// +/// That the release can happen HERE rather than in the caller is a property of this +/// backend: a VAAPI ledger slot is not a surface (the DXVA rung's `setup_slot` IS +/// its surface index, which is why `pf_dxvadec` has to hold the slot until the frame +/// has been read). Nine such frames would otherwise exhaust a nine-slot ledger and +/// kill a session on correct streams. +/// +/// # What a refusal leaves behind, and what the caller owes it +/// +/// ⚠ `slots` is mutated BEFORE the tile walk and before the film grain gate, so a +/// refusal from either of those has already applied this access unit's removals and +/// assigned the setup picture its slot. That is deliberate and the module docs say +/// why: the planner stored the picture before this function was called, and a refusal +/// that skipped the assignment would desynchronise the ledger from the planner's store +/// permanently. +/// +/// What the caller owes in return is that on a refusal it binds **nothing** to the +/// assigned slot — no surface was written, and leaving the slot's PREVIOUS binding in +/// place would make the next frame predict from a picture that is not the one the +/// bitstream named. An unbound slot reads back as `VA_INVALID_SURFACE` in +/// `surfaces` and is then substituted (module docs), which is the concealment libva +/// documents. +/// +/// The refusals that can still fire before any mutation — [`PlanToVaAv1Error::NoDecode`], +/// [`PlanToVaAv1Error::CapacityMismatch`], [`PlanToVaAv1Error::SurfaceOutOfRange`], +/// [`PlanToVaAv1Error::UnresolvedReference`] and the `RefPic::slot` overflow — leave +/// `slots` untouched, so the same "bind nothing" answer is correct for them too. +pub fn plan_to_va_av1( + plan: &AuPlanAv1, + au: &[u8], + slots: &mut SlotMap, + surfaces: &[u32], + setup_surface: u32, +) -> Result { + let setup_id = plan.dpb.stored.ok_or(PlanToVaAv1Error::NoDecode)?; + let h = &*plan.header; + let seq = &*plan.sequence; + let color = &seq.color_config; + + // AV1's DPB depth is a constant of the codec: eight reference slots plus the + // picture being decoded. + let required = NUM_REF_SLOTS + 1; + if slots.capacity() != required { + return Err(PlanToVaAv1Error::CapacityMismatch { + required, + capacity: slots.capacity(), + }); + } + // A pre-check, so the caller's post-call bind of `setup_surface` to the returned + // slot is always in range. + if surfaces.len() < slots.capacity() { + return Err(PlanToVaAv1Error::SurfaceOutOfRange { + slot: (slots.capacity() - 1) as u8, + surfaces: surfaces.len(), + }); + } + + // --- the reference store, by AV1 SLOT, holding SURFACES ------------------- + // + // ⚠ Indexed by `RefPic::slot` (the bitstream's own 0..8 reference slot), NOT by + // the ledger slot — the ledger is only how a PicId finds its surface. Writing + // the ledger slot's number here would name a different reference on every frame + // whose store is not in ledger order, which is every frame after the first + // eviction. + let mut ref_frame_map = [VA_INVALID_SURFACE; NUM_REF_SLOTS]; + // ⚠ A SHOWN KEY FRAME publishes an empty store. libavcodec: + // `if (frame_type == AV1_FRAME_KEY && frame_header->show_frame) + // pic_param.ref_frame_map[i] = VA_INVALID_ID;` + // — the frame decodes from nothing and refreshes every slot, so the surfaces the + // store held a moment ago are not references for it. Ours would still list them + // (the plan's `dpb_refs` is the store BEFORE this frame's refresh), and the + // difference is exactly the one place drivers have been exercised. + let publishes_store = !(h.frame_type == FrameType::KeyFrame && h.show_frame); + if publishes_store { + for r in &plan.dpb_refs { + let ledger = slots + .slot_of(r.id) + .ok_or(PlanToVaAv1Error::UnresolvedReference(r.id))?; + let surface = + *surfaces + .get(usize::from(ledger)) + .ok_or(PlanToVaAv1Error::SurfaceOutOfRange { + slot: ledger, + surfaces: surfaces.len(), + })?; + let slot = usize::from(r.slot); + if slot >= NUM_REF_SLOTS { + return Err(PlanToVaAv1Error::FieldOverflow { + field: "RefPic::slot", + value: u32::from(r.slot), + }); + } + ref_frame_map[slot] = surface; + } + } + + // ⚠ An empty entry is pointed at a LIVE surface — the header's own prescription + // for a missing reference, quoted in the module docs. Two different losses land + // here and both need it: a slot the planner reports empty (its picture never + // arrived) and a slot whose picture this rung refused to convert, which the caller + // signals by binding no surface to it. + // + // ⚠ A resolved reference is preferred over `setup_surface`. Both are live and + // correctly sized, but the decode target is the surface the driver is about to + // WRITE, and naming it as its own reference is a shape some drivers validate + // against; a picture that actually decoded is the better substitute and is the + // "alternative frame buffer" the header means. The target is the fallback for the + // one case with nothing else to reach for — a store that resolved nothing at all. + let mut substituted_refs = 0u8; + if publishes_store { + let alternative = ref_frame_map + .iter() + .copied() + .find(|&s| s != VA_INVALID_SURFACE) + .unwrap_or(setup_surface); + for (slot, entry) in ref_frame_map.iter_mut().enumerate() { + if *entry == VA_INVALID_SURFACE { + *entry = alternative; + substituted_refs |= 1 << slot; + } + } + } + + // The seven reference NAMES, each holding the SLOT it reads — which is + // `ref_frame_idx[name]` verbatim, and libavcodec copies it unconditionally + // (a key or intra-only frame reads no references and the driver ignores it). + // + // ⚠ Deliberately NOT taken from `plan.refs`: a lost reference leaves a hole + // there, and a hole is not a slot. The name still points at the slot the + // bitstream coded, and the concealment is done one level down — that slot's + // `ref_frame_map` entry is the substituted surface above, so the name resolves to a + // live picture rather than to nothing. + let ref_frame_idx = h.ref_frame_idx; + + // --- mutations, once the references have resolved ------------------------ + // + // ⚠ HERE, and not after the tile walk. Every fallible step below leaves the ledger + // in step with the planner's store, which is what a refusal needs (fn docs); doing + // it the other way round turns one lost tile group into a hard `Err` on every + // frame until the next shown key frame. + // + // ⚠ But not before the loop above either: a reference resolves against the store as + // it stood BEFORE this access unit's removals, and releasing first would lose the + // picture a name still points at. + + for &id in &plan.dpb.removed { + if id == setup_id { + continue; + } + let _ = slots.release(id); + } + let assigned = slots.assign(setup_id)?; + // The frame that refreshes nothing never enters the store, so nothing will ever + // ask the ledger for it again (fn docs). + let setup_slot = if h.refresh_frame_flags == 0 { + slots.release(setup_id); + None + } else { + Some(assigned) + }; + + // Film grain: refused, not approximated (module docs). After the mutations, so the + // refusal costs this frame and not the rest of the GOP. + if seq.film_grain_params_present && h.film_grain_params.apply_grain { + return Err(PlanToVaAv1Error::FilmGrain); + } + + // --- tiles --------------------------------------------------------------- + // + // A frame header whose tile groups did not arrive is the everyday shape of a lost + // packet: `PlanWarning::TruncatedAu`, a plan that still stores its picture, and an + // empty or short tile list. It refuses here — there is nothing to submit — and + // [`PlanToVaAv1Error::lost_tiles`] is how the caller tells that damage apart from a + // defect. + if plan.tiles.is_empty() { + return Err(PlanToVaAv1Error::NoTiles); + } + let t = &h.tile_info; + if t.tile_cols == 0 || t.tile_rows == 0 { + return Err(PlanToVaAv1Error::NoTiles); + } + let grid = (t.tile_cols as usize).saturating_mul(t.tile_rows as usize); + if t.tile_cols as usize > MAX_TILE_DIM || t.tile_rows as usize > MAX_TILE_DIM { + return Err(PlanToVaAv1Error::TooManyTiles { + cols: t.tile_cols, + rows: t.tile_rows, + }); + } + if grid > MAX_TILES { + return Err(PlanToVaAv1Error::Tiles(Av1TileError::TooManyTiles { + tiles: grid, + })); + } + let bitstream: Av1Bitstream = + plan_bitstream(au, &plan.tiles, h).map_err(PlanToVaAv1Error::Tiles)?; + + let mut tile_groups: Vec = Vec::with_capacity(plan.tiles.len()); + let mut walked = 0usize; + // `plan_bitstream` pushes one region per plan tile group, in order, so `index` + // addresses this group's region — `get` rather than `[]` because a panic in a + // decode thread is a worse answer than a refusal even for a case the walk cannot + // produce. + for (index, tg) in plan.tiles.iter().enumerate() { + let region = bitstream + .groups + .get(index) + .ok_or(PlanToVaAv1Error::TileCountMismatch { + records: walked, + walked: bitstream.tiles.len(), + grid, + })? + .clone(); + // A group whose end precedes its start is malformed; the walk refuses it + // too, so this saturates rather than growing a second refusal path. + let count = tg.tg_end.saturating_sub(tg.tg_start).saturating_add(1); + let mut records = Vec::with_capacity(count as usize); + for step in 0..count { + let tile_num = tg.tg_start.saturating_add(step); + let payload = bitstream.tiles.get(walked).ok_or({ + PlanToVaAv1Error::TileCountMismatch { + records: walked, + walked: bitstream.tiles.len(), + grid, + } + })?; + walked += 1; + // The offset libva wants is relative to the DATA BUFFER, which is this + // group's whole `tile_data` region — not to the access unit, and not to + // the tile. Rebased here rather than by a packer, because unlike DXVA + // this rung uploads the region itself and has nothing to rebase against + // later. + if payload.start < region.start || payload.end > region.end { + return Err(PlanToVaAv1Error::TileOutsideGroup { tile: walked - 1 }); + } + records.push(VaSliceParameterBufferAV1 { + slice_data_size: narrow32("slice_data_size", payload.end - payload.start)?, + slice_data_offset: narrow32("slice_data_offset", payload.start - region.start)?, + slice_data_flag: VA_SLICE_DATA_FLAG_ALL, + // Tile numbering is libavcodec's: `tile_row = tile_num / tile_cols`, + // `tile_column = tile_num % tile_cols`, with `tile_num` running + // `tg_start..=tg_end` across the frame's groups. + tile_row: narrow16("tile_row", tile_num / t.tile_cols)?, + tile_column: narrow16("tile_column", tile_num % t.tile_cols)?, + // `va_deprecated`, and libavcodec fills both anyway. + tg_start: narrow16("tg_start", tg.tg_start)?, + tg_end: narrow16("tg_end", tg.tg_end)?, + anchor_frame_idx: ANCHOR_FRAME_UNUSED, + tile_idx_in_tile_list: 0, + va_reserved: [0; 4], + }); + } + tile_groups.push(TileGroupVa { + tiles: records, + data: region, + }); + } + // The independent cross-check, and the same one the DXVA rung makes: the tile + // GRID comes from the frame header and is what `tile_cols`/`tile_rows` announce + // to the driver, while the record count comes from the tile groups the access + // unit actually carried. A dropped tile group raises no warning anywhere else — + // the OBU walk simply never sees it — and submitting anyway declares a grid the + // tile buffers are short for. + if walked != grid || bitstream.tiles.len() != grid { + return Err(PlanToVaAv1Error::TileCountMismatch { + records: walked, + walked: bitstream.tiles.len(), + grid, + }); + } + + // --- the picture parameter blocks ---------------------------------------- + + let lf = &h.loop_filter_params; + let q = &h.quantization_params; + let c = &h.cdef_params; + let lr = &h.loop_restoration_params; + let sp = &h.segmentation_params; + let gm = &h.global_motion_params; + + let mut seg_info = VaSegmentationStructAV1::zeroed(); + seg_info.segment_info_fields = SegmentInfoFieldsAV1 { + enabled: sp.segmentation_enabled, + update_map: sp.segmentation_update_map, + temporal_update: sp.segmentation_temporal_update, + update_data: sp.segmentation_update_data, + } + .pack(); + for segment in 0..8 { + let mut mask = 0u8; + for (feature, enabled) in sp.feature_enabled[segment].iter().enumerate() { + if *enabled { + mask |= 1 << feature; + } + } + seg_info.feature_mask[segment] = mask; + // No clipping here: libva wants `FeatureData` AFTER 5.9.14's Clip3, and the + // vendored parser clips as it reads (`helpers::clip3` against the spec's + // `FEATURE_MAX`). Clipping again would be a no-op; not clipping at all would + // have been the bug, which is why this says which side did it. + seg_info.feature_data[segment] = sp.feature_data[segment]; + } + + let mut cdef_y_strengths = [0u8; crate::va_av1::CDEF_MAX]; + let mut cdef_uv_strengths = [0u8; crate::va_av1::CDEF_MAX]; + for i in 0..crate::va_av1::CDEF_MAX { + // The header's own formula: `(pri << 2) | (sec & 0x03)`. + // + // ⚠ `sec` must be the CODED two-bit read. AV1 5.9.19 rewrites the syntax + // element in place (a coded 3 becomes 4) and cros-codecs follows the spec, so + // masking the parser's value with 3 would turn the STRONGEST secondary filter + // into NO filter — on 68 of the vendored vector's 274 frames, including + // frame 0. `coded_cdef_sec_strength` is the inverse; its docs carry the + // evidence. + let pri_y = narrow("cdef_y_pri_strength", c.cdef_y_pri_strength[i])?; + let pri_uv = narrow("cdef_uv_pri_strength", c.cdef_uv_pri_strength[i])?; + cdef_y_strengths[i] = (pri_y << 2) | coded_cdef_sec_strength(c.cdef_y_sec_strength[i]); + cdef_uv_strengths[i] = (pri_uv << 2) | coded_cdef_sec_strength(c.cdef_uv_sec_strength[i]); + } + + let mut width_in_sbs_minus_1 = [0u16; TILE_SBS_LEN]; + let mut height_in_sbs_minus_1 = [0u16; TILE_SBS_LEN]; + // ⚠ Clamped to 63 entries. The arrays ARE 63 long and the header says why — the + // last tile's size is derived from the others and the frame size — but + // libavcodec loops to `tile_cols`, which writes index 63 on a 64-column frame. + // That is a one-element overrun in libavcodec, not a layout we should reproduce. + for (out, coded) in width_in_sbs_minus_1 + .iter_mut() + .zip(&t.width_in_sbs_minus_1[..(t.tile_cols as usize).min(TILE_SBS_LEN)]) + { + *out = narrow16("width_in_sbs_minus_1", *coded)?; + } + for (out, coded) in height_in_sbs_minus_1 + .iter_mut() + .zip(&t.height_in_sbs_minus_1[..(t.tile_rows as usize).min(TILE_SBS_LEN)]) + { + *out = narrow16("height_in_sbs_minus_1", *coded)?; + } + + let mut wm = [VaWarpedMotionParamsAV1::zeroed(); REFS_PER_FRAME]; + for (name, entry) in wm.iter_mut().enumerate() { + // ⚠ Global motion is indexed by reference NAME, never by DPB slot. AV1's + // `global_motion_params()` loops `ref = LAST_FRAME..ALTREF_FRAME` and the + // vendored parser stores it that way; libavcodec's `vaapi_av1.c` writes + // `pic_param.wm[i - 1]` for `i = LAST_FRAME..=ALTREF_FRAME`. Reading by slot + // agrees with the truth only while reference `i` happens to sit in slot + // `i + 1`, and silently hands every warped reference somebody else's warp + // the moment it does not. + let gm_name = LAST_FRAME + name; + entry.wmtype = gm.gm_type[gm_name] as u32; + // Six warp parameters, not eight: 5.9.24 codes six and libavcodec copies + // `for (j = 0; j < 6; j++)`. `wmmat[6]`/`wmmat[7]` stay zero. + entry.wmmat[..6].copy_from_slice(&gm.gm_params[gm_name]); + // `warp_valid` is the parser's `setup_shear` verdict — a warp whose shear + // parameters are out of range is unusable — and libva's flag is its inverse. + entry.invalid = u8::from(!gm.warp_valid[gm_name]); + } + + let bit_depth_idx = if color.high_bitdepth { + if color.twelve_bit { + 2 + } else { + 1 + } + } else { + 0 + }; + + let mut pic_params = VaDecPictureParameterBufferAV1::zeroed(); + pic_params.profile = seq.seq_profile as u8; + // ⚠ The parser types this `i32` and leaves it **-1** when `enable_order_hint` is + // 0 (`parser.rs`: `s.order_hint_bits_minus_1 = -1`). `as u8` on that is 255 — a + // decoder told the order hints are 256 bits wide — so the disabled case sends 0, + // which is what libavcodec's CBS holds for a field it never read. + pic_params.order_hint_bits_minus_1 = if seq.enable_order_hint { + narrow( + "order_hint_bits_minus_1", + u32::try_from(seq.order_hint_bits_minus_1).map_err(|_| { + PlanToVaAv1Error::FieldOverflow { + field: "order_hint_bits_minus_1", + value: 0, + } + })?, + )? + } else { + 0 + }; + pic_params.bit_depth_idx = bit_depth_idx; + pic_params.matrix_coefficients = color.matrix_coefficients as u8; + pic_params.seq_info_fields = SeqInfoFieldsAV1 { + still_picture: seq.still_picture, + use_128x128_superblock: seq.use_128x128_superblock, + enable_filter_intra: seq.enable_filter_intra, + enable_intra_edge_filter: seq.enable_intra_edge_filter, + enable_interintra_compound: seq.enable_interintra_compound, + enable_masked_compound: seq.enable_masked_compound, + enable_dual_filter: seq.enable_dual_filter, + enable_order_hint: seq.enable_order_hint, + enable_jnt_comp: seq.enable_jnt_comp, + enable_cdef: seq.enable_cdef, + mono_chrome: color.mono_chrome, + color_range: color.color_range, + subsampling_x: color.subsampling_x, + subsampling_y: color.subsampling_y, + chroma_sample_position: color.chroma_sample_position as u8, + film_grain_params_present: seq.film_grain_params_present, + } + .pack(); + pic_params.current_frame = setup_surface; + // Equal to `current_frame` because `apply_grain` is 0 on every frame that + // reaches here (module docs); libva then ignores this field entirely. + pic_params.current_display_picture = setup_surface; + pic_params.anchor_frames_num = 0; + pic_params.anchor_frames_list = std::ptr::null_mut(); + // The UPSCALED width — the same quantity libavcodec sends as the coded + // `frame_width_minus_1`, which AV1 5.9.8 reads into `UpscaledWidth` before + // superres divides it down into `FrameWidth`. + pic_params.frame_width_minus1 = narrow16( + "frame_width_minus1", + h.upscaled_width + .checked_sub(1) + .ok_or(PlanToVaAv1Error::FieldOverflow { + field: "upscaled_width", + value: 0, + })?, + )?; + pic_params.frame_height_minus1 = narrow16( + "frame_height_minus1", + h.frame_height + .checked_sub(1) + .ok_or(PlanToVaAv1Error::FieldOverflow { + field: "frame_height", + value: 0, + })?, + )?; + pic_params.ref_frame_map = ref_frame_map; + pic_params.ref_frame_idx = ref_frame_idx; + pic_params.primary_ref_frame = narrow("primary_ref_frame", h.primary_ref_frame)?; + pic_params.order_hint = narrow("order_hint", h.order_hint)?; + pic_params.seg_info = seg_info; + pic_params.tile_cols = narrow("tile_cols", t.tile_cols)?; + pic_params.tile_rows = narrow("tile_rows", t.tile_rows)?; + pic_params.width_in_sbs_minus_1 = width_in_sbs_minus_1; + pic_params.height_in_sbs_minus_1 = height_in_sbs_minus_1; + pic_params.context_update_tile_id = + narrow16("context_update_tile_id", t.context_update_tile_id)?; + pic_params.pic_info_fields = PicInfoFieldsAV1 { + frame_type: h.frame_type as u8, + show_frame: h.show_frame, + showable_frame: h.showable_frame, + error_resilient_mode: h.error_resilient_mode, + disable_cdf_update: h.disable_cdf_update, + allow_screen_content_tools: h.allow_screen_content_tools != 0, + force_integer_mv: h.force_integer_mv != 0, + allow_intrabc: h.allow_intrabc, + use_superres: h.use_superres, + allow_high_precision_mv: h.allow_high_precision_mv, + is_motion_mode_switchable: h.is_motion_mode_switchable, + use_ref_frame_mvs: h.use_ref_frame_mvs, + disable_frame_end_update_cdf: h.disable_frame_end_update_cdf, + uniform_tile_spacing_flag: t.uniform_tile_spacing_flag, + allow_warped_motion: h.allow_warped_motion, + large_scale_tile: false, + } + .pack(); + // The REAL denominator, not the coded one, and `SUPERRES_NUM` when superres is + // off — libva documents 8 there and 9..=16 otherwise, so a 0 would be outside + // the field's stated range. + pic_params.superres_scale_denominator = if h.use_superres { + narrow("superres_denom", h.superres_denom)? + } else { + SUPERRES_NUM + }; + pic_params.interp_filter = h.interpolation_filter as u8; + pic_params.filter_level = [lf.loop_filter_level[0], lf.loop_filter_level[1]]; + pic_params.filter_level_u = lf.loop_filter_level[2]; + pic_params.filter_level_v = lf.loop_filter_level[3]; + pic_params.loop_filter_info_fields = LoopFilterInfoFieldsAV1 { + sharpness_level: lf.loop_filter_sharpness, + mode_ref_delta_enabled: lf.loop_filter_delta_enabled, + mode_ref_delta_update: lf.loop_filter_delta_update, + } + .pack(); + pic_params.ref_deltas = lf.loop_filter_ref_deltas; + pic_params.mode_deltas = lf.loop_filter_mode_deltas; + pic_params.base_qindex = narrow("base_qindex", q.base_q_idx)?; + // The five deltas are `su(1+6)` reads, so the parser cannot hand out anything + // outside -63..=63 and the narrowing cannot truncate. + pic_params.y_dc_delta_q = q.delta_q_y_dc as i8; + pic_params.u_dc_delta_q = q.delta_q_u_dc as i8; + pic_params.u_ac_delta_q = q.delta_q_u_ac as i8; + pic_params.v_dc_delta_q = q.delta_q_v_dc as i8; + pic_params.v_ac_delta_q = q.delta_q_v_ac as i8; + pic_params.qmatrix_fields = QmatrixFieldsAV1 { + using_qmatrix: q.using_qmatrix, + // No 0xFF sentinel here, unlike DXVA: libva carries `using_qmatrix` itself, + // so a frame without a matrix simply leaves these ignored. + qm_y: narrow("qm_y", q.qm_y)?, + qm_u: narrow("qm_u", q.qm_u)?, + qm_v: narrow("qm_v", q.qm_v)?, + } + .pack(); + pic_params.mode_control_fields = ModeControlFieldsAV1 { + delta_q_present_flag: q.delta_q_present, + log2_delta_q_res: narrow("delta_q_res", q.delta_q_res)?, + delta_lf_present_flag: lf.delta_lf_present, + log2_delta_lf_res: lf.delta_lf_res, + delta_lf_multi: lf.delta_lf_multi, + tx_mode: h.tx_mode as u8, + reference_select: h.reference_select, + reduced_tx_set_used: h.reduced_tx_set, + skip_mode_present: h.skip_mode_present, + } + .pack(); + // The parser holds `CdefDamping` (coded + 3); libva wants the coded value. + pic_params.cdef_damping_minus_3 = + narrow("cdef_damping_minus_3", c.cdef_damping.saturating_sub(3))?; + pic_params.cdef_bits = narrow("cdef_bits", c.cdef_bits)?; + pic_params.cdef_y_strengths = cdef_y_strengths; + pic_params.cdef_uv_strengths = cdef_uv_strengths; + pic_params.loop_restoration_fields = LoopRestorationFieldsAV1 { + // The parser's `FrameRestorationType` IS the spec's, so no remap — see + // [`LoopRestorationFieldsAV1`]'s docs for why libavcodec appears to remap + // and this does not. + yframe_restoration_type: lr.frame_restoration_type[0] as u8, + cbframe_restoration_type: lr.frame_restoration_type[1] as u8, + crframe_restoration_type: lr.frame_restoration_type[2] as u8, + lr_unit_shift: lr.lr_unit_shift, + lr_uv_shift: lr.lr_uv_shift, + } + .pack(); + pic_params.wm = wm; + // Left zero, and deliberately: `apply_grain` is 0 on every frame that reaches + // here, which libva documents as "all the rest parameters should be set to zero + // and ignored". + pic_params.film_grain_info.film_grain_info_fields = FilmGrainInfoFieldsAV1::default().pack(); + + Ok(DecodePlanVaAv1 { + pic_params, + tile_groups, + setup_slot, + setup_id, + substituted_refs, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use cros_codecs::bitstream_utils::IvfIterator; + use pf_bitstream::av1::Av1Planner; + use std::collections::HashMap; + + const AV1_25FPS: &[u8] = include_bytes!( + "../../pf-bitstream/vendor/cros-codecs/src/codec/av1/test_data/test-25fps.ivf.av1" + ); + + /// A surface table with a recognisable value per ledger slot, so a wrong index + /// is a wrong NUMBER rather than a plausible one. + fn surface_table() -> Vec { + (0..NUM_REF_SLOTS as u32 + 1).map(|i| 0x1000 + i).collect() + } + + /// The caller's binding step, reproduced: re-derive the ledger-slot → surface + /// table from the ledger (a slot the conversion released binds nothing), then + /// bind the picture just converted. + /// + /// This is `video_vaapi_native`'s `bind_setup` + `sync_slot_bindings` field for + /// field, down to asking the LEDGER where the picture landed rather than being told + /// — and the test has to do it because the surface table the NEXT frame resolves + /// its references through is exactly this table. A fixed table would let the + /// reference checks below pass while reading somebody else's surface. + /// + /// `surface` is `None` for the refusal path, where the conversion assigned the slot + /// but nothing was decoded into a surface for it. Binding nothing is the caller's + /// half of [`plan_to_va_av1`]'s contract. + fn bind( + slot_surface: &mut [u32], + slots: &SlotMap, + stored: Option, + surface: Option, + ) { + let live: std::collections::HashSet = slots.held().map(|(slot, _)| slot).collect(); + for (index, bound) in slot_surface.iter_mut().enumerate() { + if !live.contains(&(index as u8)) { + *bound = VA_INVALID_SURFACE; + } + } + if let Some(slot) = stored.and_then(|id| slots.slot_of(id)) { + slot_surface[usize::from(slot)] = surface.unwrap_or(VA_INVALID_SURFACE); + } + } + + /// The whole vendored vector, converted — and every statement that could be + /// transposed checked against an independently kept shadow of the truth. + /// + /// The load-bearing assertions are the two indexings this API gets wrong most + /// easily: `ref_frame_map` is by AV1 SLOT (checked against a `PicId → surface` + /// map this test keeps itself, so a ledger-slot index would read a different + /// surface), and `wm[]` is by reference NAME (checked against + /// `gm_params[name + 1]`, so the off-by-one that hands every reference its + /// neighbour's warp fails here). + #[test] + fn the_whole_vendored_vector_converts_and_the_indexings_hold() { + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + // The caller's ledger-slot → surface bindings, maintained exactly as the + // client maintains them. + let mut slot_surface = vec![VA_INVALID_SURFACE; NUM_REF_SLOTS + 1]; + // Our own PicId → surface record, kept INDEPENDENTLY of the ledger — so a + // conversion that indexed the store by the wrong thing reads a surface this + // map disagrees with. + let mut surface_of: HashMap = HashMap::new(); + + let (mut frames, mut inter, mut warped, mut cdef_fixups) = (0u32, 0u32, 0u32, 0u32); + let mut multi_ref_slot_pictures = 0u32; + + for packet in IvfIterator::new(AV1_25FPS) { + for plan in planner.plan_au(packet).expect("the clean vector plans") { + let Some(id) = plan.dpb.stored else { + continue; + }; + frames += 1; + // The caller's free-list choice, faked deterministically: a surface + // number that is unique per picture, so a stale binding is visible. + let setup_surface = 0x9000 + frames; + // The table is snapshotted BEFORE the conversion, as the client does + // — references resolve against the pre-removal bindings. + let surfaces = slot_surface.clone(); + let va = plan_to_va_av1(&plan, packet, &mut slots, &surfaces, setup_surface) + .unwrap_or_else(|e| panic!("frame {frames}: {e}")); + surface_of.insert(id, setup_surface); + bind(&mut slot_surface, &slots, Some(id), Some(setup_surface)); + + assert_eq!(va.pic_params.current_frame, setup_surface); + assert_eq!( + va.pic_params.current_display_picture, setup_surface, + "no film grain means the display surface IS the decode target" + ); + + let h = &*plan.header; + let shown_key = h.frame_type == FrameType::KeyFrame && h.show_frame; + + // --- ref_frame_map is by AV1 SLOT and holds SURFACES --------- + let mut expected = [VA_INVALID_SURFACE; NUM_REF_SLOTS]; + if !shown_key { + for r in &plan.dpb_refs { + expected[usize::from(r.slot)] = *surface_of + .get(&r.id) + .unwrap_or_else(|| panic!("frame {frames}: no surface for {}", r.id)); + } + } + assert_eq!( + va.pic_params.ref_frame_map, expected, + "frame {frames}: the store must be indexed by AV1 slot and hold \ + the surface each slot's picture decoded into" + ); + assert_eq!( + va.substituted_refs, 0, + "frame {frames}: a stream that lost nothing must conceal nothing — \ + a substitution here means the reference plumbing is inventing \ + surfaces on a clean vector" + ); + if shown_key { + assert!( + va.pic_params + .ref_frame_map + .iter() + .all(|&s| s == VA_INVALID_SURFACE), + "frame {frames}: a shown key frame publishes an empty store" + ); + } + // One picture in SEVERAL AV1 slots is what makes the indexing above + // falsifiable: while every picture holds exactly one slot, an + // AV1-slot index and a per-picture index cannot be told apart. + let distinct_slots: std::collections::HashSet = + plan.dpb_refs.iter().map(|r| r.slot).collect(); + assert_eq!( + distinct_slots.len(), + plan.dpb_refs.len(), + "the marked store lists each slot once" + ); + let distinct_ids: std::collections::HashSet = + plan.dpb_refs.iter().map(|r| r.id).collect(); + if distinct_ids.len() < plan.dpb_refs.len() { + multi_ref_slot_pictures += 1; + } + + // --- ref_frame_idx is by NAME and holds a SLOT --------------- + assert_eq!( + va.pic_params.ref_frame_idx, h.ref_frame_idx, + "frame {frames}: the name table is the header's own slot list" + ); + if !h.frame_is_intra { + inter += 1; + for (name, r) in plan.refs.iter().enumerate() { + let r = r.expect("the clean vector loses no reference"); + assert_eq!( + va.pic_params.ref_frame_idx[name], r.slot, + "frame {frames}: name {name} must carry its SLOT" + ); + assert_eq!( + va.pic_params.ref_frame_map[usize::from(r.slot)], + surface_of[&r.id], + "frame {frames}: following name {name} through the slot \ + table must reach that reference's own surface" + ); + } + } + + // --- global motion is by NAME, one step off the parser ------- + for name in 0..REFS_PER_FRAME { + let gm = &h.global_motion_params; + assert_eq!( + va.pic_params.wm[name].wmmat[..6], + gm.gm_params[name + 1], + "frame {frames}: wm[{name}] must be reference name \ + {}'s warp, not slot {name}'s", + name + 1 + ); + assert_eq!(va.pic_params.wm[name].wmmat[6..], [0, 0]); + assert_eq!(va.pic_params.wm[name].wmtype, gm.gm_type[name + 1] as u32); + assert_eq!( + va.pic_params.wm[name].invalid, + u8::from(!gm.warp_valid[name + 1]) + ); + if va.pic_params.wm[name].wmtype != 0 { + warped += 1; + } + } + + // --- the tile records address the tile PAYLOADS -------------- + let grid = (h.tile_info.tile_cols * h.tile_info.tile_rows) as usize; + let records: usize = va.tile_groups.iter().map(|g| g.tiles.len()).sum(); + assert_eq!(records, grid, "frame {frames}: one record per tile"); + for group in &va.tile_groups { + let region = &packet[group.data.clone()]; + for tile in &group.tiles { + let start = tile.slice_data_offset as usize; + let end = start + tile.slice_data_size as usize; + assert!( + end <= region.len(), + "frame {frames}: a record runs past its data buffer" + ); + // The bytes the record addresses must BE a tile payload — + // and specifically not the group's own header, which is + // where the region starts and the payload does not. + assert!( + !region[start..end].is_empty(), + "frame {frames}: an empty tile" + ); + } + // The whole region must be accounted for: every tile's payload + // plus one `TileSizeBytes` field per tile EXCEPT the last. This + // is `tile_group_obu()`'s own arithmetic and is a fact about the + // bitstream rather than about this conversion, which is what + // makes it independent of the offsets it checks. + let size_bytes = if grid > 1 { + h.tile_info.tile_size_bytes as usize + } else { + 0 + }; + let payloads: usize = + group.tiles.iter().map(|t| t.slice_data_size as usize).sum(); + assert_eq!( + payloads + (group.tiles.len() - 1) * size_bytes, + group.data.end - group.data.start, + "frame {frames}: the group's tiles and its size fields must \ + account for the region exactly" + ); + assert_eq!( + group.tiles[0].slice_data_offset, 0, + "the first tile's payload starts the tile_data region" + ); + } + + // --- the scalar traps ---------------------------------------- + assert_eq!( + va.pic_params.frame_width_minus1 as u32, + h.upscaled_width - 1, + "frame {frames}: the width field is the UPSCALED width" + ); + assert_eq!(va.pic_params.frame_height_minus1 as u32, h.frame_height - 1); + assert_eq!( + va.pic_params.superres_scale_denominator, SUPERRES_NUM, + "this vector uses no superres, so the denominator is 8 — never 0" + ); + assert_eq!( + va.pic_params.order_hint_bits_minus_1 as i32, + plan.sequence.order_hint_bits_minus_1, + "the vector enables order hints, so the field is the parser's" + ); + let coded = 1usize << h.cdef_params.cdef_bits; + for i in 0..coded { + let sec = va.pic_params.cdef_y_strengths[i] & 0x3; + let pri = va.pic_params.cdef_y_strengths[i] >> 2; + assert_eq!(pri as u32, h.cdef_params.cdef_y_pri_strength[i]); + if h.cdef_params.cdef_y_sec_strength[i] == 4 { + cdef_fixups += 1; + assert_eq!( + sec, 3, + "frame {frames}: the spec's in-place 4 is the coded 3, \ + and masking it with 3 would send 0" + ); + } else { + assert_eq!(sec as u32, h.cdef_params.cdef_y_sec_strength[i]); + } + } + } + } + + assert_eq!(frames, 274, "every frame of the vector converted"); + assert!( + inter > 0, + "no inter frame: the name-table checks were vacuous" + ); + assert!( + multi_ref_slot_pictures > 0, + "no picture ever occupied two reference slots at once, so nothing here \ + could tell an AV1-slot index from a per-picture one" + ); + assert!( + cdef_fixups > 0, + "no frame coded a secondary strength needing the fixup, so the CDEF \ + packing above compared a correction against a stream that never needs it" + ); + // ⚠ Honest about what this vector does NOT cover: it codes no global motion + // at all, so the wm[] comparison above proves the INDEXING (each entry is + // read from `gm_params[name + 1]`) but every value compared is the identity + // warp. A transposition of two identical zeros is invisible. + eprintln!( + "frames {frames} · inter {inter} · non-identity warps {warped} \ + (0 means the warp VALUES are untested; the indexing is not) · \ + cdef fixups {cdef_fixups}" + ); + } + + /// A `show_existing_frame` plan has no submission — and the caller must be able + /// to tell that apart from a failure. + /// + /// ⚠ Built by hand, because the vendored vector uses `show_existing_frame` + /// **zero times** (pf-bitstream's own planner test says so and asserts it stays + /// 0). So what is exercised here is this function's `dpb.stored == None` arm and + /// nothing about the parser's display-only path. + #[test] + fn a_show_existing_frame_plan_is_not_a_decode() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("the key frame plans") + .first() + .expect("a frame") + .clone(); + let display_only = AuPlanAv1 { + dpb: pf_bitstream::av1::DpbUpdate { + stored: None, + outputs: vec![1], + removed: Vec::new(), + }, + tiles: Vec::new(), + ..key + }; + let mut slots = SlotMap::new(NUM_REF_SLOTS); + assert_eq!( + plan_to_va_av1(&display_only, first, &mut slots, &surface_table(), 7).err(), + Some(PlanToVaAv1Error::NoDecode) + ); + assert_eq!(slots.active(), 0, "a refusal must not touch the ledger"); + } + + /// Film grain is refused, not approximated (module docs) — and the refusal costs + /// this frame only. + /// + /// ⚠ The ledger assertion is the load-bearing half now. The gate sits AFTER the + /// mutation block precisely so a grained frame in the middle of a GOP does not + /// leave the ledger one picture short of the planner's store, which would turn + /// every later reference to it into a hard `UnresolvedReference` — a refusal that + /// can never repair itself. + #[test] + fn a_frame_that_applies_film_grain_is_refused() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("the key frame plans") + .first() + .expect("a frame") + .clone(); + + // The vector codes neither, so both halves of the gate are set by hand. + let mut seq = (*key.sequence).clone(); + seq.film_grain_params_present = true; + let mut header = (*key.header).clone(); + header.film_grain_params.apply_grain = true; + let grained = AuPlanAv1 { + sequence: std::rc::Rc::new(seq.clone()), + header: std::rc::Rc::new(header), + ..key.clone() + }; + let mut slots = SlotMap::new(NUM_REF_SLOTS); + assert_eq!( + plan_to_va_av1(&grained, first, &mut slots, &surface_table(), 7).err(), + Some(PlanToVaAv1Error::FilmGrain) + ); + let stored = grained.dpb.stored.expect("the key frame is stored"); + assert_eq!( + slots.slot_of(stored), + Some(0), + "the refusal must leave the ledger holding the picture the PLANNER stored \ + — the planner has no idea this rung said no, and every later frame that \ + names this picture resolves through this ledger" + ); + + // A sequence that DECLARES the tool but a frame that does not apply it is + // ordinary: the declaration alone must not cost the session this rung. + let declared_only = AuPlanAv1 { + sequence: std::rc::Rc::new(seq), + ..key + }; + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let va = plan_to_va_av1(&declared_only, first, &mut slots, &surface_table(), 7) + .expect("a declared-but-unused film grain tool still decodes"); + assert_eq!( + va.pic_params.film_grain_info, + crate::va_av1::VaFilmGrainStructAV1::zeroed(), + "apply_grain is 0, which libva documents as 'set the rest to zero'" + ); + assert_eq!( + va.pic_params.seq_info_fields & (1 << 15), + 1 << 15, + "the sequence's declaration is still reported" + ); + } + + /// `order_hint_bits_minus_1` must be 0 — not 255 — when order hints are off. + /// + /// The parser stores **-1** there, and `as u8` on that is 255: a decoder told + /// its order hints are 256 bits wide. Worth its own test because no vector here + /// disables order hints, so nothing else would ever exercise the branch. + #[test] + fn order_hints_off_sends_zero_not_the_parsers_minus_one() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("the key frame plans") + .first() + .expect("a frame") + .clone(); + assert!( + key.sequence.enable_order_hint, + "the vector enables order hints; this test is about the other branch" + ); + let mut seq = (*key.sequence).clone(); + seq.enable_order_hint = false; + seq.order_hint_bits_minus_1 = -1; + seq.order_hint_bits = 0; + let plan = AuPlanAv1 { + sequence: std::rc::Rc::new(seq), + ..key + }; + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let va = plan_to_va_av1(&plan, first, &mut slots, &surface_table(), 7).expect("converts"); + assert_eq!(va.pic_params.order_hint_bits_minus_1, 0); + assert_eq!( + va.pic_params.seq_info_fields & (1 << 7), + 0, + "and the sequence flag says so too" + ); + } + + /// A frame that refreshes no slot gives its ledger slot straight back. + /// + /// Nine such frames would otherwise fill a nine-slot ledger and kill the session + /// with `SlotError::Full` on a perfectly legal stream, which is the defect the + /// Vulkan and DXVA rungs each had to close separately. + #[test] + fn a_frame_that_refreshes_nothing_returns_its_slot() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("the key frame plans") + .first() + .expect("a frame") + .clone(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let surfaces = surface_table(); + + // The real key frame refreshes all eight slots and keeps its ledger slot. + let va = plan_to_va_av1(&key, first, &mut slots, &surfaces, 7).expect("converts"); + assert_eq!(va.setup_slot, Some(0)); + assert_eq!(slots.active(), 1); + + // The same frame with `refresh_frame_flags == 0`: converted, then released. + let mut header = (*key.header).clone(); + header.refresh_frame_flags = 0; + let ephemeral = AuPlanAv1 { + header: std::rc::Rc::new(header), + dpb: pf_bitstream::av1::DpbUpdate { + stored: Some(999), + outputs: vec![999], + removed: Vec::new(), + }, + ..key + }; + let va = plan_to_va_av1(&ephemeral, first, &mut slots, &surfaces, 8).expect("converts"); + assert_eq!( + va.setup_slot, None, + "nothing binds the surface — only the pending output claims it" + ); + assert_eq!( + slots.active(), + 1, + "the ledger is back where it was; a ninth such frame must still fit" + ); + assert_eq!(slots.slot_of(999), None); + } + + /// A ledger sized for another codec is refused rather than silently overflowed. + #[test] + fn a_ledger_of_the_wrong_capacity_is_refused() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("plans") + .first() + .expect("a frame") + .clone(); + // An H.264-shaped ledger (4-frame DPB) cannot hold AV1's eight slots. + let mut slots = SlotMap::new(4); + assert_eq!( + plan_to_va_av1(&key, first, &mut slots, &surface_table(), 7).err(), + Some(PlanToVaAv1Error::CapacityMismatch { + required: 9, + capacity: 5 + }) + ); + } + + /// A short surface table is refused BEFORE the ledger is touched, so the caller's + /// post-call bind is always in range. + #[test] + fn a_short_surface_table_is_refused_before_any_mutation() { + let mut planner = Av1Planner::new(); + let first = IvfIterator::new(AV1_25FPS).next().expect("a first packet"); + let key = planner + .plan_au(first) + .expect("plans") + .first() + .expect("a frame") + .clone(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let short = vec![0u32; NUM_REF_SLOTS]; + assert!(matches!( + plan_to_va_av1(&key, first, &mut slots, &short, 7).err(), + Some(PlanToVaAv1Error::SurfaceOutOfRange { .. }) + )); + assert_eq!(slots.active(), 0); + } + + /// **The lost-packet regression.** A frame header whose tile groups did not arrive + /// refuses — and the GOP survives it. + /// + /// This is the shape one lost UDP packet makes, and pf-bitstream produces it + /// deliberately: `plan_au` pushes a plan whose picture IS stored and whose tile list + /// is short or empty, with a `TruncatedAu` warning saying so. The planner's own + /// reference store already holds that picture by then, so a refusal that skipped + /// this rung's `slots.assign` would leave the two permanently one picture apart — + /// and the very next frame that names it would refuse with `UnresolvedReference`, + /// which is ALSO before the assignment and so can never repair. Every frame to the + /// next shown key frame would hard-error: one lost packet, one lost GOP. + /// + /// So this test asserts the three things that stop that: the refusal is + /// recognisable as damage ([`PlanToVaAv1Error::lost_tiles`]), the ledger holds the + /// picture the planner stored, and the NEXT access unit converts — with the lost + /// picture's slot concealed by a live surface rather than resolved to the surface + /// of whatever picture held that slot before. + #[test] + fn a_truncated_access_unit_refuses_but_leaves_the_ledger_in_step() { + let packets: Vec<&[u8]> = IvfIterator::new(AV1_25FPS).take(3).collect(); + assert_eq!(packets.len(), 3, "the vector has at least three packets"); + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let mut slot_surface = vec![VA_INVALID_SURFACE; NUM_REF_SLOTS + 1]; + + // --- packet 0: an ordinary shown key frame ----------------------------- + let key = planner.plan_au(packets[0]).expect("plans").remove(0); + let key_id = key.dpb.stored.expect("the key frame decodes"); + plan_to_va_av1(&key, packets[0], &mut slots, &slot_surface.clone(), 0x9001) + .expect("the key frame converts"); + bind(&mut slot_surface, &slots, Some(key_id), Some(0x9001)); + + // --- packet 1: two frames, and the FIRST loses its tile groups --------- + // + // Packet 1 of this vector is the standard AV1 shape: a hidden ALTREF that later + // frames predict from, then the frame that displays. Losing the hidden one is + // the worst case — nothing shows it, so nothing else would ever notice. + let mut unit = planner.plan_au(packets[1]).expect("plans"); + assert_eq!( + unit.len(), + 2, + "packet 1 is a hidden ALTREF plus a shown frame" + ); + let shown = unit.remove(1); + let mut lost = unit.remove(0); + let lost_id = lost.dpb.stored.expect("a truncated frame is still STORED"); + assert!( + !lost.header.show_frame, + "the frame this test loses is the hidden one" + ); + assert!( + !lost.tiles.is_empty(), + "the vector's own packet carries tiles; this test takes them away" + ); + // Exactly what pf-bitstream hands over when the tile-group OBUs are gone: the + // picture is stored, the tile list is empty, and the warning says damage. + lost.tiles.clear(); + lost.warnings + .push(pf_bitstream::av1::PlanWarning::TruncatedAu { offset: 0 }); + assert!( + lost.warnings.iter().any(crate::is_integrity_warning_av1), + "the plan this test drives must be one the rung CONCEALS" + ); + + let refusal = plan_to_va_av1(&lost, packets[1], &mut slots, &slot_surface.clone(), 0x9002) + .expect_err("no tiles, nothing to submit"); + assert_eq!(refusal, PlanToVaAv1Error::NoTiles); + assert!( + refusal.lost_tiles(), + "the caller tells damage from a defect through this predicate; a refusal \ + it does not recognise is a hard error and demotes the rung" + ); + let ledger_slot = slots.slot_of(lost_id).expect( + "THE REGRESSION: the planner stored this picture, so the ledger must hold \ + it too — without this every later reference to it is a hard Err that \ + never repairs", + ); + // The caller's half of the contract: the slot is live, but NOTHING is bound to + // it, because nothing was decoded. + bind(&mut slot_surface, &slots, Some(lost_id), None); + assert_eq!( + slot_surface[usize::from(ledger_slot)], + VA_INVALID_SURFACE, + "a picture that never decoded must not inherit the surface of whatever \ + held its ledger slot before" + ); + + // --- the rest of the unit, and the next one, must still convert -------- + let mut substituted_somewhere = false; + for (plan, packet, surface) in [ + (&shown, packets[1], 0x9003u32), + ( + &planner.plan_au(packets[2]).expect("plans").remove(0), + packets[2], + 0x9004, + ), + ] { + let id = plan.dpb.stored.expect("decodes"); + assert!( + plan.dpb_refs.iter().any(|r| r.id == lost_id), + "this frame must reference the truncated picture or it proves nothing" + ); + let va = plan_to_va_av1(plan, packet, &mut slots, &slot_surface.clone(), surface) + .expect("a frame after a lost one converts — it does not hard-error"); + + // Every slot the lost picture holds is concealed with a LIVE surface, and + // the surface chosen is a picture that really decoded (the key frame's), + // never the `VA_INVALID_SURFACE` a driver would dereference. + for r in &plan.dpb_refs { + let bit = 1u8 << r.slot; + if r.id == lost_id { + substituted_somewhere = true; + assert_eq!( + va.substituted_refs & bit, + bit, + "slot {} holds a picture with no surface and must be reported \ + substituted", + r.slot + ); + assert_eq!( + va.pic_params.ref_frame_map[usize::from(r.slot)], + 0x9001, + "and it must point at a picture that DECODED — the key \ + frame's surface — rather than at nothing" + ); + } else { + assert_eq!( + va.substituted_refs & bit, + 0, + "slot {} resolved; substituting it would hide a real reference", + r.slot + ); + } + } + bind(&mut slot_surface, &slots, Some(id), Some(surface)); + } + assert!( + substituted_somewhere, + "no frame ever named the lost picture, so nothing above was checked" + ); + } + + /// A store that resolved NOTHING still submits live surfaces. + /// + /// The fallback arm of the substitution, which the truncated-AU test above cannot + /// reach: with no decoded reference to reach for, the decode target itself is the + /// "alternative frame buffer" `va_dec_av1.h:352` prescribes. What must never + /// happen is `VA_INVALID_SURFACE` reaching a driver the same header says is *"not + /// responsible to validate reference frames' id"*. + #[test] + fn a_store_with_no_surfaces_at_all_falls_back_to_the_decode_target() { + let packets: Vec<&[u8]> = IvfIterator::new(AV1_25FPS).take(2).collect(); + let mut planner = Av1Planner::new(); + let mut slots = SlotMap::new(NUM_REF_SLOTS); + + let key = planner.plan_au(packets[0]).expect("plans").remove(0); + let key_id = key.dpb.stored.expect("decodes"); + plan_to_va_av1(&key, packets[0], &mut slots, &surface_table(), 0x9001).expect("converts"); + assert!(slots.slot_of(key_id).is_some()); + + // The key frame's picture holds every slot, and the caller bound none of them — + // the state after a whole access unit was refused. + let unbound = vec![VA_INVALID_SURFACE; NUM_REF_SLOTS + 1]; + let next = planner.plan_au(packets[1]).expect("plans").remove(0); + assert_eq!(next.dpb_refs.len(), NUM_REF_SLOTS, "a full store"); + let va = plan_to_va_av1(&next, packets[1], &mut slots, &unbound, 0x9002).expect("converts"); + assert_eq!( + va.substituted_refs, 0xff, + "every slot of the store was concealed" + ); + assert_eq!( + va.pic_params.ref_frame_map, [0x9002; NUM_REF_SLOTS], + "with nothing else live, the decode target is the substitute" + ); + + // ⚠ And a SHOWN KEY FRAME is exempt: libavcodec publishes an all-invalid map + // there deliberately, and that is the one path every driver is exercised on. + let mut slots = SlotMap::new(NUM_REF_SLOTS); + let va = plan_to_va_av1(&key, packets[0], &mut slots, &unbound, 0x9001).expect("converts"); + assert_eq!(va.substituted_refs, 0); + assert_eq!(va.pic_params.ref_frame_map, [VA_INVALID_SURFACE; 8]); + } +} diff --git a/crates/pf-vaadec/src/va_av1.rs b/crates/pf-vaadec/src/va_av1.rs new file mode 100644 index 00000000..72e076ea --- /dev/null +++ b/crates/pf-vaadec/src/va_av1.rs @@ -0,0 +1,1305 @@ +//! The libva decode buffer layouts for AV1, hand-declared — the third sibling of +//! [`crate::va`] and [`crate::va_h265`], measured the same way and pinned the same +//! way. +//! +//! Sizes and offsets come from the committed `layout-probe.c` run against libva +//! **2.23.0** headers (`va_dec_av1.h`, x86_64-linux-gnu): +//! `VASegmentationStructAV1` **156**, `VAFilmGrainStructAV1` **176**, +//! `VAWarpedMotionParamsAV1` **56**, `VADecPictureParameterBufferAV1` **1160** +//! (align **8**), `VASliceParameterBufferAV1` **40**. Every bit position below was +//! read back off a real header one field at a time, not counted by eye — which +//! matters more here than for the other two codecs, because **three of AV1's six +//! bit-field unions are NARROWER than a word**: `loop_filter_info_fields` is a +//! `uint8_t`, `qmatrix_fields` and `loop_restoration_fields` are `uint16_t`. A +//! `u32` packer over any of them would write straight through the neighbouring +//! field, and on the two `uint16_t` ones that neighbour is padding on one side and +//! `mode_control_fields` / `wm[0]` on the other. +//! +//! # AV1's reference plumbing is a FIFTH convention +//! +//! This program has now written down five spellings of "which pictures does this +//! frame use", and they are not interchangeable: +//! +//! * **Vulkan H.265** — DPB *slot* indices in `RefPicSetStCurr*`; +//! * **DXVA H.265** — positions into `RefPicList[]` in identically named arrays; +//! * **VAAPI H.265** — membership *flags* ORed onto each DPB entry, with per-slice +//! lists indexing `ReferenceFrames`; +//! * **DXVA AV1** — `frame_refs[7]` by reference NAME, each entry carrying a +//! reference SLOT that indexes `RefFrameMapTextureIndex[8]`, plus that +//! reference's own size and own global motion; +//! * **VAAPI AV1** — [`VaDecPictureParameterBufferAV1::ref_frame_map`] is indexed +//! by AV1 reference **SLOT** (0..8) and holds a **`VASurfaceID`** — an actual +//! surface handle, not an index into anything — while +//! [`VaDecPictureParameterBufferAV1::ref_frame_idx`] is indexed by reference +//! **NAME** and holds *"a list of indices into `ref_frame_map[8]`"*, i.e. the +//! slot. Global motion is a **picture-level** array +//! ([`VaDecPictureParameterBufferAV1::wm`], seven entries, `wm[0]` = `LAST_FRAME`) +//! and NOT part of a reference entry, and **there is no per-reference size +//! anywhere in this structure at all**. +//! +//! Both halves of that last sentence are measured rather than assumed: +//! `grep -c ref_frame_width /usr/include/va/va_dec_av1.h` is **0** on libva 2.23.0, +//! so a VAAPI driver takes each reference's dimensions from the SURFACE it was +//! decoded into. (`pf_bitstream::av1::RefState::upscaled_width`'s doc comment says +//! VA-API has `ref_frame_width`/`ref_frame_height`; it does not, and the field is +//! still load-bearing for DXVA, which does.) The two statements that DO reach a +//! VAAPI driver — the slot table and the name table — come from `va_dec_av1.h`'s +//! own comments and from libavcodec's `vaapi_av1.c`: +//! +//! ```text +//! pic_param.ref_frame_map[i] = for i in 0..8 +//! pic_param.ref_frame_idx[i] = frame_header->ref_frame_idx[i] for i in 0..7 +//! pic_param.wm[i - 1] = +//! for i in LAST_FRAME..=ALTREF_FRAME +//! ``` +//! +//! # Where libva's AV1 buffers differ from every other codec here +//! +//! * **The "slice" parameter buffer is a TILE parameter buffer.** The header says so +//! in as many words: *"It uses the name VASliceParameterBufferAV1 to be consistent +//! with other codec, but actually means VATileParameterBufferAV1."* One record per +//! TILE, not per tile group. +//! * **Several records share one data buffer.** libavcodec's `vaapi_av1.c` calls +//! `ff_vaapi_decode_make_slice_buffer` once per tile-group OBU with `nb_params = +//! tg_end - tg_start + 1`, so one `VASliceParameterBufferType` buffer carries +//! `nb_params` ELEMENTS beside one `VASliceDataBufferType` buffer holding the whole +//! group's `tile_data` region, and each record's `slice_data_offset` is relative to +//! THAT buffer. H.264 and H.265 send one record per buffer, so this is the only +//! place `vaCreateBuffer`'s `num_elements` is not 1. +//! * **There is no IQ matrix buffer.** AV1's quantiser matrices are SELECTED by +//! index out of tables the decoder already holds +//! ([`QmatrixFieldsAV1`]), so a submission is picture parameters plus tile +//! pairs and nothing else. + +/// `VASliceParameterBufferAV1::anchor_frame_idx` on an ordinary frame. +/// +/// `anchor_frame_idx` selects a reference for LARGE-SCALE TILE decoding, which no +/// punktfunk stream and no conformance vector here uses; libavcodec leaves the whole +/// record zero-initialised and never writes the field. +pub const ANCHOR_FRAME_UNUSED: u8 = 0; + +/// `PRIMARY_REF_NONE` (AV1 spec 6.8.2): `primary_ref_frame` meaning "this frame +/// loads no propagated state". +pub const PRIMARY_REF_NONE: u8 = 7; + +/// `SUPERRES_NUM` (AV1 spec): the `superres_scale_denominator` that means "no +/// upscaling". libva documents the field as 8 when `use_superres` is 0 and 9..=16 +/// when it is 1 — so a frame without superres does NOT send 0 here. +pub const SUPERRES_NUM: u8 = 8; + +/// `VAAV1TransformationType` (measured: 0, 1, 2, 3). The same numbering AV1 5.9.24 +/// gives `GmType`, and the same the vendored parser's `WarpModelType` uses — so the +/// conversion casts rather than remaps, and this table is here to make that +/// checkable. +pub const VA_AV1_TRANSFORMATION_IDENTITY: u32 = 0; +pub const VA_AV1_TRANSFORMATION_TRANSLATION: u32 = 1; +pub const VA_AV1_TRANSFORMATION_ROTZOOM: u32 = 2; +pub const VA_AV1_TRANSFORMATION_AFFINE: u32 = 3; + +/// `ref_frame_map[8]` — AV1's `NUM_REF_FRAMES`. +pub const REF_FRAME_MAP_LEN: usize = 8; + +/// `ref_frame_idx[7]` / `wm[7]` — AV1's `REFS_PER_FRAME`. +pub const REFS_PER_FRAME: usize = 7; + +/// `ref_deltas[8]` — AV1's `TOTAL_REFS_PER_FRAME`. +pub const TOTAL_REFS_PER_FRAME: usize = 8; + +/// `cdef_y_strengths[8]` / `cdef_uv_strengths[8]` — as many as `cdef_bits` can +/// select (`1 << 3`). +pub const CDEF_MAX: usize = 8; + +/// `width_in_sbs_minus_1[63]` / `height_in_sbs_minus_1[63]`. +/// +/// ⚠ **63, not 64**, and the header explains why: *"Though the maximum number of +/// tiles is 64, since ones of the last tile are computed from ones of the other +/// tiles and frame_width/height, they are not necessarily specified."* libavcodec's +/// `vaapi_av1.c` nonetheless loops `for (i = 0; i < frame_header->tile_cols; i++)`, +/// which writes index 63 — one past the end — on a 64-column frame. The conversion +/// clamps instead; see [`crate::pic_av1`]. +pub const TILE_SBS_LEN: usize = 63; + +/// `wm[7]`'s index for a reference NAME. +/// +/// libavcodec writes `pic_param.wm[i - 1]` for `i = LAST_FRAME..=ALTREF_FRAME`, so +/// `wm[0]` is `LAST_FRAME` and `wm[6]` is `ALTREF_FRAME` — the same indexing +/// `pf_bitstream::av1::AuPlan::refs` uses, and one step off the parser's own +/// `gm_params[]`, which is indexed by the spec's reference index (`INTRA_FRAME` = 0). +pub const LAST_FRAME: usize = 1; + +// --------------------------------------------------------------------------- +// The bit-field unions, unpacked +// --------------------------------------------------------------------------- + +/// `VADecPictureParameterBufferAV1::seq_info_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SeqInfoFieldsAV1 { + pub still_picture: bool, + 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_dual_filter: bool, + pub enable_order_hint: bool, + pub enable_jnt_comp: bool, + pub enable_cdef: bool, + pub mono_chrome: bool, + pub color_range: bool, + pub subsampling_x: bool, + pub subsampling_y: bool, + /// `va_deprecated` in the header, and still part of the layout — a field that is + /// deprecated is not a field that moved. + /// + /// ⚠ **ONE bit**, where AV1's `chroma_sample_position` is a two-bit enumerator + /// (UNKNOWN 0, VERTICAL 1, COLOCATED 2). [`Self::pack`] masks, which is exactly + /// what libavcodec's assignment into the C bit-field does — so COLOCATED reaches a + /// driver as UNKNOWN through both paths. Not a defect this rung can fix: there is + /// no second bit to put it in, and the field is deprecated precisely because + /// drivers do not read it. + pub chroma_sample_position: u8, + pub film_grain_params_present: bool, +} + +impl SeqInfoFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.still_picture as u32) + | ((self.use_128x128_superblock as u32) << 1) + | ((self.enable_filter_intra as u32) << 2) + | ((self.enable_intra_edge_filter as u32) << 3) + | ((self.enable_interintra_compound as u32) << 4) + | ((self.enable_masked_compound as u32) << 5) + | ((self.enable_dual_filter as u32) << 6) + | ((self.enable_order_hint as u32) << 7) + | ((self.enable_jnt_comp as u32) << 8) + | ((self.enable_cdef as u32) << 9) + | ((self.mono_chrome as u32) << 10) + | ((self.color_range as u32) << 11) + | ((self.subsampling_x as u32) << 12) + | ((self.subsampling_y as u32) << 13) + | ((self.chroma_sample_position as u32 & 0x1) << 14) + | ((self.film_grain_params_present as u32) << 15) + } +} + +/// `VADecPictureParameterBufferAV1::pic_info_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct PicInfoFieldsAV1 { + /// 0 KEY, 1 INTER, 2 INTRA_ONLY, 3 SWITCH — the AV1 spec's own numbering, which + /// is the vendored parser's `FrameType` discriminant too. + pub frame_type: u8, + 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 allow_intrabc: bool, + pub use_superres: bool, + pub allow_high_precision_mv: bool, + pub is_motion_mode_switchable: bool, + pub use_ref_frame_mvs: bool, + pub disable_frame_end_update_cdf: bool, + pub uniform_tile_spacing_flag: bool, + pub allow_warped_motion: bool, + /// Large-scale tile decoding — outside this rung's envelope, always false. + pub large_scale_tile: bool, +} + +impl PicInfoFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.frame_type as u32 & 0x3) + | ((self.show_frame as u32) << 2) + | ((self.showable_frame as u32) << 3) + | ((self.error_resilient_mode as u32) << 4) + | ((self.disable_cdf_update as u32) << 5) + | ((self.allow_screen_content_tools as u32) << 6) + | ((self.force_integer_mv as u32) << 7) + | ((self.allow_intrabc as u32) << 8) + | ((self.use_superres as u32) << 9) + | ((self.allow_high_precision_mv as u32) << 10) + | ((self.is_motion_mode_switchable as u32) << 11) + | ((self.use_ref_frame_mvs as u32) << 12) + | ((self.disable_frame_end_update_cdf as u32) << 13) + | ((self.uniform_tile_spacing_flag as u32) << 14) + | ((self.allow_warped_motion as u32) << 15) + | ((self.large_scale_tile as u32) << 16) + } +} + +/// `VADecPictureParameterBufferAV1::loop_filter_info_fields` — **8 bits**, not 32. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct LoopFilterInfoFieldsAV1 { + pub sharpness_level: u8, + pub mode_ref_delta_enabled: bool, + pub mode_ref_delta_update: bool, +} + +impl LoopFilterInfoFieldsAV1 { + pub const fn pack(self) -> u8 { + (self.sharpness_level & 0x7) + | ((self.mode_ref_delta_enabled as u8) << 3) + | ((self.mode_ref_delta_update as u8) << 4) + } +} + +/// `VADecPictureParameterBufferAV1::qmatrix_fields` — **16 bits**, not 32. +/// +/// Unlike DXVA, libva carries `using_qmatrix` itself, so the three indices need no +/// `0xFF` sentinel: they are simply ignored when the flag is clear. (`DXVA_PicParams_AV1` +/// has no such flag, which is why `pf_dxvadec::pic_av1` has to send 0xFF and why +/// leaving the parser's 0 there dequantised against matrix 0 on every frame.) +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct QmatrixFieldsAV1 { + pub using_qmatrix: bool, + pub qm_y: u8, + pub qm_u: u8, + pub qm_v: u8, +} + +impl QmatrixFieldsAV1 { + pub const fn pack(self) -> u16 { + (self.using_qmatrix as u16) + | ((self.qm_y as u16 & 0xf) << 1) + | ((self.qm_u as u16 & 0xf) << 5) + | ((self.qm_v as u16 & 0xf) << 9) + } +} + +/// `VADecPictureParameterBufferAV1::mode_control_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct ModeControlFieldsAV1 { + pub delta_q_present_flag: bool, + pub log2_delta_q_res: u8, + pub delta_lf_present_flag: bool, + pub log2_delta_lf_res: u8, + pub delta_lf_multi: bool, + /// 0 ONLY_4X4, 1 LARGEST, 2 SELECT. + pub tx_mode: u8, + pub reference_select: bool, + pub reduced_tx_set_used: bool, + pub skip_mode_present: bool, +} + +impl ModeControlFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.delta_q_present_flag as u32) + | ((self.log2_delta_q_res as u32 & 0x3) << 1) + | ((self.delta_lf_present_flag as u32) << 3) + | ((self.log2_delta_lf_res as u32 & 0x3) << 4) + | ((self.delta_lf_multi as u32) << 6) + | ((self.tx_mode as u32 & 0x3) << 7) + | ((self.reference_select as u32) << 9) + | ((self.reduced_tx_set_used as u32) << 10) + | ((self.skip_mode_present as u32) << 11) + } +} + +/// `VADecPictureParameterBufferAV1::loop_restoration_fields` — **16 bits**, not 32. +/// +/// The three `*frame_restoration_type` fields take the SPEC's `FrameRestorationType` +/// (`RESTORE_NONE` 0, `RESTORE_WIENER` 1, `RESTORE_SGRPROJ` 2, `RESTORE_SWITCHABLE` +/// 3), not the coded two-bit `lr_type`. libavcodec sends +/// `remap_lr_type[frame_header->lr_type[i]]` with +/// `remap_lr_type = {NONE, SWITCHABLE, WIENER, SGRPROJ}` — i.e. it applies AV1 +/// 5.9.20's `Remap_Lr_Type` mapping — and the vendored parser has already applied it +/// (`LoopRestorationParams::frame_restoration_type` is documented "Same as +/// FrameRestorationType in the specification"), so the conversion casts the parser's +/// enum and remaps nothing. Sending the coded value instead swaps WIENER and +/// SWITCHABLE on every frame that restores. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct LoopRestorationFieldsAV1 { + pub yframe_restoration_type: u8, + pub cbframe_restoration_type: u8, + pub crframe_restoration_type: u8, + pub lr_unit_shift: u8, + pub lr_uv_shift: u8, +} + +impl LoopRestorationFieldsAV1 { + pub const fn pack(self) -> u16 { + (self.yframe_restoration_type as u16 & 0x3) + | ((self.cbframe_restoration_type as u16 & 0x3) << 2) + | ((self.crframe_restoration_type as u16 & 0x3) << 4) + | ((self.lr_unit_shift as u16 & 0x3) << 6) + | ((self.lr_uv_shift as u16 & 0x1) << 8) + } +} + +/// `VASegmentationStructAV1::segment_info_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct SegmentInfoFieldsAV1 { + pub enabled: bool, + pub update_map: bool, + pub temporal_update: bool, + pub update_data: bool, +} + +impl SegmentInfoFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.enabled as u32) + | ((self.update_map as u32) << 1) + | ((self.temporal_update as u32) << 2) + | ((self.update_data as u32) << 3) + } +} + +/// `VAFilmGrainStructAV1::film_grain_info_fields` (32 bits). +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub struct FilmGrainInfoFieldsAV1 { + pub apply_grain: bool, + pub chroma_scaling_from_luma: bool, + pub grain_scaling_minus_8: u8, + pub ar_coeff_lag: u8, + pub ar_coeff_shift_minus_6: u8, + pub grain_scale_shift: u8, + pub overlap_flag: bool, + pub clip_to_restricted_range: bool, +} + +impl FilmGrainInfoFieldsAV1 { + pub const fn pack(self) -> u32 { + (self.apply_grain as u32) + | ((self.chroma_scaling_from_luma as u32) << 1) + | ((self.grain_scaling_minus_8 as u32 & 0x3) << 2) + | ((self.ar_coeff_lag as u32 & 0x3) << 4) + | ((self.ar_coeff_shift_minus_6 as u32 & 0x3) << 6) + | ((self.grain_scale_shift as u32 & 0x3) << 8) + | ((self.overlap_flag as u32) << 10) + | ((self.clip_to_restricted_range as u32) << 11) + } +} + +// --------------------------------------------------------------------------- +// The structures +// --------------------------------------------------------------------------- + +/// `VASegmentationStructAV1`. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaSegmentationStructAV1 { + pub segment_info_fields: u32, + /// `FeatureData[segment][feature]` **after** AV1 5.9.14's `Clip3` — libva says + /// so ("equivalent to variable FeatureData\[\]\[\] in spec, which is after + /// clip3() operation"), and the vendored parser clips as it reads + /// (`parse_segmentation_params` calls `helpers::clip3` with the spec's + /// `FEATURE_MAX`), so no clipping happens in the conversion. + pub feature_data: [[i16; 8]; 8], + /// Bit `feature` set where `feature_enabled[segment][feature]` is. Indexed by + /// SEGMENT; the bit position is the feature id. + pub feature_mask: [u8; 8], + pub va_reserved: [u32; 4], +} + +impl VaSegmentationStructAV1 { + pub const fn zeroed() -> Self { + VaSegmentationStructAV1 { + segment_info_fields: 0, + feature_data: [[0; 8]; 8], + feature_mask: [0; 8], + va_reserved: [0; 4], + } + } +} + +/// `VAFilmGrainStructAV1`. +/// +/// ⚠ The `ar_coeffs_*` are **signed** here (`int8_t`), where the bitstream — and the +/// vendored parser, and `DXVA_FilmGrain_AV1` — carry the `+128` biased form. +/// libavcodec writes `film_grain->ar_coeffs_y_plus_128[i] - 128`. Copying the biased +/// bytes across unchanged is a silent 128-offset on every autoregressive coefficient. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaFilmGrainStructAV1 { + pub film_grain_info_fields: u32, + pub grain_seed: u16, + pub num_y_points: u8, + pub point_y_value: [u8; 14], + pub point_y_scaling: [u8; 14], + 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 ar_coeffs_y: [i8; 24], + pub ar_coeffs_cb: [i8; 25], + pub ar_coeffs_cr: [i8; 25], + 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 va_reserved: [u32; 4], +} + +impl VaFilmGrainStructAV1 { + pub const fn zeroed() -> Self { + VaFilmGrainStructAV1 { + film_grain_info_fields: 0, + grain_seed: 0, + num_y_points: 0, + point_y_value: [0; 14], + point_y_scaling: [0; 14], + 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], + ar_coeffs_y: [0; 24], + ar_coeffs_cb: [0; 25], + ar_coeffs_cr: [0; 25], + cb_mult: 0, + cb_luma_mult: 0, + cb_offset: 0, + cr_mult: 0, + cr_luma_mult: 0, + cr_offset: 0, + va_reserved: [0; 4], + } + } +} + +/// `VAWarpedMotionParamsAV1` — one reference NAME's global motion. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaWarpedMotionParamsAV1 { + /// `VAAV1TransformationType`. Declared `u32` because a C enum whose enumerators + /// are all non-negative is `unsigned int` on this ABI; either signedness is four + /// bytes and every value written here is 0..=3, so the choice is a naming one. + pub wmtype: u32, + /// `gm_params[ref][0..6]`. ⚠ Only the first SIX are meaningful: AV1 5.9.24 codes + /// six warp parameters and libavcodec copies `for (j = 0; j < 6; j++)`, leaving + /// `wmmat[6]`/`wmmat[7]` zero. + pub wmmat: [i32; 8], + /// The INVERSE of the parser's `warp_valid` (`setup_shear`'s verdict): libva's + /// field says the affine set is unusable. + pub invalid: u8, + pub va_reserved: [u32; 4], +} + +impl VaWarpedMotionParamsAV1 { + pub const fn zeroed() -> Self { + VaWarpedMotionParamsAV1 { + wmtype: VA_AV1_TRANSFORMATION_IDENTITY, + wmmat: [0; 8], + invalid: 0, + va_reserved: [0; 4], + } + } +} + +/// `VADecPictureParameterBufferAV1`. +/// +/// ⚠ **Eight-byte aligned, 1160 bytes**, and the reason is +/// [`Self::anchor_frames_list`]: a pointer member drags the whole structure's +/// alignment up and inserts seven bytes of padding after `anchor_frames_num` that +/// nothing in the field list suggests. Measured, not counted. +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaDecPictureParameterBufferAV1 { + /// `seq_profile`: 0, 1 or 2. + pub profile: u8, + /// ⚠ The parser types this `i32` and leaves it **-1** when `enable_order_hint` + /// is 0. Narrowed to a `u8` that would be 255 — a 256-bit order hint — so the + /// conversion sends 0 there instead. + pub order_hint_bits_minus_1: u8, + /// 0 = 8-bit, 1 = 10-bit, 2 = 12-bit. An INDEX, not a depth. + pub bit_depth_idx: u8, + pub matrix_coefficients: u8, + pub seq_info_fields: u32, + /// The decode target's `VASurfaceID`. + pub current_frame: u32, + /// The surface the film-grained picture is written to. libva: *"Valid only when + /// apply_grain equals 1."* This rung refuses `apply_grain` (see + /// [`crate::pic_av1`]), so it always equals [`Self::current_frame`]. + pub current_display_picture: u32, + /// Large-scale tile only; always 0 here. + pub anchor_frames_num: u8, + /// Large-scale tile only; always null here. Declared as a real pointer so the + /// layout follows the target ABI rather than a hard-coded width — which also + /// means the offsets pinned below are the LP64 ones, as everywhere else in this + /// crate. + pub anchor_frames_list: *mut u32, + /// ⚠ The **upscaled** (post-superres) width minus one — libva: *"Picture + /// original resolution. If SuperRes is enabled, this is the upscaled + /// resolution."* libavcodec sends the coded `frame_width_minus_1` syntax + /// element, which is that same quantity: AV1 5.9.8 reads it into + /// `UpscaledWidth` and only then divides down into `FrameWidth`. + pub frame_width_minus1: u16, + pub frame_height_minus1: u16, + /// Large-scale tile only. + pub output_frame_width_in_tiles_minus_1: u16, + pub output_frame_height_in_tiles_minus_1: u16, + /// Indexed by AV1 reference **SLOT**, holding a **`VASurfaceID`** (module docs). + /// `VA_INVALID_ID` for a slot holding nothing — which is the DEFAULT this + /// structure zeroes to, not what a submission carries: `pic_av1` substitutes a live + /// surface for every empty entry before the buffer reaches a driver, because + /// `va_dec_av1.h:352` says the driver will not check the ids and prescribes exactly + /// that recovery. + pub ref_frame_map: [u32; REF_FRAME_MAP_LEN], + /// Indexed by reference **NAME**, holding an index into [`Self::ref_frame_map`] + /// — i.e. an AV1 slot (module docs). + pub ref_frame_idx: [u8; REFS_PER_FRAME], + /// Index into [`Self::ref_frame_idx`], or [`PRIMARY_REF_NONE`]. + pub primary_ref_frame: u8, + /// ⚠ A `u8`, where AV1 allows up to 8 order-hint bits — so the full range fits, + /// but only just. + pub order_hint: u8, + pub seg_info: VaSegmentationStructAV1, + pub film_grain_info: VaFilmGrainStructAV1, + pub tile_cols: u8, + pub tile_rows: u8, + /// Each tile's width in superblocks MINUS ONE — the coded syntax element, not a + /// count. (DXVA's `tiles.widths[]` is the count, `+1`; the two APIs disagree and + /// both are documented.) + pub width_in_sbs_minus_1: [u16; TILE_SBS_LEN], + pub height_in_sbs_minus_1: [u16; TILE_SBS_LEN], + /// Large-scale tile only. + pub tile_count_minus_1: u16, + pub context_update_tile_id: u16, + pub pic_info_fields: u32, + pub superres_scale_denominator: u8, + pub interp_filter: u8, + /// `loop_filter_level[0..2]` — the two LUMA levels. + pub filter_level: [u8; 2], + /// `loop_filter_level[2]`. + pub filter_level_u: u8, + /// `loop_filter_level[3]`. + pub filter_level_v: u8, + /// An **8-bit** union ([`LoopFilterInfoFieldsAV1`]). + pub loop_filter_info_fields: u8, + pub ref_deltas: [i8; TOTAL_REFS_PER_FRAME], + pub mode_deltas: [i8; 2], + pub base_qindex: u8, + pub y_dc_delta_q: i8, + pub u_dc_delta_q: i8, + pub u_ac_delta_q: i8, + pub v_dc_delta_q: i8, + pub v_ac_delta_q: i8, + /// A **16-bit** union ([`QmatrixFieldsAV1`]). + pub qmatrix_fields: u16, + pub mode_control_fields: u32, + pub cdef_damping_minus_3: u8, + pub cdef_bits: u8, + /// `(primary << 2) | (secondary & 3)`, per the header's own formula. The + /// secondary strength must be the CODED two-bit value — see + /// [`pf_bitstream::av1::coded_cdef_sec_strength`]. + pub cdef_y_strengths: [u8; CDEF_MAX], + pub cdef_uv_strengths: [u8; CDEF_MAX], + /// A **16-bit** union ([`LoopRestorationFieldsAV1`]). + pub loop_restoration_fields: u16, + /// Global motion by reference NAME: `wm[0]` is `LAST_FRAME` (module docs). + pub wm: [VaWarpedMotionParamsAV1; REFS_PER_FRAME], + /// `va_reserved[VA_PADDING_MEDIUM]` — eight, where the other two codecs' picture + /// buffers use `VA_PADDING_MEDIUM` and `VA_PADDING_LOW` respectively. + pub va_reserved: [u32; 8], +} + +impl VaDecPictureParameterBufferAV1 { + /// An all-zero buffer with the sentinels a driver must not read as real values: + /// every reference slot empty, and no anchor-frame list. + pub const fn zeroed() -> Self { + VaDecPictureParameterBufferAV1 { + profile: 0, + order_hint_bits_minus_1: 0, + bit_depth_idx: 0, + matrix_coefficients: 0, + seq_info_fields: 0, + current_frame: crate::va::VA_INVALID_SURFACE, + current_display_picture: crate::va::VA_INVALID_SURFACE, + anchor_frames_num: 0, + anchor_frames_list: std::ptr::null_mut(), + frame_width_minus1: 0, + frame_height_minus1: 0, + output_frame_width_in_tiles_minus_1: 0, + output_frame_height_in_tiles_minus_1: 0, + ref_frame_map: [crate::va::VA_INVALID_SURFACE; REF_FRAME_MAP_LEN], + ref_frame_idx: [0; REFS_PER_FRAME], + primary_ref_frame: PRIMARY_REF_NONE, + order_hint: 0, + seg_info: VaSegmentationStructAV1::zeroed(), + film_grain_info: VaFilmGrainStructAV1::zeroed(), + tile_cols: 0, + tile_rows: 0, + width_in_sbs_minus_1: [0; TILE_SBS_LEN], + height_in_sbs_minus_1: [0; TILE_SBS_LEN], + tile_count_minus_1: 0, + context_update_tile_id: 0, + pic_info_fields: 0, + superres_scale_denominator: SUPERRES_NUM, + interp_filter: 0, + filter_level: [0; 2], + filter_level_u: 0, + filter_level_v: 0, + loop_filter_info_fields: 0, + ref_deltas: [0; TOTAL_REFS_PER_FRAME], + mode_deltas: [0; 2], + base_qindex: 0, + y_dc_delta_q: 0, + u_dc_delta_q: 0, + u_ac_delta_q: 0, + v_dc_delta_q: 0, + v_ac_delta_q: 0, + qmatrix_fields: 0, + mode_control_fields: 0, + cdef_damping_minus_3: 0, + cdef_bits: 0, + cdef_y_strengths: [0; CDEF_MAX], + cdef_uv_strengths: [0; CDEF_MAX], + loop_restoration_fields: 0, + wm: [VaWarpedMotionParamsAV1::zeroed(); REFS_PER_FRAME], + va_reserved: [0; 8], + } + } +} + +/// `VASliceParameterBufferAV1` — **one per TILE**, not per tile group (module docs). +#[repr(C)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VaSliceParameterBufferAV1 { + /// This tile's byte count. + pub slice_data_size: u32, + /// This tile's offset **inside the accompanying `VASliceDataBufferType` buffer** + /// — which holds the whole tile group's `tile_data` region, not just this tile. + pub slice_data_offset: u32, + pub slice_data_flag: u32, + pub tile_row: u16, + pub tile_column: u16, + /// `va_deprecated` in the header — and libavcodec fills both anyway, so this + /// rung does too. A deprecated field a driver may still read is not a field to + /// leave at whatever `zeroed()` chose. + pub tg_start: u16, + pub tg_end: u16, + pub anchor_frame_idx: u8, + pub tile_idx_in_tile_list: u16, + pub va_reserved: [u32; 4], +} + +impl VaSliceParameterBufferAV1 { + pub const fn zeroed() -> Self { + VaSliceParameterBufferAV1 { + slice_data_size: 0, + slice_data_offset: 0, + slice_data_flag: crate::va::VA_SLICE_DATA_FLAG_ALL, + tile_row: 0, + tile_column: 0, + tg_start: 0, + tg_end: 0, + anchor_frame_idx: ANCHOR_FRAME_UNUSED, + tile_idx_in_tile_list: 0, + va_reserved: [0; 4], + } + } +} + +// --------------------------------------------------------------------------- +// Layout proofs — the probe's output, pinned (libva 2.23.0, x86_64-linux-gnu). +// --------------------------------------------------------------------------- + +const _: () = { + use std::mem::align_of; + use std::mem::offset_of; + use std::mem::size_of; + + assert!(size_of::() == 156); + assert!(offset_of!(VaSegmentationStructAV1, segment_info_fields) == 0); + assert!(offset_of!(VaSegmentationStructAV1, feature_data) == 4); + assert!(offset_of!(VaSegmentationStructAV1, feature_mask) == 132); + assert!(offset_of!(VaSegmentationStructAV1, va_reserved) == 140); + + assert!(size_of::() == 176); + assert!(offset_of!(VaFilmGrainStructAV1, film_grain_info_fields) == 0); + assert!(offset_of!(VaFilmGrainStructAV1, grain_seed) == 4); + assert!(offset_of!(VaFilmGrainStructAV1, num_y_points) == 6); + assert!(offset_of!(VaFilmGrainStructAV1, point_y_value) == 7); + assert!(offset_of!(VaFilmGrainStructAV1, point_y_scaling) == 21); + assert!(offset_of!(VaFilmGrainStructAV1, num_cb_points) == 35); + assert!(offset_of!(VaFilmGrainStructAV1, point_cb_value) == 36); + assert!(offset_of!(VaFilmGrainStructAV1, point_cb_scaling) == 46); + assert!(offset_of!(VaFilmGrainStructAV1, num_cr_points) == 56); + assert!(offset_of!(VaFilmGrainStructAV1, point_cr_value) == 57); + assert!(offset_of!(VaFilmGrainStructAV1, point_cr_scaling) == 67); + assert!(offset_of!(VaFilmGrainStructAV1, ar_coeffs_y) == 77); + assert!(offset_of!(VaFilmGrainStructAV1, ar_coeffs_cb) == 101); + assert!(offset_of!(VaFilmGrainStructAV1, ar_coeffs_cr) == 126); + assert!(offset_of!(VaFilmGrainStructAV1, cb_mult) == 151); + assert!(offset_of!(VaFilmGrainStructAV1, cb_luma_mult) == 152); + assert!(offset_of!(VaFilmGrainStructAV1, cb_offset) == 154); + assert!(offset_of!(VaFilmGrainStructAV1, cr_mult) == 156); + assert!(offset_of!(VaFilmGrainStructAV1, cr_luma_mult) == 157); + assert!(offset_of!(VaFilmGrainStructAV1, cr_offset) == 158); + assert!(offset_of!(VaFilmGrainStructAV1, va_reserved) == 160); + + assert!(size_of::() == 56); + assert!(offset_of!(VaWarpedMotionParamsAV1, wmtype) == 0); + assert!(offset_of!(VaWarpedMotionParamsAV1, wmmat) == 4); + assert!(offset_of!(VaWarpedMotionParamsAV1, invalid) == 36); + assert!(offset_of!(VaWarpedMotionParamsAV1, va_reserved) == 40); + + // The pointer member is what makes this one align 8 rather than 4, and it is + // asserted for its own sake: the padding it creates at offsets 17..24 is the + // kind a hand-written declaration silently omits. + assert!(size_of::() == 1160); + assert!(align_of::() == 8); + assert!(offset_of!(VaDecPictureParameterBufferAV1, profile) == 0); + assert!(offset_of!(VaDecPictureParameterBufferAV1, order_hint_bits_minus_1) == 1); + assert!(offset_of!(VaDecPictureParameterBufferAV1, bit_depth_idx) == 2); + assert!(offset_of!(VaDecPictureParameterBufferAV1, matrix_coefficients) == 3); + assert!(offset_of!(VaDecPictureParameterBufferAV1, seq_info_fields) == 4); + assert!(offset_of!(VaDecPictureParameterBufferAV1, current_frame) == 8); + assert!(offset_of!(VaDecPictureParameterBufferAV1, current_display_picture) == 12); + assert!(offset_of!(VaDecPictureParameterBufferAV1, anchor_frames_num) == 16); + assert!(offset_of!(VaDecPictureParameterBufferAV1, anchor_frames_list) == 24); + assert!(offset_of!(VaDecPictureParameterBufferAV1, frame_width_minus1) == 32); + assert!(offset_of!(VaDecPictureParameterBufferAV1, frame_height_minus1) == 34); + assert!( + offset_of!( + VaDecPictureParameterBufferAV1, + output_frame_width_in_tiles_minus_1 + ) == 36 + ); + assert!( + offset_of!( + VaDecPictureParameterBufferAV1, + output_frame_height_in_tiles_minus_1 + ) == 38 + ); + assert!(offset_of!(VaDecPictureParameterBufferAV1, ref_frame_map) == 40); + assert!(offset_of!(VaDecPictureParameterBufferAV1, ref_frame_idx) == 72); + assert!(offset_of!(VaDecPictureParameterBufferAV1, primary_ref_frame) == 79); + assert!(offset_of!(VaDecPictureParameterBufferAV1, order_hint) == 80); + assert!(offset_of!(VaDecPictureParameterBufferAV1, seg_info) == 84); + assert!(offset_of!(VaDecPictureParameterBufferAV1, film_grain_info) == 240); + assert!(offset_of!(VaDecPictureParameterBufferAV1, tile_cols) == 416); + assert!(offset_of!(VaDecPictureParameterBufferAV1, tile_rows) == 417); + assert!(offset_of!(VaDecPictureParameterBufferAV1, width_in_sbs_minus_1) == 418); + assert!(offset_of!(VaDecPictureParameterBufferAV1, height_in_sbs_minus_1) == 544); + assert!(offset_of!(VaDecPictureParameterBufferAV1, tile_count_minus_1) == 670); + assert!(offset_of!(VaDecPictureParameterBufferAV1, context_update_tile_id) == 672); + assert!(offset_of!(VaDecPictureParameterBufferAV1, pic_info_fields) == 676); + assert!(offset_of!(VaDecPictureParameterBufferAV1, superres_scale_denominator) == 680); + assert!(offset_of!(VaDecPictureParameterBufferAV1, interp_filter) == 681); + assert!(offset_of!(VaDecPictureParameterBufferAV1, filter_level) == 682); + assert!(offset_of!(VaDecPictureParameterBufferAV1, filter_level_u) == 684); + assert!(offset_of!(VaDecPictureParameterBufferAV1, filter_level_v) == 685); + assert!(offset_of!(VaDecPictureParameterBufferAV1, loop_filter_info_fields) == 686); + assert!(offset_of!(VaDecPictureParameterBufferAV1, ref_deltas) == 687); + assert!(offset_of!(VaDecPictureParameterBufferAV1, mode_deltas) == 695); + assert!(offset_of!(VaDecPictureParameterBufferAV1, base_qindex) == 697); + assert!(offset_of!(VaDecPictureParameterBufferAV1, y_dc_delta_q) == 698); + assert!(offset_of!(VaDecPictureParameterBufferAV1, u_dc_delta_q) == 699); + assert!(offset_of!(VaDecPictureParameterBufferAV1, u_ac_delta_q) == 700); + assert!(offset_of!(VaDecPictureParameterBufferAV1, v_dc_delta_q) == 701); + assert!(offset_of!(VaDecPictureParameterBufferAV1, v_ac_delta_q) == 702); + assert!(offset_of!(VaDecPictureParameterBufferAV1, qmatrix_fields) == 704); + assert!(offset_of!(VaDecPictureParameterBufferAV1, mode_control_fields) == 708); + assert!(offset_of!(VaDecPictureParameterBufferAV1, cdef_damping_minus_3) == 712); + assert!(offset_of!(VaDecPictureParameterBufferAV1, cdef_bits) == 713); + assert!(offset_of!(VaDecPictureParameterBufferAV1, cdef_y_strengths) == 714); + assert!(offset_of!(VaDecPictureParameterBufferAV1, cdef_uv_strengths) == 722); + assert!(offset_of!(VaDecPictureParameterBufferAV1, loop_restoration_fields) == 730); + assert!(offset_of!(VaDecPictureParameterBufferAV1, wm) == 732); + assert!(offset_of!(VaDecPictureParameterBufferAV1, va_reserved) == 1124); + + assert!(size_of::() == 40); + assert!(offset_of!(VaSliceParameterBufferAV1, slice_data_size) == 0); + assert!(offset_of!(VaSliceParameterBufferAV1, slice_data_offset) == 4); + assert!(offset_of!(VaSliceParameterBufferAV1, slice_data_flag) == 8); + assert!(offset_of!(VaSliceParameterBufferAV1, tile_row) == 12); + assert!(offset_of!(VaSliceParameterBufferAV1, tile_column) == 14); + assert!(offset_of!(VaSliceParameterBufferAV1, tg_start) == 16); + assert!(offset_of!(VaSliceParameterBufferAV1, tg_end) == 18); + assert!(offset_of!(VaSliceParameterBufferAV1, anchor_frame_idx) == 20); + assert!(offset_of!(VaSliceParameterBufferAV1, tile_idx_in_tile_list) == 22); + assert!(offset_of!(VaSliceParameterBufferAV1, va_reserved) == 24); +}; + +#[cfg(test)] +mod tests { + use super::*; + + /// The probe's own single-field vectors, restated. Each of these is a number a + /// real `gcc` printed after setting exactly one bit-field. + #[test] + fn av1_bit_fields_pack_where_the_probe_measured() { + assert_eq!( + SeqInfoFieldsAV1 { + still_picture: true, + ..Default::default() + } + .pack(), + 0x0000_0001 + ); + assert_eq!( + SeqInfoFieldsAV1 { + mono_chrome: true, + ..Default::default() + } + .pack(), + 0x0000_0400 + ); + assert_eq!( + SeqInfoFieldsAV1 { + film_grain_params_present: true, + ..Default::default() + } + .pack(), + 0x0000_8000 + ); + assert_eq!( + PicInfoFieldsAV1 { + frame_type: 3, + ..Default::default() + } + .pack(), + 0x0000_0003 + ); + assert_eq!( + PicInfoFieldsAV1 { + use_ref_frame_mvs: true, + ..Default::default() + } + .pack(), + 0x0000_1000 + ); + assert_eq!( + PicInfoFieldsAV1 { + large_scale_tile: true, + ..Default::default() + } + .pack(), + 0x0001_0000 + ); + assert_eq!( + LoopFilterInfoFieldsAV1 { + sharpness_level: 7, + ..Default::default() + } + .pack(), + 0x07 + ); + assert_eq!( + LoopFilterInfoFieldsAV1 { + mode_ref_delta_update: true, + ..Default::default() + } + .pack(), + 0x10 + ); + assert_eq!( + QmatrixFieldsAV1 { + using_qmatrix: true, + ..Default::default() + } + .pack(), + 0x0001 + ); + assert_eq!( + QmatrixFieldsAV1 { + qm_v: 0xf, + ..Default::default() + } + .pack(), + 0x1e00 + ); + assert_eq!( + ModeControlFieldsAV1 { + delta_q_present_flag: true, + ..Default::default() + } + .pack(), + 0x0000_0001 + ); + assert_eq!( + ModeControlFieldsAV1 { + tx_mode: 3, + ..Default::default() + } + .pack(), + 0x0000_0180 + ); + assert_eq!( + ModeControlFieldsAV1 { + skip_mode_present: true, + ..Default::default() + } + .pack(), + 0x0000_0800 + ); + assert_eq!( + LoopRestorationFieldsAV1 { + yframe_restoration_type: 3, + ..Default::default() + } + .pack(), + 0x0003 + ); + assert_eq!( + LoopRestorationFieldsAV1 { + lr_uv_shift: 1, + ..Default::default() + } + .pack(), + 0x0100 + ); + assert_eq!( + SegmentInfoFieldsAV1 { + enabled: true, + ..Default::default() + } + .pack(), + 0x0000_0001 + ); + assert_eq!( + SegmentInfoFieldsAV1 { + update_data: true, + ..Default::default() + } + .pack(), + 0x0000_0008 + ); + assert_eq!( + FilmGrainInfoFieldsAV1 { + apply_grain: true, + ..Default::default() + } + .pack(), + 0x0000_0001 + ); + assert_eq!( + FilmGrainInfoFieldsAV1 { + grain_scale_shift: 3, + ..Default::default() + } + .pack(), + 0x0000_0300 + ); + assert_eq!( + FilmGrainInfoFieldsAV1 { + clip_to_restricted_range: true, + ..Default::default() + } + .pack(), + 0x0000_0800 + ); + } + + /// Every field alone must light only its own bits, and nothing may reach the + /// reserved tail. Two probe vectors per word would not catch a shift typo that + /// overlapped two neighbours — and on the three NARROW unions, a field that + /// overflowed its declared width would be invisible in a `u32` comparison, which + /// is why each of those is checked against its own type's mask. + #[test] + fn every_av1_field_owns_a_distinct_bit_range() { + // A free function rather than a closure so `seen` can be reset between the + // unions without the closure's borrow outliving it. + fn check(seen: &mut u32, bits: u32, mask: u32) { + assert_ne!(bits, 0, "a field packed to nothing"); + assert_eq!(*seen & bits, 0, "two fields share a bit: {bits:#010x}"); + assert_eq!(bits & !mask, 0, "a field reached the reserved tail"); + *seen |= bits; + } + + let mut seen = 0u32; + const SEQ_MASK: u32 = 0x0000_ffff; + check( + &mut seen, + SeqInfoFieldsAV1 { + chroma_sample_position: 1, + ..Default::default() + } + .pack(), + SEQ_MASK, + ); + for set in [ + |f: &mut SeqInfoFieldsAV1| f.still_picture = true, + |f: &mut SeqInfoFieldsAV1| f.use_128x128_superblock = true, + |f: &mut SeqInfoFieldsAV1| f.enable_filter_intra = true, + |f: &mut SeqInfoFieldsAV1| f.enable_intra_edge_filter = true, + |f: &mut SeqInfoFieldsAV1| f.enable_interintra_compound = true, + |f: &mut SeqInfoFieldsAV1| f.enable_masked_compound = true, + |f: &mut SeqInfoFieldsAV1| f.enable_dual_filter = true, + |f: &mut SeqInfoFieldsAV1| f.enable_order_hint = true, + |f: &mut SeqInfoFieldsAV1| f.enable_jnt_comp = true, + |f: &mut SeqInfoFieldsAV1| f.enable_cdef = true, + |f: &mut SeqInfoFieldsAV1| f.mono_chrome = true, + |f: &mut SeqInfoFieldsAV1| f.color_range = true, + |f: &mut SeqInfoFieldsAV1| f.subsampling_x = true, + |f: &mut SeqInfoFieldsAV1| f.subsampling_y = true, + |f: &mut SeqInfoFieldsAV1| f.film_grain_params_present = true, + ] { + let mut f = SeqInfoFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), SEQ_MASK); + } + assert_eq!(seen, SEQ_MASK, "all 16 seq_info bits accounted for"); + + seen = 0; + const PIC_MASK: u32 = 0x0001_ffff; + check( + &mut seen, + PicInfoFieldsAV1 { + frame_type: 3, + ..Default::default() + } + .pack(), + PIC_MASK, + ); + for set in [ + |f: &mut PicInfoFieldsAV1| f.show_frame = true, + |f: &mut PicInfoFieldsAV1| f.showable_frame = true, + |f: &mut PicInfoFieldsAV1| f.error_resilient_mode = true, + |f: &mut PicInfoFieldsAV1| f.disable_cdf_update = true, + |f: &mut PicInfoFieldsAV1| f.allow_screen_content_tools = true, + |f: &mut PicInfoFieldsAV1| f.force_integer_mv = true, + |f: &mut PicInfoFieldsAV1| f.allow_intrabc = true, + |f: &mut PicInfoFieldsAV1| f.use_superres = true, + |f: &mut PicInfoFieldsAV1| f.allow_high_precision_mv = true, + |f: &mut PicInfoFieldsAV1| f.is_motion_mode_switchable = true, + |f: &mut PicInfoFieldsAV1| f.use_ref_frame_mvs = true, + |f: &mut PicInfoFieldsAV1| f.disable_frame_end_update_cdf = true, + |f: &mut PicInfoFieldsAV1| f.uniform_tile_spacing_flag = true, + |f: &mut PicInfoFieldsAV1| f.allow_warped_motion = true, + |f: &mut PicInfoFieldsAV1| f.large_scale_tile = true, + ] { + let mut f = PicInfoFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), PIC_MASK); + } + assert_eq!(seen, PIC_MASK, "all 17 pic_info bits accounted for"); + + seen = 0; + const MODE_MASK: u32 = 0x0000_0fff; + for set in [ + |f: &mut ModeControlFieldsAV1| f.delta_q_present_flag = true, + |f: &mut ModeControlFieldsAV1| f.log2_delta_q_res = 3, + |f: &mut ModeControlFieldsAV1| f.delta_lf_present_flag = true, + |f: &mut ModeControlFieldsAV1| f.log2_delta_lf_res = 3, + |f: &mut ModeControlFieldsAV1| f.delta_lf_multi = true, + |f: &mut ModeControlFieldsAV1| f.tx_mode = 3, + |f: &mut ModeControlFieldsAV1| f.reference_select = true, + |f: &mut ModeControlFieldsAV1| f.reduced_tx_set_used = true, + |f: &mut ModeControlFieldsAV1| f.skip_mode_present = true, + ] { + let mut f = ModeControlFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), MODE_MASK); + } + assert_eq!(seen, MODE_MASK); + + seen = 0; + const FG_MASK: u32 = 0x0000_0fff; + for set in [ + |f: &mut FilmGrainInfoFieldsAV1| f.apply_grain = true, + |f: &mut FilmGrainInfoFieldsAV1| f.chroma_scaling_from_luma = true, + |f: &mut FilmGrainInfoFieldsAV1| f.grain_scaling_minus_8 = 3, + |f: &mut FilmGrainInfoFieldsAV1| f.ar_coeff_lag = 3, + |f: &mut FilmGrainInfoFieldsAV1| f.ar_coeff_shift_minus_6 = 3, + |f: &mut FilmGrainInfoFieldsAV1| f.grain_scale_shift = 3, + |f: &mut FilmGrainInfoFieldsAV1| f.overlap_flag = true, + |f: &mut FilmGrainInfoFieldsAV1| f.clip_to_restricted_range = true, + ] { + let mut f = FilmGrainInfoFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), FG_MASK); + } + assert_eq!(seen, FG_MASK); + + seen = 0; + for set in [ + |f: &mut SegmentInfoFieldsAV1| f.enabled = true, + |f: &mut SegmentInfoFieldsAV1| f.update_map = true, + |f: &mut SegmentInfoFieldsAV1| f.temporal_update = true, + |f: &mut SegmentInfoFieldsAV1| f.update_data = true, + ] { + let mut f = SegmentInfoFieldsAV1::default(); + set(&mut f); + check(&mut seen, f.pack(), 0x0000_000f); + } + assert_eq!(seen, 0x0000_000f); + + // The three NARROW unions, checked in their own widths. + let mut seen8 = 0u8; + for bits in [ + LoopFilterInfoFieldsAV1 { + sharpness_level: 7, + ..Default::default() + } + .pack(), + LoopFilterInfoFieldsAV1 { + mode_ref_delta_enabled: true, + ..Default::default() + } + .pack(), + LoopFilterInfoFieldsAV1 { + mode_ref_delta_update: true, + ..Default::default() + } + .pack(), + ] { + assert_ne!(bits, 0); + assert_eq!(seen8 & bits, 0); + seen8 |= bits; + } + assert_eq!(seen8, 0x1f, "five bits, and nothing in the reserved three"); + + let mut seen16 = 0u16; + for bits in [ + QmatrixFieldsAV1 { + using_qmatrix: true, + ..Default::default() + } + .pack(), + QmatrixFieldsAV1 { + qm_y: 0xf, + ..Default::default() + } + .pack(), + QmatrixFieldsAV1 { + qm_u: 0xf, + ..Default::default() + } + .pack(), + QmatrixFieldsAV1 { + qm_v: 0xf, + ..Default::default() + } + .pack(), + ] { + assert_ne!(bits, 0); + assert_eq!(seen16 & bits, 0); + seen16 |= bits; + } + assert_eq!(seen16, 0x1fff, "13 bits, and nothing in the reserved three"); + + seen16 = 0; + for bits in [ + LoopRestorationFieldsAV1 { + yframe_restoration_type: 3, + ..Default::default() + } + .pack(), + LoopRestorationFieldsAV1 { + cbframe_restoration_type: 3, + ..Default::default() + } + .pack(), + LoopRestorationFieldsAV1 { + crframe_restoration_type: 3, + ..Default::default() + } + .pack(), + LoopRestorationFieldsAV1 { + lr_unit_shift: 3, + ..Default::default() + } + .pack(), + LoopRestorationFieldsAV1 { + lr_uv_shift: 1, + ..Default::default() + } + .pack(), + ] { + assert_ne!(bits, 0); + assert_eq!(seen16 & bits, 0); + seen16 |= bits; + } + assert_eq!( + seen16, 0x01ff, + "nine bits, and nothing in the reserved seven" + ); + } + + /// The sentinels a zeroed picture buffer must NOT leave as plausible values. + /// + /// `ref_frame_map` is the one that matters: a zero there is a perfectly valid + /// `VASurfaceID`, and `va_dec_av1.h` says outright that the *"Driver is not + /// responsible to validate reference frames' id"* — so an unfilled slot has to + /// carry `VA_INVALID_ID` or the driver predicts from surface 0. + #[test] + fn a_zeroed_picture_buffer_carries_the_sentinels_not_zeros() { + let p = VaDecPictureParameterBufferAV1::zeroed(); + assert!(p + .ref_frame_map + .iter() + .all(|&s| s == crate::va::VA_INVALID_SURFACE)); + assert_eq!(p.current_frame, crate::va::VA_INVALID_SURFACE); + assert_eq!(p.current_display_picture, crate::va::VA_INVALID_SURFACE); + assert_eq!(p.primary_ref_frame, PRIMARY_REF_NONE); + assert_eq!( + p.superres_scale_denominator, SUPERRES_NUM, + "a frame without superres sends 8, never 0 — libva documents 8 or 9..=16" + ); + assert!(p.anchor_frames_list.is_null()); + let t = VaSliceParameterBufferAV1::zeroed(); + assert_eq!(t.slice_data_flag, crate::va::VA_SLICE_DATA_FLAG_ALL); + } + + /// Every enumerator [`crate::pic_av1`] CASTS rather than remaps, pinned against + /// libva's own documented numbering. + /// + /// Four families reach a driver as a bare `as u8` / `as u32` of a vendored-parser + /// enum. Each cast is only correct because the parser's discriminants happen to be + /// the spec's, and "happen to be" is what a test is for: a vendored-parser bump + /// that renumbered any of them would decode to something plausible rather than + /// failing. + /// + /// The right-hand sides are transcribed from `va_dec_av1.h` and from libavcodec's + /// `av1.h`, not from the parser — comparing the parser against itself would pass + /// forever. + #[test] + fn every_enumerator_this_rung_casts_matches_the_numbering_libva_documents() { + use cros_codecs::codec::av1::parser::FrameRestorationType; + use cros_codecs::codec::av1::parser::FrameType; + use cros_codecs::codec::av1::parser::InterpolationFilter; + use cros_codecs::codec::av1::parser::TxMode; + use cros_codecs::codec::av1::parser::WarpModelType; + + // `VAAV1TransformationType` (measured off the header). + assert_eq!( + WarpModelType::Identity as u32, + VA_AV1_TRANSFORMATION_IDENTITY + ); + assert_eq!( + WarpModelType::Translation as u32, + VA_AV1_TRANSFORMATION_TRANSLATION + ); + assert_eq!(WarpModelType::RotZoom as u32, VA_AV1_TRANSFORMATION_ROTZOOM); + assert_eq!(WarpModelType::Affine as u32, VA_AV1_TRANSFORMATION_AFFINE); + + // `pic_info_fields.frame_type`, which the header documents inline: + // "0: KEY_FRAME; 1: INTER_FRAME; 2: INTRA_ONLY_FRAME; 3: SWITCH_FRAME". + assert_eq!(FrameType::KeyFrame as u8, 0); + assert_eq!(FrameType::InterFrame as u8, 1); + assert_eq!(FrameType::IntraOnlyFrame as u8, 2); + assert_eq!(FrameType::SwitchFrame as u8, 3); + + // `mode_control_fields.tx_mode` — "read_tx_mode, value range [0..2]", i.e. + // ONLY_4X4 / TX_MODE_LARGEST / TX_MODE_SELECT. + assert_eq!(TxMode::Only4x4 as u8, 0); + assert_eq!(TxMode::Largest as u8, 1); + assert_eq!(TxMode::Select as u8, 2); + + // `interp_filter` — "value range [0..4]", AV1 6.8.9's + // EIGHTTAP / EIGHTTAP_SMOOTH / EIGHTTAP_SHARP / BILINEAR / SWITCHABLE. + assert_eq!(InterpolationFilter::EightTap as u8, 0); + assert_eq!(InterpolationFilter::EightTapSmooth as u8, 1); + assert_eq!(InterpolationFilter::EightTapSharp as u8, 2); + assert_eq!(InterpolationFilter::Bilinear as u8, 3); + assert_eq!(InterpolationFilter::Switchable as u8, 4); + + // `loop_restoration_fields.*frame_restoration_type` — libavcodec's + // `AV1_RESTORE_NONE/WIENER/SGRPROJ/SWITCHABLE` = 0/1/2/3, which is what its + // `remap_lr_type[] = {NONE, SWITCHABLE, WIENER, SGRPROJ}` PRODUCES from the + // coded two-bit `lr_type`. The vendored parser applies the same + // `REMAP_LR_TYPE` as it reads, so [`crate::pic_av1`] casts and remaps + // nothing; if these four numbers moved, it would have to. + assert_eq!(FrameRestorationType::None as u8, 0); + assert_eq!(FrameRestorationType::Wiener as u8, 1); + assert_eq!(FrameRestorationType::Sgrproj as u8, 2); + assert_eq!(FrameRestorationType::Switchable as u8, 3); + } +}