From a20cd44ed498a8c7f964947754cc0889b87be5cb Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Fri, 7 Aug 2026 04:22:23 +0200 Subject: [PATCH] =?UTF-8?q?feat(client):=20native=20VAAPI=20AV1=20?= =?UTF-8?q?=E2=80=94=20the=20third=20rung,=20and=20two=20failure-path=20de?= =?UTF-8?q?fects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The libva AV1 layouts, the AuPlan conversion and the Linux rung's AV1 arm, completing AV1 across all three hardware backends. Pin-only. Layouts measured, not transcribed: the committed probe grew the AV1 structures and every size and offset it printed against libva 2.23.0 is a compile-time assertion. Three that a hand-count gets wrong — the picture buffer is align 8 because anchor_frames_list is a pointer, inserting seven bytes of padding; seg_info and film_grain_info carry their own padding tails inside the parent; and THREE of AV1's six bit-field unions are narrower than a word (one uint8_t, two uint16_t), so a u32 packer over any of them writes through its neighbour. This is the fifth way this program has had to spell "which pictures does this frame use", and it is unlike the other four: ref_frame_map is indexed by SLOT and holds actual VASurfaceIDs rather than indices into anything, ref_frame_idx is indexed by NAME and holds slots taken from the header — not from the plan's refs, where a lost reference leaves a hole and a hole is not a slot — global motion is picture-level, and there is no per-reference size field at all. Established from va_dec_av1.h and libavcodec's vaapi_av1.c, and stated in the module docs so the next reader does not re-derive it. Review verified the whole happy path — every layout assertion re-measured, every packer width and bit position, the reference convention, the num_elements buffer shape — and found both defects on FAILURE paths, neither reachable on the vendored vector. A conversion refusal permanently desynced the ledger. The mutation block sat after the tile walk, so any tile-shape refusal left the planner holding a picture with no ledger slot — and the resulting UnresolvedReference fires before that block too, so it never repaired. Every later access unit hard-errored until a shown key frame: one lost packet costing a GOP. The arm's own doc already warned that skipping conversion would desynchronise the slot map; the refusal door did exactly what the skip door was written to avoid. The block is hoisted, and a tile-shape refusal on an already-damaged plan is now concealed rather than refused. Fixing that exposed a sharper edge: the conversion can release a slot and reassign it to the refused picture in one call, so the binding would still hold the PREVIOUS picture's surface — a wrong reference rather than a missing one, which nothing downstream could notice. The caller now clears the binding unconditionally on the refusal path. And a damaged frame's surface was never written yet was bound as a reference and left in pending, so a later clean show_existing_frame would claim it with damaged = false and ship uninitialised GPU memory to the presenter — on several drivers another client's framebuffer. The justification quoted half of va_dec_av1.h; its next sentence gives the remedy, which is to point the problematic index at an alternative buffer. Damaged frames now submit as they do on the other two arms, with live surfaces substituted for invalid entries and reported as a bitmask — preferring a reference that really decoded over the decode target, and keeping libavcodec's deliberate all-invalid map on a shown key frame. Film grain is refused rather than decoded wrong: libva wants two surfaces, one ungrained for prediction and one grained for output, and libavcodec allocates a second frame for exactly that. The gate now sits after the mutation block so a grained frame costs itself rather than the GOP, and stays per-AU rather than per-sequence because a stream that merely DECLARES the tool decodes here perfectly. ⚠ Residual, flagged not fixed: a picture decoded from substituted references can still be shown by a later show_existing_frame. It is decoded memory now rather than uninitialised, and it is what the H.264/H.265 arms do, but tracking "this was concealed" through to display needs new session state. Gates: macOS fmt/clippy/125 tests/cargo-doc, container clippy -D warnings over seven crates and 548 tests, workspace check. pf-bitstream's diff is comment-only — verified — so the Vulkan rung's 250/250 stands untouched. Nothing here has decoded a frame: no VAAPI hardware is reachable. --- crates/pf-bitstream/src/av1.rs | 35 +- crates/pf-client-core/src/video.rs | 8 +- .../pf-client-core/src/video_vaapi_native.rs | 660 ++++++- crates/pf-vaadec/Cargo.toml | 2 +- crates/pf-vaadec/layout-probe.c | 225 ++- crates/pf-vaadec/src/config.rs | 73 + crates/pf-vaadec/src/lib.rs | 50 +- crates/pf-vaadec/src/pic_av1.rs | 1591 +++++++++++++++++ crates/pf-vaadec/src/va_av1.rs | 1305 ++++++++++++++ 9 files changed, 3887 insertions(+), 62 deletions(-) create mode 100644 crates/pf-vaadec/src/pic_av1.rs create mode 100644 crates/pf-vaadec/src/va_av1.rs 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); + } +}