The AV1 use-after-free fix (cdd1f3ef) stabilised the wrong half. NVIDIA was
measured retaining pColorConfig, so StoredParamsAv1 boxed the colour and timing
blocks — but OwnedStdAv1SequenceHeader kept the Std struct ITSELF inline, so the
pStdSequenceHeader we handed vkCreateVideoSessionParametersKHR was a stack
address inside ensure_parameters, dead the moment it returned. The fix worked
because of WHICH pointer that driver happened to hold. A driver retaining the
outer one instead — no more of a spec violation than retaining pColorConfig was —
reproduces the original bug exactly: plausible pictures, wrong content, no error
and no counter moved.
The same shape was in the shipping codecs, one step further from evidence: the
H.264 and H.265 create paths pointed pStdSPSs/pStdPPSs/pStdVPSs at function-local
Vecs, and both Add paths handed over the wrapper's inline std field and then moved
the wrapper. Those are spec-legal — the object stores copies — and have never
misbehaved on the fleet. They are fixed anyway, because that is precisely what was
true of H.264/H.265 before the same class of bug was found in them, and a
correctness argument that reduces to which vendor we tested is not one.
So: the Std struct is boxed inside each owning wrapper (one level out from what
_color_backing already did), and the contiguous create-time arrays are now fields
of the stored parameters, assembled at their final address. Identical bytes at
identical offsets — only where they live changed.
The line drawn deliberately, in prose at session.rs:29: Std DATA is pinned; the
VkVideoSessionParametersCreateInfoKHR chain itself is not. Retention there would
be a different and far more extreme class of driver bug, and pinning it needs a
self-referential struct over lifetime-parameterised builders.
⚠ NOT hardware-verified. No GPU has run this — the fleet is unreachable and the
250/250 parity that proved this code bit-exact cannot be re-run. That is why the
change is constrained to address stability alone, and why it ships five CPU-only
tests instead: three capture the pointer handed to Vulkan, perform the real move,
and assert it survives — each verified FAILING first, with genuinely differing
addresses, not a tautology. Two more pin the create-array ownership; those fail
before the fix as compile errors rather than assertions, because the pre-fix bug
there is a dangling pointer and asserting on it is UB.
Also: caps.rs claimed the borrow checker pins a profile chain between wire() and
its last use. False at exactly one site — decoder.rs took a raw *const, ending the
borrow, leaving nothing but inspection to stop a future editor moving the chain
before create_query_pool. Correct today, guarded by prose, which is how the first
bug shipped. It is now compiler-enforced: the pointer write and the create call
live inside one helper that takes the profile by reference, so the borrow is held
across both by the signature. An audit cleared the chains otherwise — no entry
point we pass one to retains it.
Gates: fmt clean; clippy -D warnings over pf-vkdecode AND pf-client-core in the
Linux container (its only real consumer, which cannot build on macOS at all —
wol.rs uses deps its manifest gates to linux/windows, so workspace clippy has
never passed there and does not now); 187 lib tests green on Linux, up from 182.
1052 lines
44 KiB
Rust
1052 lines
44 KiB
Rust
//! `VkVideoSessionKHR` + `VkVideoSessionParametersKHR` lifecycle.
|
|
//!
|
|
//! The session is created from the STREAM's facts (the SPS's coded extent and DPB
|
|
//! depth), its memory requirements bound exactly like the encoder does, and its
|
|
//! parameters object holds WP-A's converted `StdVideoH264*ParameterSet`s. Parameter
|
|
//! versioning follows Vulkan's rules precisely:
|
|
//!
|
|
//! - a NEW (sps-id / pps-id) is ADDED via `vkUpdateVideoSessionParametersKHR` with
|
|
//! `updateSequenceCount` = previous + 1 (the spec's exact-increment rule);
|
|
//! - an EXISTING id whose content changed cannot be updated in place — the object
|
|
//! is RECREATED (Vulkan forbids replacing a stored parameter set), as is an
|
|
//! object whose capacity would overflow;
|
|
//! - a stream renegotiation that resizes the DPB or the coded extent recreates the
|
|
//! whole session — `plan_to_vk`'s `CapacityMismatch` is the trigger the decoder
|
|
//! sees for the DPB half, the extent comparison covers the other.
|
|
//!
|
|
//! ⚠⚠⚠ **The Std sets' heap blocks must outlive the parameters OBJECT, not just the
|
|
//! call that hands them over.** Vulkan reads as though parameter data were captured
|
|
//! by `vkCreateVideoSessionParametersKHR`, and all three codecs in this crate
|
|
//! assumed it. NVIDIA 610.57.04 does not: for AV1 it was measured keeping
|
|
//! `StdVideoAV1SequenceHeader::pColorConfig` and dereferencing it when a decode is
|
|
//! RECORDED, which decoded every frame against recycled heap ([`crate::session_av1`]
|
|
//! carries the measurement). H.264's Std sets embed the same kind of pointer —
|
|
//! `pOffsetForRefFrame` and `pScalingLists` on the SPS, `pScalingLists` on the PPS —
|
|
//! so [`StoredParams`] holds the object and its backings in ONE value with one
|
|
//! lifetime, and both the recreate path and `Drop` destroy the object before that
|
|
//! value is released.
|
|
//!
|
|
//! **Both LEVELS of pointer are covered, not just the one the measurement caught.**
|
|
//! Fixing the inner pointers left the OUTER ones — `pStdSPSs`/`pStdPPSs`, and AV1's
|
|
//! `pStdSequenceHeader` — still addressing function locals, and a driver retaining
|
|
//! those instead would reproduce the same bug with the same silent signature. So the
|
|
//! Std structs are boxed inside their wrappers ([`crate::OwnedStdSps`]) and the
|
|
//! contiguous arrays are FIELDS of `StoredParams`: no address the driver is given
|
|
//! is a temporary's. The line is drawn at Std DATA — `VkVideoSessionParametersCreateInfoKHR`
|
|
//! and its `pNext`/`pParametersAddInfo` plumbing stay function-local, because those
|
|
//! are ordinary create-info structures every `vkCreate*` in Vulkan reads during the
|
|
//! call; it is the `pStd*` members whose retention the spec's wording left ambiguous
|
|
//! and this fleet was measured exercising.
|
|
//!
|
|
//! [`ParamsLedger`] is the pure half of that decision table (unit-tested);
|
|
//! [`VideoSession`] is the thin Vulkan half.
|
|
|
|
use std::rc::Rc;
|
|
|
|
use ash::vk;
|
|
use ash::vk::native as hh;
|
|
use cros_codecs::codec::h264::parser::Pps;
|
|
use cros_codecs::codec::h264::parser::Sps;
|
|
use tracing::debug;
|
|
|
|
use crate::caps::DecodeCaps;
|
|
use crate::caps::H264ProfileChain;
|
|
use crate::device::find_memory_type_preferring;
|
|
use crate::device::AllocError;
|
|
use crate::device::DecodeDevice;
|
|
use crate::params::pps_to_std;
|
|
use crate::params::sps_to_std;
|
|
use crate::params::OwnedStdPps;
|
|
use crate::params::OwnedStdSps;
|
|
use crate::params::ParamsError;
|
|
use crate::params_av1::ParamsAv1Error;
|
|
use crate::params_h265::H265ParamsError;
|
|
|
|
/// Parameter-object capacity. Punktfunk hosts emit one SPS + one PPS per stream;
|
|
/// the headroom absorbs id churn across renegotiations without recreation, and an
|
|
/// overflow beyond it recreates rather than fails.
|
|
pub(crate) const MAX_STD_SPS: usize = 4;
|
|
pub(crate) const MAX_STD_PPS: usize = 8;
|
|
|
|
/// What the ledger decided for one (SPS, PPS) activation.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ParamsAction {
|
|
/// Both sets are already stored with identical content — nothing to do.
|
|
Current,
|
|
/// At least one set is new; one update call (seq += 1) adds what is missing.
|
|
Add { add_sps: bool, add_pps: bool },
|
|
/// A stored id changed content, or capacity would overflow: recreate the
|
|
/// parameters object (Vulkan cannot replace or evict a stored set).
|
|
Recreate,
|
|
}
|
|
|
|
/// Pure bookkeeping for the parameters object: which sets it holds (by id AND
|
|
/// content — the parser re-parses in-band parameter sets every keyframe, so
|
|
/// pointer identity means nothing) and the update sequence counter.
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct ParamsLedger {
|
|
sps: Vec<(u8, Rc<Sps>)>,
|
|
pps: Vec<((u8, u8), Rc<Pps>)>,
|
|
update_seq: u32,
|
|
}
|
|
|
|
impl ParamsLedger {
|
|
/// Decide the action for activating (`sps`, `pps`). Pure — mutate via
|
|
/// [`Self::commit`].
|
|
pub(crate) fn plan(&self, sps: &Rc<Sps>, pps: &Rc<Pps>) -> ParamsAction {
|
|
let sps_key = sps.seq_parameter_set_id;
|
|
let pps_key = (pps.seq_parameter_set_id, pps.pic_parameter_set_id);
|
|
|
|
let stored_sps = self.sps.iter().find(|(id, _)| *id == sps_key);
|
|
let stored_pps = self.pps.iter().find(|(id, _)| *id == pps_key);
|
|
if let Some((_, stored)) = stored_sps {
|
|
if **stored != **sps {
|
|
return ParamsAction::Recreate;
|
|
}
|
|
}
|
|
if let Some((_, stored)) = stored_pps {
|
|
if **stored != **pps {
|
|
return ParamsAction::Recreate;
|
|
}
|
|
}
|
|
let add_sps = stored_sps.is_none();
|
|
let add_pps = stored_pps.is_none();
|
|
if !add_sps && !add_pps {
|
|
return ParamsAction::Current;
|
|
}
|
|
if (add_sps && self.sps.len() >= MAX_STD_SPS) || (add_pps && self.pps.len() >= MAX_STD_PPS)
|
|
{
|
|
return ParamsAction::Recreate;
|
|
}
|
|
ParamsAction::Add { add_sps, add_pps }
|
|
}
|
|
|
|
/// Apply a decided action. `Add` bumps the sequence count by EXACTLY one (the
|
|
/// Vulkan update rule — one call may carry both sets); `Recreate` resets the
|
|
/// ledger to just the current pair with a fresh object's zero counter (any
|
|
/// other id the stream still references simply re-Adds on next activation).
|
|
pub(crate) fn commit(&mut self, action: ParamsAction, sps: &Rc<Sps>, pps: &Rc<Pps>) {
|
|
match action {
|
|
ParamsAction::Current => {}
|
|
ParamsAction::Add { add_sps, add_pps } => {
|
|
if add_sps {
|
|
self.sps.push((sps.seq_parameter_set_id, Rc::clone(sps)));
|
|
}
|
|
if add_pps {
|
|
self.pps.push((
|
|
(pps.seq_parameter_set_id, pps.pic_parameter_set_id),
|
|
Rc::clone(pps),
|
|
));
|
|
}
|
|
self.update_seq += 1;
|
|
}
|
|
ParamsAction::Recreate => {
|
|
self.sps.clear();
|
|
self.pps.clear();
|
|
self.sps.push((sps.seq_parameter_set_id, Rc::clone(sps)));
|
|
self.pps.push((
|
|
(pps.seq_parameter_set_id, pps.pic_parameter_set_id),
|
|
Rc::clone(pps),
|
|
));
|
|
self.update_seq = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The sequence count the NEXT `vkUpdateVideoSessionParametersKHR` must carry.
|
|
pub(crate) fn next_update_seq(&self) -> u32 {
|
|
self.update_seq + 1
|
|
}
|
|
}
|
|
|
|
/// The session's create-time shape; a plan disagreeing with it forces a rebuild.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct SessionConfig {
|
|
pub max_coded_extent: vk::Extent2D,
|
|
pub max_dpb_slots: u32,
|
|
pub max_active_references: u32,
|
|
/// The Std profile the session was created against (a profile change is a
|
|
/// renegotiation too).
|
|
pub std_profile_idc: hh::StdVideoH264ProfileIdc,
|
|
}
|
|
|
|
/// Session creation/parameter failures the decoder maps into its error type.
|
|
#[derive(Debug)]
|
|
pub(crate) enum SessionError {
|
|
Vk(vk::Result),
|
|
Params(ParamsError),
|
|
/// An H.265 parameter set has no Std representation (the H.265 session's
|
|
/// counterpart of [`SessionError::Params`]).
|
|
ParamsH265(H265ParamsError),
|
|
/// An AV1 sequence header has no Std representation (the AV1 session's
|
|
/// counterpart of [`SessionError::Params`]).
|
|
ParamsAv1(ParamsAv1Error),
|
|
/// Session memory binding found no matching memory type (never a fallback).
|
|
NoMemoryType {
|
|
type_bits: u32,
|
|
flags: vk::MemoryPropertyFlags,
|
|
},
|
|
}
|
|
|
|
impl From<vk::Result> for SessionError {
|
|
fn from(r: vk::Result) -> Self {
|
|
SessionError::Vk(r)
|
|
}
|
|
}
|
|
|
|
impl From<ParamsError> for SessionError {
|
|
fn from(e: ParamsError) -> Self {
|
|
SessionError::Params(e)
|
|
}
|
|
}
|
|
|
|
impl From<H265ParamsError> for SessionError {
|
|
fn from(e: H265ParamsError) -> Self {
|
|
SessionError::ParamsH265(e)
|
|
}
|
|
}
|
|
|
|
impl From<ParamsAv1Error> for SessionError {
|
|
fn from(e: ParamsAv1Error) -> Self {
|
|
SessionError::ParamsAv1(e)
|
|
}
|
|
}
|
|
|
|
impl From<AllocError> for SessionError {
|
|
fn from(e: AllocError) -> Self {
|
|
match e {
|
|
AllocError::Vk(r) => SessionError::Vk(r),
|
|
AllocError::NoMemoryType { type_bits, flags } => {
|
|
SessionError::NoMemoryType { type_bits, flags }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A [`bind_session_memory`] failure and whatever allocations the CALLER must now
|
|
/// take over.
|
|
///
|
|
/// The distinction is a lifetime rule, not bookkeeping taste. Vulkan defines no
|
|
/// partial-bind rollback: once `vkBindVideoSessionMemoryKHR` has been called, some
|
|
/// bind indices may have taken, and memory bound into a live session may NOT be
|
|
/// freed while that session exists. So:
|
|
///
|
|
/// - an ALLOCATE failure happens before any bind — nothing is attached to the
|
|
/// session, the function frees everything itself, and `allocations` is empty;
|
|
/// - a BIND failure hands the allocations back UNFREED, because the caller's
|
|
/// session object must be destroyed FIRST. The caller parks them where its own
|
|
/// `Drop` frees them after the destroy (that is exactly [`VideoSession`]'s and
|
|
/// [`crate::session_h265::VideoSessionH265`]'s field order).
|
|
pub(crate) struct BindFailure {
|
|
/// Allocations that may be bound into the session — free them only AFTER the
|
|
/// session is destroyed. Empty when the failure preceded any bind.
|
|
pub(crate) allocations: Vec<vk::DeviceMemory>,
|
|
pub(crate) error: SessionError,
|
|
}
|
|
|
|
/// Query and bind one video session's memory requirements (the encoder's exact
|
|
/// shape), returning the allocations the session now owns. Codec-agnostic —
|
|
/// `VkVideoSessionKHR` memory binding says nothing about H.264 vs H.265 — so both
|
|
/// session types call this, and the NVIDIA placement rationale below lives once.
|
|
///
|
|
/// Failure hands back a [`BindFailure`] whose `allocations` the caller must adopt
|
|
/// (see its docs for the destroy-before-free rule); an allocate-stage failure
|
|
/// frees eagerly and hands back none.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// `dev` wraps live handles ([`crate::DeviceHandles`] contract) and `session` is a
|
|
/// live, not-yet-memory-bound session created on it.
|
|
pub(crate) unsafe fn bind_session_memory(
|
|
dev: &DecodeDevice,
|
|
session: vk::VideoSessionKHR,
|
|
) -> Result<Vec<vk::DeviceMemory>, BindFailure> {
|
|
let device = dev.ash();
|
|
let get = dev
|
|
.video_queue()
|
|
.fp()
|
|
.get_video_session_memory_requirements_khr;
|
|
let mut count = 0u32;
|
|
// SAFETY: live device + session (fn contract); null pointer is the
|
|
// count-query form.
|
|
let _ = unsafe { get(device.handle(), session, &mut count, std::ptr::null_mut()) };
|
|
let mut reqs = vec![vk::VideoSessionMemoryRequirementsKHR::default(); count as usize];
|
|
// SAFETY: as above with an array of the reported count.
|
|
let _ = unsafe { get(device.handle(), session, &mut count, reqs.as_mut_ptr()) };
|
|
|
|
let props = dev.memory_properties();
|
|
let mut allocated: Vec<vk::DeviceMemory> = Vec::with_capacity(reqs.len());
|
|
let mut binds = Vec::with_capacity(reqs.len());
|
|
// Free everything allocated so far — for the ALLOCATE-stage exits only, which
|
|
// are reached before `vkBindVideoSessionMemoryKHR` is ever called. The
|
|
// BIND-stage exit must NOT come through here (BindFailure docs).
|
|
let unwind = |device: &ash::Device, allocated: &[vk::DeviceMemory]| {
|
|
for &memory in allocated {
|
|
// SAFETY: allocations made in this function on this live device, none
|
|
// of which the bind call has been reached for — so none can be bound
|
|
// into any session, and freeing them here cannot outlive-order a
|
|
// session destroy.
|
|
unsafe { device.free_memory(memory, None) };
|
|
}
|
|
};
|
|
for rq in &reqs {
|
|
let mr = rq.memory_requirements;
|
|
// DEVICE_LOCAL preferred, any type from `memoryTypeBits` accepted:
|
|
// NVIDIA (610.88) constrains some session bindings to host-visible-only
|
|
// types, and the driver knows where its own session state belongs. NEVER
|
|
// a hard DEVICE_LOCAL requirement — that shape is unsatisfiable there.
|
|
let type_index = match find_memory_type_preferring(
|
|
&props,
|
|
mr.memory_type_bits,
|
|
vk::MemoryPropertyFlags::DEVICE_LOCAL,
|
|
) {
|
|
Ok(index) => index,
|
|
Err(e) => {
|
|
unwind(device, &allocated);
|
|
return Err(BindFailure {
|
|
allocations: Vec::new(),
|
|
error: e.into(),
|
|
});
|
|
}
|
|
};
|
|
let alloc = vk::MemoryAllocateInfo::default()
|
|
.allocation_size(mr.size)
|
|
.memory_type_index(type_index);
|
|
// SAFETY: live device (fn contract).
|
|
let memory = match unsafe { device.allocate_memory(&alloc, None) } {
|
|
Ok(memory) => memory,
|
|
Err(e) => {
|
|
unwind(device, &allocated);
|
|
return Err(BindFailure {
|
|
allocations: Vec::new(),
|
|
error: SessionError::Vk(e),
|
|
});
|
|
}
|
|
};
|
|
allocated.push(memory);
|
|
binds.push(
|
|
vk::BindVideoSessionMemoryInfoKHR::default()
|
|
.memory_bind_index(rq.memory_bind_index)
|
|
.memory(memory)
|
|
.memory_offset(0)
|
|
.memory_size(mr.size),
|
|
);
|
|
}
|
|
// SAFETY: session + freshly allocated memory, one bind per requirement.
|
|
let r = unsafe {
|
|
(dev.video_queue().fp().bind_video_session_memory_khr)(
|
|
device.handle(),
|
|
session,
|
|
binds.len() as u32,
|
|
binds.as_ptr(),
|
|
)
|
|
};
|
|
if r != vk::Result::SUCCESS {
|
|
// NOT freed here: a partial bind may have attached some of these to
|
|
// `session`, and Vulkan has no rollback for that. They go back to the
|
|
// caller, whose session object destroys BEFORE freeing them.
|
|
return Err(BindFailure {
|
|
allocations: allocated,
|
|
error: SessionError::Vk(r),
|
|
});
|
|
}
|
|
Ok(allocated)
|
|
}
|
|
|
|
/// A live parameters object **and every Std parameter set it was given**, in one
|
|
/// field — because the two may not drift apart.
|
|
///
|
|
/// The wrapper is not decoration and not defensive: a driver in this fleet keeps
|
|
/// the embedded pointers out of a Std set and dereferences them long after the call
|
|
/// that handed them over returned (module docs), so releasing the backing early
|
|
/// hands it freed memory. One value rather than two fields makes "an object whose
|
|
/// backing is gone" unrepresentable, which is the only shape of this bug — and the
|
|
/// shape a `let owned = …;` local silently had.
|
|
///
|
|
/// What is pinned, precisely: the wrappers' BOXED blocks, which is what the driver
|
|
/// was measured retaining. The contiguous array of outer `StdVideoH264*` structs
|
|
/// each call receives is a short-lived temporary, and the driver copies THAT before
|
|
/// returning — which is what the AV1 fix itself rests on, its Std header being moved
|
|
/// into storage after the create call on a rung that is now 250/250 bit-exact. So
|
|
/// moving these wrappers, or reallocating the `Vec`s holding them, disturbs nothing
|
|
/// the driver kept; `params::moving_the_wrapper_leaves_the_driver_s_pointers_put`
|
|
/// pins the half that matters.
|
|
struct StoredParams {
|
|
object: vk::VideoSessionParametersKHR,
|
|
/// One entry per set the OBJECT stores, held for the object's whole life.
|
|
/// Never read by this crate after the create/update call; the DRIVER reads the
|
|
/// blocks they own.
|
|
sps: Vec<OwnedStdSps>,
|
|
pps: Vec<OwnedStdPps>,
|
|
/// The contiguous Std ARRAYS the create call was handed as `pStdSPSs`/`pStdPPSs`
|
|
/// — the OUTER pointers, held for the object's life for the reason the wrappers
|
|
/// are. They were function-local `Vec`s, dropped the moment
|
|
/// [`VideoSession::create_parameters_object`] returned; nothing but the spec's
|
|
/// wording said a driver may not keep them, and that wording is what the AV1
|
|
/// measurement already disproved for the pointers one level in. Built by
|
|
/// [`Self::assemble`] at their final address, so the pointer the driver is given
|
|
/// never moves at all.
|
|
std_sps: Vec<hh::StdVideoH264SequenceParameterSet>,
|
|
std_pps: Vec<hh::StdVideoH264PictureParameterSet>,
|
|
}
|
|
|
|
impl StoredParams {
|
|
/// The wrappers plus the contiguous Std arrays the create call reads its
|
|
/// `pStdSPSs`/`pStdPPSs` out of, with a NULL object the caller fills in once
|
|
/// `vkCreateVideoSessionParametersKHR` has succeeded.
|
|
///
|
|
/// Assembling BEFORE the call is the point: the arrays are copies of the
|
|
/// wrappers' Std structs, and building them here puts them at the address they
|
|
/// will keep for the object's whole life rather than in a temporary the call
|
|
/// outlives.
|
|
fn assemble(sps: Vec<OwnedStdSps>, pps: Vec<OwnedStdPps>) -> Self {
|
|
// COPIES of each wrapper's Std struct (it is `Copy`); the embedded pointers
|
|
// they carry still address the wrappers' own boxed blocks, which is why
|
|
// both halves have to be kept.
|
|
let std_sps = sps.iter().map(|o| *o.std()).collect();
|
|
let std_pps = pps.iter().map(|o| *o.std()).collect();
|
|
Self {
|
|
object: vk::VideoSessionParametersKHR::null(),
|
|
sps,
|
|
pps,
|
|
std_sps,
|
|
std_pps,
|
|
}
|
|
}
|
|
|
|
/// The placeholder a half-built session holds. `vkDestroyVideoSessionParametersKHR`
|
|
/// ignores a NULL handle, so a [`VideoSession::create`] that fails before the
|
|
/// object exists still drops cleanly.
|
|
fn none() -> Self {
|
|
Self::assemble(Vec::new(), Vec::new())
|
|
}
|
|
|
|
/// Take over sets an `Add` just handed to the live object — they belong to the
|
|
/// OBJECT now, so their blocks live as long as it does rather than as long as
|
|
/// the update call. Only ever reached after that call SUCCEEDED: a failed
|
|
/// update stored nothing, and its wrappers are dropped instead.
|
|
fn adopt(&mut self, sps: Option<OwnedStdSps>, pps: Option<OwnedStdPps>) {
|
|
self.sps.extend(sps);
|
|
self.pps.extend(pps);
|
|
}
|
|
}
|
|
|
|
/// The Vulkan half: session + bound memory + parameters object.
|
|
pub(crate) struct VideoSession {
|
|
device: ash::Device,
|
|
video_queue: ash::khr::video_queue::Device,
|
|
session: vk::VideoSessionKHR,
|
|
memory: Vec<vk::DeviceMemory>,
|
|
parameters: StoredParams,
|
|
ledger: ParamsLedger,
|
|
pub(crate) config: SessionConfig,
|
|
/// The session has never run a coding scope: the first one records a
|
|
/// `VK_VIDEO_CODING_CONTROL_RESET_BIT_KHR` control before anything else (the
|
|
/// spec's initialization requirement; same shape as the encoder's first-frame
|
|
/// RESET install).
|
|
needs_reset: ResetArm,
|
|
}
|
|
|
|
impl VideoSession {
|
|
/// Create the session + an EMPTY parameters object (sets arrive via
|
|
/// [`Self::ensure_parameters`], which the decoder calls before the first
|
|
/// decode).
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// `dev` wraps live handles ([`crate::DeviceHandles`] contract).
|
|
pub(crate) unsafe fn create(
|
|
dev: &DecodeDevice,
|
|
caps: &DecodeCaps,
|
|
config: SessionConfig,
|
|
) -> Result<Self, SessionError> {
|
|
let mut chain = H264ProfileChain::new(config.std_profile_idc);
|
|
let profile = chain.wire();
|
|
let std_header_version = caps.std_header_version;
|
|
let session_ci = vk::VideoSessionCreateInfoKHR::default()
|
|
.queue_family_index(dev.decode_qf())
|
|
.video_profile(profile)
|
|
.picture_format(caps.output_format)
|
|
.max_coded_extent(config.max_coded_extent)
|
|
.reference_picture_format(caps.dpb_format)
|
|
.max_dpb_slots(config.max_dpb_slots)
|
|
.max_active_reference_pictures(config.max_active_references)
|
|
.std_header_version(&std_header_version);
|
|
let mut session = vk::VideoSessionKHR::null();
|
|
// SAFETY: live device; `session_ci` roots locals (chain, header version)
|
|
// that outlive the call.
|
|
let r = unsafe {
|
|
(dev.video_queue().fp().create_video_session_khr)(
|
|
dev.ash().handle(),
|
|
&session_ci,
|
|
std::ptr::null(),
|
|
&mut session,
|
|
)
|
|
};
|
|
if r != vk::Result::SUCCESS {
|
|
return Err(SessionError::Vk(r));
|
|
}
|
|
|
|
let mut built = Self {
|
|
device: dev.ash().clone(),
|
|
video_queue: dev.video_queue().clone(),
|
|
session,
|
|
memory: Vec::new(),
|
|
parameters: StoredParams::none(),
|
|
ledger: ParamsLedger::default(),
|
|
config,
|
|
needs_reset: ResetArm::armed(),
|
|
};
|
|
// SAFETY: fn contract; on error `built` drops and unwinds the session +
|
|
// whatever memory was bound.
|
|
unsafe {
|
|
// A bind failure hands its allocations BACK: parking them in `built`
|
|
// is what makes the early return destroy the session before freeing
|
|
// them (BindFailure docs — Vulkan defines no partial-bind rollback).
|
|
match bind_session_memory(dev, session) {
|
|
Ok(memory) => built.memory = memory,
|
|
Err(failure) => {
|
|
built.memory = failure.allocations;
|
|
return Err(failure.error);
|
|
}
|
|
}
|
|
built.parameters = built.create_parameters_object(Vec::new(), Vec::new())?;
|
|
}
|
|
Ok(built)
|
|
}
|
|
|
|
/// Create a parameters object holding exactly `sps`/`pps` (either may be
|
|
/// empty), **fused with the wrappers whose heap blocks it points at**.
|
|
///
|
|
/// Taking the wrappers BY VALUE rather than as Std slices is the point: there is
|
|
/// no way to reach `vkCreateVideoSessionParametersKHR` from here without the
|
|
/// resulting object taking ownership of everything it will go on dereferencing
|
|
/// (module docs, [`StoredParams`]).
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// Live device + live session.
|
|
unsafe fn create_parameters_object(
|
|
&self,
|
|
sps: Vec<OwnedStdSps>,
|
|
pps: Vec<OwnedStdPps>,
|
|
) -> Result<StoredParams, SessionError> {
|
|
// Assembled FIRST so the arrays `pStdSPSs`/`pStdPPSs` will point at are
|
|
// already where they will stay: `stored` is returned by value, and moving a
|
|
// `Vec` moves its handle, not the block the driver was given.
|
|
let mut stored = StoredParams::assemble(sps, pps);
|
|
let add = vk::VideoDecodeH264SessionParametersAddInfoKHR::default()
|
|
.std_sp_ss(&stored.std_sps)
|
|
.std_pp_ss(&stored.std_pps);
|
|
let mut h264 = vk::VideoDecodeH264SessionParametersCreateInfoKHR::default()
|
|
.max_std_sps_count(MAX_STD_SPS as u32)
|
|
.max_std_pps_count(MAX_STD_PPS as u32)
|
|
.parameters_add_info(&add);
|
|
let ci = vk::VideoSessionParametersCreateInfoKHR::default()
|
|
.video_session(self.session)
|
|
.push_next(&mut h264);
|
|
let mut object = vk::VideoSessionParametersKHR::null();
|
|
// SAFETY: fn contract; `ci` roots locals outliving the call, and everything
|
|
// the driver may retain past it — the Std arrays AND the blocks their
|
|
// embedded pointers address — is owned by `stored`, which is returned
|
|
// rather than dropped here.
|
|
let r = unsafe {
|
|
(self.video_queue.fp().create_video_session_parameters_khr)(
|
|
self.device.handle(),
|
|
&ci,
|
|
std::ptr::null(),
|
|
&mut object,
|
|
)
|
|
};
|
|
if r != vk::Result::SUCCESS {
|
|
return Err(SessionError::Vk(r));
|
|
}
|
|
stored.object = object;
|
|
Ok(stored)
|
|
}
|
|
|
|
/// The ledger's verdict for activating (`sps`, `pps`), without mutating
|
|
/// anything — the decoder consults this BEFORE [`Self::ensure_parameters`] so
|
|
/// a [`ParamsAction::Recreate`] can be preceded by a full in-flight drain
|
|
/// (the destroy inside the recreate must never race a submitted decode).
|
|
pub(crate) fn parameters_action(&self, sps: &Rc<Sps>, pps: &Rc<Pps>) -> ParamsAction {
|
|
self.ledger.plan(sps, pps)
|
|
}
|
|
|
|
/// Make the parameters object hold this AU's activated (SPS, PPS), converting
|
|
/// through WP-A and Adding/Recreating per the ledger's decision.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// Live device; when [`Self::parameters_action`] says `Recreate`, the caller
|
|
/// has ALREADY drained every in-flight decode (waited each output slot's
|
|
/// newest submitted timeline value) — the old object is destroyed here, and a
|
|
/// still-executing decode reading it would be use-after-free at the driver
|
|
/// level. The decoder enforces exactly that ordering in `decode_inner`;
|
|
/// `Current`/`Add` touch no object a submitted decode can be reading.
|
|
pub(crate) unsafe fn ensure_parameters(
|
|
&mut self,
|
|
sps: &Rc<Sps>,
|
|
pps: &Rc<Pps>,
|
|
) -> Result<(), SessionError> {
|
|
let action = self.ledger.plan(sps, pps);
|
|
match action {
|
|
ParamsAction::Current => Ok(()),
|
|
ParamsAction::Add { add_sps, add_pps } => {
|
|
let owned_sps = if add_sps {
|
|
Some(sps_to_std(sps)?)
|
|
} else {
|
|
None
|
|
};
|
|
let owned_pps = if add_pps {
|
|
Some(pps_to_std(pps)?)
|
|
} else {
|
|
None
|
|
};
|
|
let sps_slice: &[hh::StdVideoH264SequenceParameterSet] = match &owned_sps {
|
|
Some(o) => std::slice::from_ref(o.std()),
|
|
None => &[],
|
|
};
|
|
let pps_slice: &[hh::StdVideoH264PictureParameterSet] = match &owned_pps {
|
|
Some(o) => std::slice::from_ref(o.std()),
|
|
None => &[],
|
|
};
|
|
let mut add = vk::VideoDecodeH264SessionParametersAddInfoKHR::default()
|
|
.std_sp_ss(sps_slice)
|
|
.std_pp_ss(pps_slice);
|
|
let update = vk::VideoSessionParametersUpdateInfoKHR::default()
|
|
.update_sequence_count(self.ledger.next_update_seq())
|
|
.push_next(&mut add);
|
|
// SAFETY: live device + parameters object; `update` roots locals
|
|
// (incl. the OwnedStd backings) outliving the call — and the
|
|
// backings go on outliving it, adopted below.
|
|
let r = unsafe {
|
|
(self.video_queue.fp().update_video_session_parameters_khr)(
|
|
self.device.handle(),
|
|
self.parameters.object,
|
|
&update,
|
|
)
|
|
};
|
|
if r != vk::Result::SUCCESS {
|
|
return Err(SessionError::Vk(r));
|
|
}
|
|
// ⚠ The added sets now belong to the OBJECT, so their heap blocks
|
|
// must too: an Add whose wrappers died at the end of this arm would
|
|
// be the AV1 use-after-free with an update call in front of it.
|
|
self.parameters.adopt(owned_sps, owned_pps);
|
|
self.ledger.commit(action, sps, pps);
|
|
Ok(())
|
|
}
|
|
ParamsAction::Recreate => {
|
|
debug!(
|
|
sps_id = sps.seq_parameter_set_id,
|
|
pps_id = pps.pic_parameter_set_id,
|
|
"recreating session parameters (content change or capacity)"
|
|
);
|
|
let owned_sps = sps_to_std(sps)?;
|
|
let owned_pps = pps_to_std(pps)?;
|
|
// SAFETY: fn contract — live device + live session. The wrappers
|
|
// are MOVED IN and come back owned by the fresh object, so they
|
|
// live as long as it does rather than merely across the call.
|
|
let fresh =
|
|
unsafe { self.create_parameters_object(vec![owned_sps], vec![owned_pps])? };
|
|
// The old object goes FIRST and its backings with it — installing
|
|
// `fresh` through a local keeps the destroy ahead of the free,
|
|
// which is the order a driver still holding the old pointers needs.
|
|
let old = std::mem::replace(&mut self.parameters, fresh);
|
|
// SAFETY: the fn-level contract — the caller drained every
|
|
// in-flight decode before a Recreate reached here (checked via
|
|
// parameters_action), so no submitted work reads the old object;
|
|
// it is this session's own handle, on a live device.
|
|
unsafe {
|
|
(self.video_queue.fp().destroy_video_session_parameters_khr)(
|
|
self.device.handle(),
|
|
old.object,
|
|
std::ptr::null(),
|
|
);
|
|
}
|
|
// Explicit, because the ORDER is the whole point: every Std block
|
|
// `old` owns is released only now, after the object that pointed at
|
|
// them is gone.
|
|
drop(old);
|
|
self.ledger.commit(action, sps, pps);
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(crate) fn session(&self) -> vk::VideoSessionKHR {
|
|
self.session
|
|
}
|
|
|
|
pub(crate) fn parameters(&self) -> vk::VideoSessionParametersKHR {
|
|
self.parameters.object
|
|
}
|
|
|
|
/// Whether the next coding scope must record the initialization RESET —
|
|
/// `true` exactly once per session, PROVIDED the command buffer that recorded
|
|
/// it actually reaches the queue: a recording/submit failure after this
|
|
/// returned `true` must call [`Self::re_arm_reset`], or the session would run
|
|
/// its whole life uninitialized.
|
|
pub(crate) fn take_needs_reset(&mut self) -> bool {
|
|
self.needs_reset.take()
|
|
}
|
|
|
|
/// Undo a consumed [`Self::take_needs_reset`] whose RESET never reached the
|
|
/// queue (end/submit failed after recording it).
|
|
pub(crate) fn re_arm_reset(&mut self) {
|
|
self.needs_reset.re_arm();
|
|
}
|
|
}
|
|
|
|
/// The one-shot session-RESET arm, its own type so the take/re-arm cycle is
|
|
/// testable without a live session object.
|
|
#[derive(Debug)]
|
|
pub(crate) struct ResetArm(bool);
|
|
|
|
impl ResetArm {
|
|
pub(crate) fn armed() -> Self {
|
|
Self(true)
|
|
}
|
|
|
|
pub(crate) fn take(&mut self) -> bool {
|
|
std::mem::take(&mut self.0)
|
|
}
|
|
|
|
pub(crate) fn re_arm(&mut self) {
|
|
self.0 = true;
|
|
}
|
|
}
|
|
|
|
impl Drop for VideoSession {
|
|
fn drop(&mut self) {
|
|
// SAFETY: all handles are this session's own on the (contract-live) device;
|
|
// the owning decoder drains GPU work before dropping state. The destroy
|
|
// entry points ignore NULL handles, covering half-built sessions. The
|
|
// ORDER is load-bearing, not stylistic: memory bound into a session may
|
|
// not be freed while the session lives, so the session is destroyed first
|
|
// — which is also why a failed bind hands its allocations back here
|
|
// instead of freeing them itself ([`BindFailure`]). The Std backings are
|
|
// freed after both, by the `parameters` field's own drop, which Rust runs
|
|
// AFTER this body — the same reason `ensure_parameters` destroys before it
|
|
// replaces.
|
|
unsafe {
|
|
(self.video_queue.fp().destroy_video_session_parameters_khr)(
|
|
self.device.handle(),
|
|
self.parameters.object,
|
|
std::ptr::null(),
|
|
);
|
|
(self.video_queue.fp().destroy_video_session_khr)(
|
|
self.device.handle(),
|
|
self.session,
|
|
std::ptr::null(),
|
|
);
|
|
for memory in self.memory.drain(..) {
|
|
self.device.free_memory(memory, None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use cros_codecs::codec::h264::parser::PpsBuilder;
|
|
use cros_codecs::codec::h264::parser::Profile;
|
|
use cros_codecs::codec::h264::parser::SpsBuilder;
|
|
use pf_bitstream::h264::Level;
|
|
|
|
use super::*;
|
|
|
|
fn authored(sps_id: u8, pps_id: u8, qp: u8) -> (Rc<Sps>, Rc<Pps>) {
|
|
let sps = SpsBuilder::new()
|
|
.seq_parameter_set_id(sps_id)
|
|
.profile_idc(Profile::Main)
|
|
.level_idc(Level::L4)
|
|
.frame_mbs_only_flag(true)
|
|
.direct_8x8_inference_flag(true)
|
|
.max_num_ref_frames(4)
|
|
.resolution(64, 64)
|
|
.build();
|
|
let pps = PpsBuilder::new(Rc::clone(&sps))
|
|
.pic_parameter_set_id(pps_id)
|
|
.pic_init_qp(qp)
|
|
.build();
|
|
(sps, pps)
|
|
}
|
|
|
|
/// An `Add` hands NEW Std sets to an EXISTING parameters object, so their heap
|
|
/// blocks must live as long as that OBJECT — not as long as the update call
|
|
/// that carried them. [`StoredParams::adopt`] is where the transfer happens,
|
|
/// and this pins that it genuinely takes ownership: `ensure_parameters` drops
|
|
/// its local wrappers the instant this returns, and a driver holding
|
|
/// `pScalingLists` would be reading freed heap from the next frame on
|
|
/// ([`crate::session_av1`] for the measurement that made this real).
|
|
#[test]
|
|
fn an_added_set_keeps_its_blocks_alive_past_the_update_call() {
|
|
let sps = SpsBuilder::new()
|
|
.seq_parameter_set_id(0)
|
|
.profile_idc(Profile::Main)
|
|
.level_idc(Level::L4)
|
|
.frame_mbs_only_flag(true)
|
|
.direct_8x8_inference_flag(true)
|
|
.max_num_ref_frames(4)
|
|
.resolution(64, 64)
|
|
// The one SPS pointer a builder can attach.
|
|
.seq_scaling_matrix_present_flag(true)
|
|
.build();
|
|
let owned = sps_to_std(&sps).expect("converts");
|
|
let lists = owned.std().pScalingLists;
|
|
assert!(!lists.is_null(), "the fixture attaches scaling lists");
|
|
|
|
let mut stored = StoredParams::none();
|
|
stored.adopt(Some(owned), None);
|
|
assert_eq!(
|
|
(stored.sps.len(), stored.pps.len()),
|
|
(1, 0),
|
|
"the object took the set itself, not a borrow of it"
|
|
);
|
|
assert_eq!(
|
|
stored.sps[0].std().pScalingLists,
|
|
lists,
|
|
"and it is the same block the driver was handed"
|
|
);
|
|
// SAFETY: `stored` owns the block — which is exactly the property here.
|
|
let read_back = unsafe { (*lists).ScalingList4x4[0] };
|
|
assert_eq!(read_back, [0; 16], "the fixture's lists, read back live");
|
|
}
|
|
|
|
/// …and it keeps the ADDRESS too, not merely the blocks.
|
|
///
|
|
/// The Add path hands `vkUpdateVideoSessionParametersKHR` a
|
|
/// `std::slice::from_ref(o.std())` — a one-element array that IS the wrapper's
|
|
/// own Std struct — and then moves the wrapper into [`StoredParams`]. The test
|
|
/// above covers a driver retaining `pScalingLists` (an INNER pointer); this
|
|
/// covers one retaining `pStdSPSs`/`pStdPPSs`, which the same wording in the
|
|
/// spec permits just as much. Boxing the Std struct inside the wrapper is what
|
|
/// makes the two addresses equal; un-boxing it would leave every other test in
|
|
/// this crate green and hand the driver a moved-from stack slot.
|
|
#[test]
|
|
fn an_added_set_keeps_the_address_the_update_call_was_given() {
|
|
let (sps, pps) = authored(0, 0, 26);
|
|
let owned_sps = sps_to_std(&sps).expect("converts");
|
|
let owned_pps = pps_to_std(&pps).expect("converts");
|
|
// Exactly what `ensure_parameters` puts in `pStdSPSs`/`pStdPPSs`.
|
|
let handed_sps = std::ptr::from_ref(owned_sps.std());
|
|
let handed_pps = std::ptr::from_ref(owned_pps.std());
|
|
|
|
let mut stored = StoredParams::none();
|
|
stored.adopt(Some(owned_sps), Some(owned_pps));
|
|
assert_eq!(
|
|
std::ptr::from_ref(stored.sps[0].std()),
|
|
handed_sps,
|
|
"the SPS address handed to Vulkan must be the one the object keeps"
|
|
);
|
|
assert_eq!(
|
|
std::ptr::from_ref(stored.pps[0].std()),
|
|
handed_pps,
|
|
"and likewise the PPS"
|
|
);
|
|
// SAFETY: `stored` owns both structs — which is exactly the property here.
|
|
let ids = unsafe {
|
|
(
|
|
(*handed_sps).seq_parameter_set_id,
|
|
(*handed_pps).pic_parameter_set_id,
|
|
)
|
|
};
|
|
assert_eq!(
|
|
ids,
|
|
(0, 0),
|
|
"read back through the pointers the driver holds"
|
|
);
|
|
}
|
|
|
|
/// The create path's OUTER pointers: `pStdSPSs`/`pStdPPSs` address contiguous
|
|
/// COPIES of the wrappers' Std structs, and those arrays must outlive the
|
|
/// create call the same way the wrappers do.
|
|
///
|
|
/// [`StoredParams::assemble`] builds them at their final address — inside the
|
|
/// value the parameters object is returned in — so the pointer the driver is
|
|
/// given never moves at all. Before this, they were function-local `Vec`s that
|
|
/// were dropped the instant `create_parameters_object` returned: a driver
|
|
/// retaining the array (rather than the embedded pointer this fleet was
|
|
/// measured retaining) would have been reading freed heap from the first frame.
|
|
#[test]
|
|
fn the_std_arrays_the_create_call_is_given_are_the_ones_the_object_keeps() {
|
|
let sps = SpsBuilder::new()
|
|
.seq_parameter_set_id(0)
|
|
.profile_idc(Profile::Main)
|
|
.level_idc(Level::L4)
|
|
.frame_mbs_only_flag(true)
|
|
.direct_8x8_inference_flag(true)
|
|
.max_num_ref_frames(4)
|
|
.resolution(64, 64)
|
|
// So the copied Std struct carries a NON-null embedded pointer and the
|
|
// "still addresses the wrapper's live block" assertion below can bite.
|
|
.seq_scaling_matrix_present_flag(true)
|
|
.build();
|
|
let pps = PpsBuilder::new(Rc::clone(&sps))
|
|
.pic_parameter_set_id(0)
|
|
.pic_init_qp(26)
|
|
.build();
|
|
let owned_sps = sps_to_std(&sps).expect("converts");
|
|
let owned_pps = pps_to_std(&pps).expect("converts");
|
|
|
|
let stored = StoredParams::assemble(vec![owned_sps], vec![owned_pps]);
|
|
// Exactly what `create_parameters_object` puts in `pStdSPSs`/`pStdPPSs`.
|
|
let (handed_sps, handed_pps) = (stored.std_sps.as_ptr(), stored.std_pps.as_ptr());
|
|
// The move `create_parameters_object` ends with: `Ok(stored)`.
|
|
let stored = std::hint::black_box(stored);
|
|
assert_eq!((stored.std_sps.len(), stored.std_pps.len()), (1, 1));
|
|
assert_eq!(
|
|
(stored.std_sps.as_ptr(), stored.std_pps.as_ptr()),
|
|
(handed_sps, handed_pps),
|
|
"the arrays handed to Vulkan must be the ones the object keeps"
|
|
);
|
|
// And their COPIES still address the wrappers' own live blocks.
|
|
let lists = stored.std_sps[0].pScalingLists;
|
|
assert!(!lists.is_null(), "the fixture attaches scaling lists");
|
|
assert_eq!(lists, stored.sps[0].std().pScalingLists);
|
|
// SAFETY: `stored` owns the block the copy points at — the property here.
|
|
let read_back = unsafe { (*lists).ScalingList4x4[0] };
|
|
assert_eq!(read_back, [0; 16], "the fixture's lists, read back live");
|
|
assert_eq!(
|
|
stored.std_pps[0].pic_parameter_set_id,
|
|
stored.pps[0].std().pic_parameter_set_id,
|
|
"the PPS copy is the wrapper's, field for field"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_reactivated_identical_pair_is_current_even_across_reparses() {
|
|
let (sps_a, pps_a) = authored(0, 0, 26);
|
|
// The parser re-parses in-band sets each keyframe: same content, NEW Rcs.
|
|
let (sps_b, pps_b) = authored(0, 0, 26);
|
|
assert!(!Rc::ptr_eq(&sps_a, &sps_b));
|
|
|
|
let mut ledger = ParamsLedger::default();
|
|
let first = ledger.plan(&sps_a, &pps_a);
|
|
assert_eq!(
|
|
first,
|
|
ParamsAction::Add {
|
|
add_sps: true,
|
|
add_pps: true
|
|
}
|
|
);
|
|
ledger.commit(first, &sps_a, &pps_a);
|
|
assert_eq!(ledger.plan(&sps_b, &pps_b), ParamsAction::Current);
|
|
}
|
|
|
|
#[test]
|
|
fn a_new_pps_id_over_a_stored_sps_adds_only_the_pps() {
|
|
let (sps, pps0) = authored(0, 0, 26);
|
|
let pps1 = PpsBuilder::new(Rc::clone(&sps))
|
|
.pic_parameter_set_id(1)
|
|
.pic_init_qp(26)
|
|
.build();
|
|
|
|
let mut ledger = ParamsLedger::default();
|
|
let a = ledger.plan(&sps, &pps0);
|
|
ledger.commit(a, &sps, &pps0);
|
|
assert_eq!(
|
|
ledger.plan(&sps, &pps1),
|
|
ParamsAction::Add {
|
|
add_sps: false,
|
|
add_pps: true
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn changed_content_under_a_stored_id_recreates_and_resets_the_sequence() {
|
|
let (sps, pps) = authored(0, 0, 26);
|
|
let mut ledger = ParamsLedger::default();
|
|
let a = ledger.plan(&sps, &pps);
|
|
ledger.commit(a, &sps, &pps);
|
|
assert_eq!(ledger.next_update_seq(), 2, "one Add happened");
|
|
|
|
// Same ids, different content (qp changed): Vulkan cannot replace a
|
|
// stored set, so this must recreate.
|
|
let (sps2, pps2) = authored(0, 0, 30);
|
|
let action = ledger.plan(&sps2, &pps2);
|
|
assert_eq!(action, ParamsAction::Recreate);
|
|
ledger.commit(action, &sps2, &pps2);
|
|
assert_eq!(
|
|
ledger.next_update_seq(),
|
|
1,
|
|
"a fresh object restarts its counter"
|
|
);
|
|
// And the pair is now Current under the new content.
|
|
assert_eq!(ledger.plan(&sps2, &pps2), ParamsAction::Current);
|
|
}
|
|
|
|
#[test]
|
|
fn capacity_overflow_recreates_with_just_the_current_pair() {
|
|
let mut ledger = ParamsLedger::default();
|
|
// Fill the PPS capacity under one SPS.
|
|
let (sps, first) = authored(0, 0, 26);
|
|
let a = ledger.plan(&sps, &first);
|
|
ledger.commit(a, &sps, &first);
|
|
for pps_id in 1..MAX_STD_PPS as u8 {
|
|
let pps = PpsBuilder::new(Rc::clone(&sps))
|
|
.pic_parameter_set_id(pps_id)
|
|
.pic_init_qp(26)
|
|
.build();
|
|
let a = ledger.plan(&sps, &pps);
|
|
assert!(matches!(a, ParamsAction::Add { .. }));
|
|
ledger.commit(a, &sps, &pps);
|
|
}
|
|
assert_eq!(ledger.next_update_seq() - 1, MAX_STD_PPS as u32);
|
|
|
|
// One past capacity: recreate; afterwards the evicted first PPS re-Adds.
|
|
let overflow = PpsBuilder::new(Rc::clone(&sps))
|
|
.pic_parameter_set_id(MAX_STD_PPS as u8)
|
|
.pic_init_qp(26)
|
|
.build();
|
|
let action = ledger.plan(&sps, &overflow);
|
|
assert_eq!(action, ParamsAction::Recreate);
|
|
ledger.commit(action, &sps, &overflow);
|
|
assert_eq!(
|
|
ledger.plan(&sps, &first),
|
|
ParamsAction::Add {
|
|
add_sps: false,
|
|
add_pps: true
|
|
},
|
|
"sets evicted by a recreate re-add on next activation"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_reset_arm_fires_once_unless_the_failed_submit_re_arms_it() {
|
|
let mut arm = ResetArm::armed();
|
|
assert!(arm.take(), "a fresh session needs its RESET");
|
|
assert!(
|
|
!arm.take(),
|
|
"consumed — the next scope must NOT reset again"
|
|
);
|
|
|
|
// The recorded RESET never reached the queue (end/submit failed): the
|
|
// re-arm makes the next successful recording carry it instead.
|
|
arm.re_arm();
|
|
assert!(arm.take());
|
|
assert!(!arm.take());
|
|
}
|
|
|
|
#[test]
|
|
fn update_sequence_counts_one_per_add_call_not_per_set() {
|
|
let (sps, pps) = authored(0, 0, 26);
|
|
let mut ledger = ParamsLedger::default();
|
|
assert_eq!(ledger.next_update_seq(), 1);
|
|
// One call carries BOTH sets: the counter moves by exactly one.
|
|
let a = ledger.plan(&sps, &pps);
|
|
assert_eq!(
|
|
a,
|
|
ParamsAction::Add {
|
|
add_sps: true,
|
|
add_pps: true
|
|
}
|
|
);
|
|
ledger.commit(a, &sps, &pps);
|
|
assert_eq!(ledger.next_update_seq(), 2);
|
|
}
|
|
}
|