A 2026-08-12 field report (RTX 5060 client): every HEVC session demoted to D3D11VA with 81 "outside device caps: stream level (Std code point 12) above the device's maxLevelIdc (H.265 Std level 11)" refusals — the host's AMF encoder stamps general_level_idc 6.2 (the codec maximum) on a 4K120 stream that needs 5.2, and NVIDIA's driver caps H.265 decode at 6.1. The hardware decodes the actual stream trivially; only the declaration was oversized. AV1 passed the same gate, which is why "native-vulkan runs only with AV1". The declared level is a claim, and the stream's real demands are enforced where they are physical facts — coded extent and DPB depth, both checked at session build. So the up-front level gate (H.264 + H.265) now warns once and proceeds, and every SPS/VPS handed to the Vulkan parameters object has its level clamped to the device ceiling (a set above maxLevelIdc is invalid usage). AV1's gate is untouched: its code space is the bitstream's own and no over-declaration has been seen in the field. Verified on .173 (RTX 4090, driver 610.88): HEVC and AV1 both decode on the native Vulkan rung at 60 fps against an NVENC host; unit tests pin the clamp (lowers, only lowers, mutates the driver-visible block in place).
1154 lines
50 KiB
Rust
1154 lines
50 KiB
Rust
//! `VkVideoSessionKHR` + `VkVideoSessionParametersKHR` lifecycle for H.265 —
|
|
//! [`crate::session`] one codec over, with the leg H.264 does not have: the VPS.
|
|
//!
|
|
//! Vulkan's H.265 session parameters hold THREE parameter-set arrays (VPS, SPS,
|
|
//! PPS) behind `VkVideoDecodeH265SessionParametersAddInfoKHR`, and
|
|
//! `StdVideoDecodeH265PictureInfo` names all three by id
|
|
//! (`sps_video_parameter_set_id`, `pps_seq_parameter_set_id`,
|
|
//! `pps_pic_parameter_set_id`) — so the object must hold the VPS its SPS names, or
|
|
//! the decode op resolves nothing. The versioning rules are Vulkan's, unchanged
|
|
//! from the H.264 ledger:
|
|
//!
|
|
//! - a NEW (vps-id / sps-id / pps-id) is ADDED via
|
|
//! `vkUpdateVideoSessionParametersKHR` with `updateSequenceCount` = previous + 1
|
|
//! (the spec's exact-increment rule — ONE call may carry all three sets, and it
|
|
//! counts as ONE);
|
|
//! - 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_h265`'s `CapacityMismatch` is the trigger for the
|
|
//! DPB half, the extent comparison covers the other.
|
|
//!
|
|
//! **The missing-VPS case is real and handled, not assumed away.** The vendored
|
|
//! parser attaches a VPS to an SPS only when it actually saw the VPS NALU; a
|
|
//! stream joined mid-flight (punktfunk clients join live sessions) can therefore
|
|
//! carry an SPS whose VPS never arrived. `VpsSource` makes that a first-class
|
|
//! state: the ledger stores either the parsed VPS or the SPS the fallback was
|
|
//! synthesized from ([`fallback_vps_from_sps`]), and dedups on THAT — so when the
|
|
//! real VPS finally arrives, the content differs from the fallback and the object
|
|
//! RECREATES onto the real one, exactly as any other content change would.
|
|
//!
|
|
//! ⚠⚠⚠ **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.265 embeds MORE such pointers than any other codec
|
|
//! here — the SPS alone carries seven — so [`StoredParamsH265`] 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. The OUTER pointers
|
|
//! (`pStdVPSs`/`pStdSPSs`/`pStdPPSs`) are held the same way and for the same reason
|
|
//! — [`crate::session`]'s module docs carry the argument and the line it draws.
|
|
//!
|
|
//! `ParamsLedgerH265` is the pure half of that decision table (unit-tested);
|
|
//! `VideoSessionH265` is the thin Vulkan half.
|
|
|
|
use std::rc::Rc;
|
|
|
|
use ash::vk;
|
|
use ash::vk::native as hh;
|
|
use tracing::debug;
|
|
|
|
use crate::caps::DecodeCaps;
|
|
use crate::caps_h265::H265ProfileChain;
|
|
use crate::caps_h265::H265ProfileKey;
|
|
use crate::device::DecodeDevice;
|
|
use crate::params_h265::fallback_vps_from_sps;
|
|
use crate::params_h265::pps_to_std_h265;
|
|
use crate::params_h265::sps_to_std_h265;
|
|
use crate::params_h265::vps_to_std_h265;
|
|
use crate::params_h265::H265ParamsError;
|
|
use crate::params_h265::OwnedStdH265Pps;
|
|
use crate::params_h265::OwnedStdH265Sps;
|
|
use crate::params_h265::OwnedStdH265Vps;
|
|
use crate::params_h265::Pps;
|
|
use crate::params_h265::Sps;
|
|
use crate::params_h265::Vps;
|
|
use crate::session::bind_session_memory;
|
|
use crate::session::ResetArm;
|
|
use crate::session::SessionError;
|
|
|
|
/// Parameter-object capacity. Punktfunk hosts emit one VPS + 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_VPS: usize = 4;
|
|
pub(crate) const MAX_STD_SPS: usize = 4;
|
|
pub(crate) const MAX_STD_PPS: usize = 8;
|
|
|
|
/// Where the VPS an activation needs comes from — and, equally, the ledger's
|
|
/// identity for it.
|
|
///
|
|
/// Comparing whole values (not just ids) is what makes the fallback→real
|
|
/// transition correct: `Parsed` and `FromSps` under the same id are never equal,
|
|
/// so the arrival of the genuine VPS is a content change and recreates the object
|
|
/// instead of silently keeping the synthesized stand-in.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) enum VpsSource {
|
|
/// The stream's own VPS, as the parser attached it to the SPS.
|
|
Parsed(Rc<Vps>),
|
|
/// No VPS NALU was ever seen; the SPS it would be synthesized from
|
|
/// ([`fallback_vps_from_sps`]) stands in as the identity.
|
|
FromSps(Rc<Sps>),
|
|
}
|
|
|
|
impl VpsSource {
|
|
/// The VPS an SPS activation needs.
|
|
pub(crate) fn for_sps(sps: &Rc<Sps>) -> Self {
|
|
match &sps.vps {
|
|
Some(vps) => VpsSource::Parsed(Rc::clone(vps)),
|
|
None => VpsSource::FromSps(Rc::clone(sps)),
|
|
}
|
|
}
|
|
|
|
/// `vps_video_parameter_set_id` — the id the parameters object stores it under
|
|
/// and the SPS names it by.
|
|
pub(crate) fn id(&self) -> u8 {
|
|
match self {
|
|
VpsSource::Parsed(vps) => vps.video_parameter_set_id,
|
|
VpsSource::FromSps(sps) => sps.video_parameter_set_id,
|
|
}
|
|
}
|
|
|
|
/// Convert to the Std struct (owning wrapper).
|
|
pub(crate) fn to_std(&self) -> Result<OwnedStdH265Vps, H265ParamsError> {
|
|
match self {
|
|
VpsSource::Parsed(vps) => vps_to_std_h265(vps),
|
|
VpsSource::FromSps(sps) => fallback_vps_from_sps(sps),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// What the ledger decided for one (VPS, SPS, PPS) activation.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ParamsActionH265 {
|
|
/// All three 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_vps: bool,
|
|
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 IRAP, so pointer
|
|
/// identity means nothing) and the update sequence counter.
|
|
#[derive(Debug, Default)]
|
|
pub(crate) struct ParamsLedgerH265 {
|
|
vps: Vec<(u8, VpsSource)>,
|
|
sps: Vec<(u8, Rc<Sps>)>,
|
|
/// Keyed `(seq_parameter_set_id, pic_parameter_set_id)`, the pair Vulkan
|
|
/// resolves a stored PPS by.
|
|
pps: Vec<((u8, u8), Rc<Pps>)>,
|
|
update_seq: u32,
|
|
}
|
|
|
|
impl ParamsLedgerH265 {
|
|
/// Decide the action for activating (`vps`, `sps`, `pps`). Pure — mutate via
|
|
/// [`Self::commit`].
|
|
pub(crate) fn plan(&self, vps: &VpsSource, sps: &Rc<Sps>, pps: &Rc<Pps>) -> ParamsActionH265 {
|
|
let vps_key = vps.id();
|
|
let sps_key = sps.seq_parameter_set_id;
|
|
let pps_key = (pps.seq_parameter_set_id, pps.pic_parameter_set_id);
|
|
|
|
let stored_vps = self.vps.iter().find(|(id, _)| *id == vps_key);
|
|
let stored_sps = self.sps.iter().find(|(id, _)| *id == sps_key);
|
|
let stored_pps = self.pps.iter().find(|(id, _)| *id == pps_key);
|
|
// Content changes under a stored id — including a fallback VPS being
|
|
// superseded by the real one (VpsSource docs).
|
|
if let Some((_, stored)) = stored_vps {
|
|
if stored != vps {
|
|
return ParamsActionH265::Recreate;
|
|
}
|
|
}
|
|
if let Some((_, stored)) = stored_sps {
|
|
if **stored != **sps {
|
|
return ParamsActionH265::Recreate;
|
|
}
|
|
}
|
|
if let Some((_, stored)) = stored_pps {
|
|
if **stored != **pps {
|
|
return ParamsActionH265::Recreate;
|
|
}
|
|
}
|
|
let add_vps = stored_vps.is_none();
|
|
let add_sps = stored_sps.is_none();
|
|
let add_pps = stored_pps.is_none();
|
|
if !add_vps && !add_sps && !add_pps {
|
|
return ParamsActionH265::Current;
|
|
}
|
|
if (add_vps && self.vps.len() >= MAX_STD_VPS)
|
|
|| (add_sps && self.sps.len() >= MAX_STD_SPS)
|
|
|| (add_pps && self.pps.len() >= MAX_STD_PPS)
|
|
{
|
|
return ParamsActionH265::Recreate;
|
|
}
|
|
ParamsActionH265::Add {
|
|
add_vps,
|
|
add_sps,
|
|
add_pps,
|
|
}
|
|
}
|
|
|
|
/// Apply a decided action. `Add` bumps the sequence count by EXACTLY one (the
|
|
/// Vulkan update rule — one call may carry all three sets); `Recreate` resets
|
|
/// the ledger to just the current triple 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: ParamsActionH265,
|
|
vps: &VpsSource,
|
|
sps: &Rc<Sps>,
|
|
pps: &Rc<Pps>,
|
|
) {
|
|
match action {
|
|
ParamsActionH265::Current => {}
|
|
ParamsActionH265::Add {
|
|
add_vps,
|
|
add_sps,
|
|
add_pps,
|
|
} => {
|
|
if add_vps {
|
|
self.vps.push((vps.id(), vps.clone()));
|
|
}
|
|
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;
|
|
}
|
|
ParamsActionH265::Recreate => {
|
|
self.vps.clear();
|
|
self.sps.clear();
|
|
self.pps.clear();
|
|
self.vps.push((vps.id(), vps.clone()));
|
|
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 SessionConfigH265 {
|
|
pub max_coded_extent: vk::Extent2D,
|
|
pub max_dpb_slots: u32,
|
|
pub max_active_references: u32,
|
|
/// The profile the session was created against — profile idc AND the chroma
|
|
/// format / bit depths, all four of which a stream can renegotiate (an SPS
|
|
/// switching Main→Main 10 mid-stream is a session rebuild, not an update).
|
|
pub profile: H265ProfileKey,
|
|
/// The device's `maxLevelIdc` for this profile (Std code point). Every VPS/SPS
|
|
/// handed to the parameters object has its declared level clamped to this —
|
|
/// over-declared levels are common (AMF stamps 6.2 on 4K streams) and a set
|
|
/// above the ceiling is invalid usage, while the stream's real demands are
|
|
/// already enforced by `max_coded_extent` / `max_dpb_slots`.
|
|
pub max_level_idc: hh::StdVideoH265LevelIdc,
|
|
}
|
|
|
|
/// A live parameters object **and every Std parameter set it was given**, in one
|
|
/// field — because the two may not drift apart. `session::StoredParams` one codec
|
|
/// over, with the VPS leg H.264 does not have.
|
|
///
|
|
/// 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. H.265 has the most to lose: its Std
|
|
/// SPS points at a profile/tier/level block, a DPB-manager block, scaling lists,
|
|
/// the short-term RPS candidate array and the long-term SPS candidates.
|
|
///
|
|
/// What is pinned, precisely: the wrappers' BOXED blocks, which is what the driver
|
|
/// was measured retaining. The contiguous array of outer `StdVideoH265*` 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_h265::moving_the_wrapper_leaves_the_driver_s_pointers_put`
|
|
/// pins the half that matters.
|
|
struct StoredParamsH265 {
|
|
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.
|
|
vps: Vec<OwnedStdH265Vps>,
|
|
sps: Vec<OwnedStdH265Sps>,
|
|
pps: Vec<OwnedStdH265Pps>,
|
|
/// The contiguous Std ARRAYS the create call was handed as
|
|
/// `pStdVPSs`/`pStdSPSs`/`pStdPPSs` — the OUTER pointers, held for the object's
|
|
/// life for the reason the wrappers are ([`crate::session`]'s `StoredParams`
|
|
/// carries the argument). Built by [`Self::assemble`] at their final address.
|
|
std_vps: Vec<hh::StdVideoH265VideoParameterSet>,
|
|
std_sps: Vec<hh::StdVideoH265SequenceParameterSet>,
|
|
std_pps: Vec<hh::StdVideoH265PictureParameterSet>,
|
|
}
|
|
|
|
impl StoredParamsH265 {
|
|
/// The wrappers plus the contiguous Std arrays the create call reads its
|
|
/// `pStdVPSs`/`pStdSPSs`/`pStdPPSs` out of, with a NULL object the caller fills
|
|
/// in once `vkCreateVideoSessionParametersKHR` has succeeded
|
|
/// ([`crate::session`]'s `StoredParams::assemble` for why it happens here).
|
|
fn assemble(
|
|
vps: Vec<OwnedStdH265Vps>,
|
|
sps: Vec<OwnedStdH265Sps>,
|
|
pps: Vec<OwnedStdH265Pps>,
|
|
) -> 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_vps = vps.iter().map(|o| *o.std()).collect();
|
|
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(),
|
|
vps,
|
|
sps,
|
|
pps,
|
|
std_vps,
|
|
std_sps,
|
|
std_pps,
|
|
}
|
|
}
|
|
|
|
/// The placeholder a half-built session holds. `vkDestroyVideoSessionParametersKHR`
|
|
/// ignores a NULL handle, so a [`VideoSessionH265::create`] that fails before
|
|
/// the object exists still drops cleanly.
|
|
fn none() -> Self {
|
|
Self::assemble(Vec::new(), 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,
|
|
vps: Option<OwnedStdH265Vps>,
|
|
sps: Option<OwnedStdH265Sps>,
|
|
pps: Option<OwnedStdH265Pps>,
|
|
) {
|
|
self.vps.extend(vps);
|
|
self.sps.extend(sps);
|
|
self.pps.extend(pps);
|
|
}
|
|
}
|
|
|
|
/// The Vulkan half: session + bound memory + parameters object.
|
|
pub(crate) struct VideoSessionH265 {
|
|
device: ash::Device,
|
|
video_queue: ash::khr::video_queue::Device,
|
|
session: vk::VideoSessionKHR,
|
|
memory: Vec<vk::DeviceMemory>,
|
|
parameters: StoredParamsH265,
|
|
ledger: ParamsLedgerH265,
|
|
pub(crate) config: SessionConfigH265,
|
|
/// The session has never run a coding scope: the first one records a
|
|
/// `VK_VIDEO_CODING_CONTROL_RESET_BIT_KHR` control before anything else.
|
|
needs_reset: ResetArm,
|
|
}
|
|
|
|
impl VideoSessionH265 {
|
|
/// 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: SessionConfigH265,
|
|
) -> Result<Self, SessionError> {
|
|
let mut chain = H265ProfileChain::new(config.profile);
|
|
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: StoredParamsH265::none(),
|
|
ledger: ParamsLedgerH265::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(), Vec::new())?;
|
|
}
|
|
Ok(built)
|
|
}
|
|
|
|
/// Create a parameters object holding exactly `vps`/`sps`/`pps` (any 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, [`StoredParamsH265`]).
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// Live device + live session.
|
|
unsafe fn create_parameters_object(
|
|
&self,
|
|
vps: Vec<OwnedStdH265Vps>,
|
|
sps: Vec<OwnedStdH265Sps>,
|
|
pps: Vec<OwnedStdH265Pps>,
|
|
) -> Result<StoredParamsH265, SessionError> {
|
|
// Assembled FIRST so the arrays `pStdVPSs`/`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 = StoredParamsH265::assemble(vps, sps, pps);
|
|
let add = vk::VideoDecodeH265SessionParametersAddInfoKHR::default()
|
|
.std_vp_ss(&stored.std_vps)
|
|
.std_sp_ss(&stored.std_sps)
|
|
.std_pp_ss(&stored.std_pps);
|
|
let mut h265 = vk::VideoDecodeH265SessionParametersCreateInfoKHR::default()
|
|
.max_std_vps_count(MAX_STD_VPS as u32)
|
|
.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 h265);
|
|
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 (`vps`, `sps`, `pps`), without mutating
|
|
/// anything — the decoder consults this BEFORE [`Self::ensure_parameters`] so
|
|
/// a [`ParamsActionH265::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,
|
|
vps: &VpsSource,
|
|
sps: &Rc<Sps>,
|
|
pps: &Rc<Pps>,
|
|
) -> ParamsActionH265 {
|
|
self.ledger.plan(vps, sps, pps)
|
|
}
|
|
|
|
/// Make the parameters object hold this AU's activated (VPS, SPS, PPS),
|
|
/// converting through the params module and Adding/Recreating per the ledger.
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// Live device; when [`Self::parameters_action`] says `Recreate`, the caller
|
|
/// has ALREADY drained every in-flight decode — the old object is destroyed
|
|
/// here, and a still-executing decode reading it would be use-after-free at
|
|
/// the driver level. `Current`/`Add` touch no object a submitted decode can be
|
|
/// reading.
|
|
pub(crate) unsafe fn ensure_parameters(
|
|
&mut self,
|
|
vps: &VpsSource,
|
|
sps: &Rc<Sps>,
|
|
pps: &Rc<Pps>,
|
|
) -> Result<(), SessionError> {
|
|
let action = self.ledger.plan(vps, sps, pps);
|
|
match action {
|
|
ParamsActionH265::Current => Ok(()),
|
|
ParamsActionH265::Add {
|
|
add_vps,
|
|
add_sps,
|
|
add_pps,
|
|
} => {
|
|
// Every owned wrapper below stays alive until after the update
|
|
// call: the Std structs embed pointers into their heap blocks.
|
|
let mut owned_vps = if add_vps { Some(vps.to_std()?) } else { None };
|
|
let mut owned_sps = if add_sps {
|
|
Some(sps_to_std_h265(sps)?)
|
|
} else {
|
|
None
|
|
};
|
|
if let Some(v) = owned_vps.as_mut() {
|
|
v.clamp_level(self.config.max_level_idc);
|
|
}
|
|
if let Some(s) = owned_sps.as_mut() {
|
|
s.clamp_level(self.config.max_level_idc);
|
|
}
|
|
let owned_pps = if add_pps {
|
|
Some(pps_to_std_h265(pps)?)
|
|
} else {
|
|
None
|
|
};
|
|
let vps_slice: &[hh::StdVideoH265VideoParameterSet] = match &owned_vps {
|
|
Some(o) => std::slice::from_ref(o.std()),
|
|
None => &[],
|
|
};
|
|
let sps_slice: &[hh::StdVideoH265SequenceParameterSet] = match &owned_sps {
|
|
Some(o) => std::slice::from_ref(o.std()),
|
|
None => &[],
|
|
};
|
|
let pps_slice: &[hh::StdVideoH265PictureParameterSet] = match &owned_pps {
|
|
Some(o) => std::slice::from_ref(o.std()),
|
|
None => &[],
|
|
};
|
|
let mut add = vk::VideoDecodeH265SessionParametersAddInfoKHR::default()
|
|
.std_vp_ss(vps_slice)
|
|
.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_vps, owned_sps, owned_pps);
|
|
self.ledger.commit(action, vps, sps, pps);
|
|
Ok(())
|
|
}
|
|
ParamsActionH265::Recreate => {
|
|
debug!(
|
|
vps_id = vps.id(),
|
|
sps_id = sps.seq_parameter_set_id,
|
|
pps_id = pps.pic_parameter_set_id,
|
|
"recreating H.265 session parameters (content change or capacity)"
|
|
);
|
|
let mut owned_vps = vps.to_std()?;
|
|
let mut owned_sps = sps_to_std_h265(sps)?;
|
|
owned_vps.clamp_level(self.config.max_level_idc);
|
|
owned_sps.clamp_level(self.config.max_level_idc);
|
|
let owned_pps = pps_to_std_h265(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_vps],
|
|
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, vps, 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();
|
|
}
|
|
}
|
|
|
|
impl Drop for VideoSessionH265 {
|
|
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 (`crate::session::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 super::*;
|
|
|
|
/// The vendored H.265 parser has no set builders (unlike its H.264 half), and
|
|
/// `Pps` derives no `Default` — so fixtures are written out once here. Only
|
|
/// the fields the ledger keys or compares carry meaning.
|
|
fn authored_sps(sps_id: u8, vps_id: u8, width: u16) -> Rc<Sps> {
|
|
Rc::new(Sps {
|
|
video_parameter_set_id: vps_id,
|
|
seq_parameter_set_id: sps_id,
|
|
chroma_format_idc: 1,
|
|
pic_width_in_luma_samples: width,
|
|
pic_height_in_luma_samples: 64,
|
|
..Default::default()
|
|
})
|
|
}
|
|
|
|
/// The same SPS with the stream's VPS attached, as the parser would.
|
|
fn with_vps(sps: &Rc<Sps>, vps_id: u8, max_layers_minus1: u8) -> Rc<Sps> {
|
|
let vps = Vps {
|
|
video_parameter_set_id: vps_id,
|
|
max_layers_minus1,
|
|
..Default::default()
|
|
};
|
|
Rc::new(Sps {
|
|
vps: Some(Rc::new(vps)),
|
|
..(**sps).clone()
|
|
})
|
|
}
|
|
|
|
fn authored_pps(sps: &Rc<Sps>, pps_id: u8, init_qp_minus26: i8) -> Rc<Pps> {
|
|
Rc::new(Pps {
|
|
pic_parameter_set_id: pps_id,
|
|
seq_parameter_set_id: sps.seq_parameter_set_id,
|
|
dependent_slice_segments_enabled_flag: false,
|
|
output_flag_present_flag: false,
|
|
num_extra_slice_header_bits: 0,
|
|
sign_data_hiding_enabled_flag: false,
|
|
cabac_init_present_flag: false,
|
|
num_ref_idx_l0_default_active_minus1: 0,
|
|
num_ref_idx_l1_default_active_minus1: 0,
|
|
init_qp_minus26,
|
|
constrained_intra_pred_flag: false,
|
|
transform_skip_enabled_flag: false,
|
|
cu_qp_delta_enabled_flag: false,
|
|
diff_cu_qp_delta_depth: 0,
|
|
cb_qp_offset: 0,
|
|
cr_qp_offset: 0,
|
|
slice_chroma_qp_offsets_present_flag: false,
|
|
weighted_pred_flag: false,
|
|
weighted_bipred_flag: false,
|
|
transquant_bypass_enabled_flag: false,
|
|
tiles_enabled_flag: false,
|
|
entropy_coding_sync_enabled_flag: false,
|
|
num_tile_columns_minus1: 0,
|
|
num_tile_rows_minus1: 0,
|
|
uniform_spacing_flag: true,
|
|
column_width_minus1: [0; 19],
|
|
row_height_minus1: [0; 21],
|
|
loop_filter_across_tiles_enabled_flag: true,
|
|
loop_filter_across_slices_enabled_flag: false,
|
|
deblocking_filter_control_present_flag: false,
|
|
deblocking_filter_override_enabled_flag: false,
|
|
deblocking_filter_disabled_flag: false,
|
|
beta_offset_div2: 0,
|
|
tc_offset_div2: 0,
|
|
scaling_list_data_present_flag: false,
|
|
scaling_list: Default::default(),
|
|
lists_modification_present_flag: false,
|
|
log2_parallel_merge_level_minus2: 0,
|
|
slice_segment_header_extension_present_flag: false,
|
|
extension_present_flag: false,
|
|
range_extension_flag: false,
|
|
range_extension: Default::default(),
|
|
scc_extension_flag: false,
|
|
scc_extension: Default::default(),
|
|
qp_bd_offset_y: 0,
|
|
sps: Rc::clone(sps),
|
|
})
|
|
}
|
|
|
|
/// 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. [`StoredParamsH265::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
|
|
/// `pDecPicBufMgr` or `pProfileTierLevel` 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() {
|
|
// The ledger fixtures never convert, so they leave `general_profile_idc`
|
|
// at the unmappable 0; Std conversion needs a real one.
|
|
let mut base = (*authored_sps(0, 0, 64)).clone();
|
|
base.profile_tier_level.general_profile_idc = 1; // Main
|
|
base.max_dec_pic_buffering_minus1 = [5, 0, 0, 0, 0, 0, 0];
|
|
let sps = Rc::new(base);
|
|
let owned_vps = VpsSource::for_sps(&sps).to_std().expect("converts");
|
|
let owned_sps = sps_to_std_h265(&sps).expect("converts");
|
|
let vps_ptl = owned_vps.std().pProfileTierLevel;
|
|
let sps_dpb = owned_sps.std().pDecPicBufMgr;
|
|
assert!(!vps_ptl.is_null() && !sps_dpb.is_null());
|
|
|
|
let mut stored = StoredParamsH265::none();
|
|
stored.adopt(Some(owned_vps), Some(owned_sps), None);
|
|
assert_eq!(
|
|
(stored.vps.len(), stored.sps.len(), stored.pps.len()),
|
|
(1, 1, 0),
|
|
"the object took the sets themselves, not borrows of them"
|
|
);
|
|
assert_eq!(stored.vps[0].std().pProfileTierLevel, vps_ptl);
|
|
assert_eq!(stored.sps[0].std().pDecPicBufMgr, sps_dpb);
|
|
// SAFETY: `stored` owns both blocks — which is exactly the property here.
|
|
let dpb = unsafe { (*sps_dpb).max_dec_pic_buffering_minus1[0] };
|
|
assert_eq!(dpb, 5, "the fixture's DPB sizing, 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 [`StoredParamsH265`]. The
|
|
/// test above covers a driver retaining `pDecPicBufMgr` (an INNER pointer);
|
|
/// this covers one retaining `pStdVPSs`/`pStdSPSs`/`pStdPPSs`, which the same
|
|
/// wording in the spec permits just as much.
|
|
#[test]
|
|
fn an_added_set_keeps_the_address_the_update_call_was_given() {
|
|
let mut base = (*authored_sps(0, 0, 64)).clone();
|
|
base.profile_tier_level.general_profile_idc = 1; // Main
|
|
let sps = Rc::new(base);
|
|
let pps = authored_pps(&sps, 0, 0);
|
|
let owned_vps = VpsSource::for_sps(&sps).to_std().expect("converts");
|
|
let owned_sps = sps_to_std_h265(&sps).expect("converts");
|
|
let owned_pps = pps_to_std_h265(&pps).expect("converts");
|
|
// Exactly what `ensure_parameters` puts in `pStdVPSs`/`pStdSPSs`/`pStdPPSs`.
|
|
let handed_vps = std::ptr::from_ref(owned_vps.std());
|
|
let handed_sps = std::ptr::from_ref(owned_sps.std());
|
|
let handed_pps = std::ptr::from_ref(owned_pps.std());
|
|
|
|
let mut stored = StoredParamsH265::none();
|
|
stored.adopt(Some(owned_vps), Some(owned_sps), Some(owned_pps));
|
|
assert_eq!(
|
|
(
|
|
std::ptr::from_ref(stored.vps[0].std()),
|
|
std::ptr::from_ref(stored.sps[0].std()),
|
|
std::ptr::from_ref(stored.pps[0].std()),
|
|
),
|
|
(handed_vps, handed_sps, handed_pps),
|
|
"the addresses handed to Vulkan must be the ones the object keeps"
|
|
);
|
|
// SAFETY: `stored` owns all three — which is exactly the property here.
|
|
let ids = unsafe {
|
|
(
|
|
(*handed_vps).vps_video_parameter_set_id,
|
|
(*handed_sps).sps_seq_parameter_set_id,
|
|
(*handed_pps).pps_pic_parameter_set_id,
|
|
)
|
|
};
|
|
assert_eq!(ids, (0, 0, 0), "read back through the driver's pointers");
|
|
}
|
|
|
|
/// The create path's OUTER pointers: `pStdVPSs`/`pStdSPSs`/`pStdPPSs` address
|
|
/// contiguous COPIES of the wrappers' Std structs, and those arrays must
|
|
/// outlive the create call the same way the wrappers do.
|
|
///
|
|
/// [`StoredParamsH265::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. Before this they were function-local `Vec`s, dropped
|
|
/// the instant `create_parameters_object` returned.
|
|
#[test]
|
|
fn the_std_arrays_the_create_call_is_given_are_the_ones_the_object_keeps() {
|
|
let mut base = (*authored_sps(0, 0, 64)).clone();
|
|
base.profile_tier_level.general_profile_idc = 1; // Main
|
|
let sps = Rc::new(base);
|
|
let pps = authored_pps(&sps, 0, 0);
|
|
let owned_vps = VpsSource::for_sps(&sps).to_std().expect("converts");
|
|
let owned_sps = sps_to_std_h265(&sps).expect("converts");
|
|
let owned_pps = pps_to_std_h265(&pps).expect("converts");
|
|
|
|
let stored = StoredParamsH265::assemble(vec![owned_vps], vec![owned_sps], vec![owned_pps]);
|
|
// Exactly what `create_parameters_object` hands the create call.
|
|
let handed = (
|
|
stored.std_vps.as_ptr(),
|
|
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_vps.len(),
|
|
stored.std_sps.len(),
|
|
stored.std_pps.len()
|
|
),
|
|
(1, 1, 1)
|
|
);
|
|
assert_eq!(
|
|
(
|
|
stored.std_vps.as_ptr(),
|
|
stored.std_sps.as_ptr(),
|
|
stored.std_pps.as_ptr(),
|
|
),
|
|
handed,
|
|
"the arrays handed to Vulkan must be the ones the object keeps"
|
|
);
|
|
// And their COPIES still address the wrappers' own live blocks.
|
|
let (ptl, dpb) = (
|
|
stored.std_vps[0].pProfileTierLevel,
|
|
stored.std_sps[0].pDecPicBufMgr,
|
|
);
|
|
assert!(!ptl.is_null() && !dpb.is_null(), "both are always attached");
|
|
assert_eq!(ptl, stored.vps[0].std().pProfileTierLevel);
|
|
assert_eq!(dpb, stored.sps[0].std().pDecPicBufMgr);
|
|
// SAFETY: `stored` owns both blocks — which is exactly the property here.
|
|
let profile_idc = unsafe { (*ptl).general_profile_idc };
|
|
assert_eq!(profile_idc, 1, "the fixture's Main profile, read back live");
|
|
assert_eq!(
|
|
stored.std_pps[0].pps_pic_parameter_set_id,
|
|
stored.pps[0].std().pps_pic_parameter_set_id,
|
|
"the PPS copy is the wrapper's, field for field"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_first_activation_adds_all_three_sets_in_one_update_call() {
|
|
let sps = with_vps(&authored_sps(0, 0, 64), 0, 0);
|
|
let vps = VpsSource::for_sps(&sps);
|
|
let pps = authored_pps(&sps, 0, 0);
|
|
|
|
let mut ledger = ParamsLedgerH265::default();
|
|
assert_eq!(ledger.next_update_seq(), 1);
|
|
let action = ledger.plan(&vps, &sps, &pps);
|
|
assert_eq!(
|
|
action,
|
|
ParamsActionH265::Add {
|
|
add_vps: true,
|
|
add_sps: true,
|
|
add_pps: true
|
|
}
|
|
);
|
|
ledger.commit(action, &vps, &sps, &pps);
|
|
// ONE call carried all three: the counter moves by exactly one.
|
|
assert_eq!(ledger.next_update_seq(), 2);
|
|
assert_eq!(ledger.plan(&vps, &sps, &pps), ParamsActionH265::Current);
|
|
}
|
|
|
|
#[test]
|
|
fn a_reactivated_identical_triple_is_current_even_across_reparses() {
|
|
// The parser re-parses in-band sets at every IRAP: same content, NEW Rcs.
|
|
let sps_a = with_vps(&authored_sps(0, 0, 64), 0, 0);
|
|
let sps_b = with_vps(&authored_sps(0, 0, 64), 0, 0);
|
|
assert!(!Rc::ptr_eq(&sps_a, &sps_b));
|
|
let (vps_a, vps_b) = (VpsSource::for_sps(&sps_a), VpsSource::for_sps(&sps_b));
|
|
assert_eq!(vps_a, vps_b, "identical content, distinct allocations");
|
|
let pps_a = authored_pps(&sps_a, 0, 0);
|
|
let pps_b = authored_pps(&sps_b, 0, 0);
|
|
|
|
let mut ledger = ParamsLedgerH265::default();
|
|
let action = ledger.plan(&vps_a, &sps_a, &pps_a);
|
|
ledger.commit(action, &vps_a, &sps_a, &pps_a);
|
|
assert_eq!(
|
|
ledger.plan(&vps_b, &sps_b, &pps_b),
|
|
ParamsActionH265::Current
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_new_pps_id_over_stored_vps_and_sps_adds_only_the_pps() {
|
|
let sps = with_vps(&authored_sps(0, 0, 64), 0, 0);
|
|
let vps = VpsSource::for_sps(&sps);
|
|
let pps0 = authored_pps(&sps, 0, 0);
|
|
let pps1 = authored_pps(&sps, 1, 0);
|
|
|
|
let mut ledger = ParamsLedgerH265::default();
|
|
let action = ledger.plan(&vps, &sps, &pps0);
|
|
ledger.commit(action, &vps, &sps, &pps0);
|
|
assert_eq!(
|
|
ledger.plan(&vps, &sps, &pps1),
|
|
ParamsActionH265::Add {
|
|
add_vps: false,
|
|
add_sps: false,
|
|
add_pps: true
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_stream_with_no_vps_nalu_stores_the_fallback_and_recreates_when_the_real_one_arrives() {
|
|
// Joined mid-stream: the SPS arrived without its VPS, so the ledger's
|
|
// identity is the SPS the fallback would be synthesized from.
|
|
let sps_no_vps = authored_sps(0, 0, 64);
|
|
assert!(sps_no_vps.vps.is_none());
|
|
let fallback = VpsSource::for_sps(&sps_no_vps);
|
|
assert!(matches!(fallback, VpsSource::FromSps(_)));
|
|
assert_eq!(fallback.id(), 0, "the id comes off the SPS's vps id");
|
|
let pps = authored_pps(&sps_no_vps, 0, 0);
|
|
|
|
let mut ledger = ParamsLedgerH265::default();
|
|
let action = ledger.plan(&fallback, &sps_no_vps, &pps);
|
|
assert_eq!(
|
|
action,
|
|
ParamsActionH265::Add {
|
|
add_vps: true,
|
|
add_sps: true,
|
|
add_pps: true
|
|
}
|
|
);
|
|
ledger.commit(action, &fallback, &sps_no_vps, &pps);
|
|
// Re-activating the same VPS-less SPS is Current, not a churn of Adds.
|
|
assert_eq!(
|
|
ledger.plan(&fallback, &sps_no_vps, &pps),
|
|
ParamsActionH265::Current
|
|
);
|
|
|
|
// The real VPS finally arrives (same id 0). Vulkan cannot replace a
|
|
// stored set, so the fallback→real transition RECREATES. (In a real
|
|
// stream the SPS changes with it — it now carries the VPS — so the
|
|
// recreate is over-determined; the VPS leg on its own is isolated in
|
|
// `changed_vps_content_under_a_stored_id_recreates_with_the_sps_and_pps_untouched`.)
|
|
let sps_with_vps = with_vps(&sps_no_vps, 0, 0);
|
|
let real = VpsSource::for_sps(&sps_with_vps);
|
|
assert!(matches!(real, VpsSource::Parsed(_)));
|
|
assert_ne!(real, fallback, "a synthesized VPS is not the parsed one");
|
|
let pps = authored_pps(&sps_with_vps, 0, 0);
|
|
let action = ledger.plan(&real, &sps_with_vps, &pps);
|
|
assert_eq!(action, ParamsActionH265::Recreate);
|
|
ledger.commit(action, &real, &sps_with_vps, &pps);
|
|
assert_eq!(
|
|
ledger.next_update_seq(),
|
|
1,
|
|
"a fresh object restarts its counter"
|
|
);
|
|
assert_eq!(
|
|
ledger.plan(&real, &sps_with_vps, &pps),
|
|
ParamsActionH265::Current
|
|
);
|
|
}
|
|
|
|
/// A VPS built directly, decoupled from any SPS. The ledger keys its three
|
|
/// arrays independently, so this is how the VPS leg is exercised ALONE — in a
|
|
/// real stream an SPS carries its VPS, so any VPS change drags the SPS's
|
|
/// content along and the recreate becomes over-determined.
|
|
fn standalone_vps(vps_id: u8, max_layers_minus1: u8) -> VpsSource {
|
|
VpsSource::Parsed(Rc::new(Vps {
|
|
video_parameter_set_id: vps_id,
|
|
max_layers_minus1,
|
|
..Default::default()
|
|
}))
|
|
}
|
|
|
|
#[test]
|
|
fn changed_vps_content_under_a_stored_id_recreates_with_the_sps_and_pps_untouched() {
|
|
let sps = authored_sps(0, 0, 64);
|
|
let pps = authored_pps(&sps, 0, 0);
|
|
let vps = standalone_vps(0, 0);
|
|
let mut ledger = ParamsLedgerH265::default();
|
|
let action = ledger.plan(&vps, &sps, &pps);
|
|
ledger.commit(action, &vps, &sps, &pps);
|
|
assert_eq!(ledger.plan(&vps, &sps, &pps), ParamsActionH265::Current);
|
|
|
|
// Same VPS id, different content: SPS and PPS are byte-identical and
|
|
// would be `Current` on their own, yet the object still has to be
|
|
// rebuilt — Vulkan has no in-place replacement for a stored set.
|
|
let vps2 = standalone_vps(0, 1);
|
|
assert_eq!(vps2.id(), vps.id());
|
|
assert_ne!(vps2, vps);
|
|
assert_eq!(ledger.plan(&vps2, &sps, &pps), ParamsActionH265::Recreate);
|
|
|
|
// A NEW vps id over the same SPS/PPS is an Add of the VPS alone.
|
|
let vps3 = standalone_vps(1, 0);
|
|
assert_eq!(
|
|
ledger.plan(&vps3, &sps, &pps),
|
|
ParamsActionH265::Add {
|
|
add_vps: true,
|
|
add_sps: false,
|
|
add_pps: false
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn changed_sps_or_pps_content_under_a_stored_id_recreates_and_resets_the_sequence() {
|
|
let sps = with_vps(&authored_sps(0, 0, 64), 0, 0);
|
|
let vps = VpsSource::for_sps(&sps);
|
|
let pps = authored_pps(&sps, 0, 0);
|
|
let mut ledger = ParamsLedgerH265::default();
|
|
let action = ledger.plan(&vps, &sps, &pps);
|
|
ledger.commit(action, &vps, &sps, &pps);
|
|
assert_eq!(ledger.next_update_seq(), 2, "one Add happened");
|
|
|
|
// Same PPS id, different content (init qp changed).
|
|
let pps2 = authored_pps(&sps, 0, 4);
|
|
let action = ledger.plan(&vps, &sps, &pps2);
|
|
assert_eq!(action, ParamsActionH265::Recreate);
|
|
ledger.commit(action, &vps, &sps, &pps2);
|
|
assert_eq!(ledger.next_update_seq(), 1);
|
|
assert_eq!(ledger.plan(&vps, &sps, &pps2), ParamsActionH265::Current);
|
|
|
|
// Same SPS id, different content (a resize the extent check would also
|
|
// catch — but the ledger must not depend on that).
|
|
let sps2 = with_vps(&authored_sps(0, 0, 128), 0, 0);
|
|
let vps2 = VpsSource::for_sps(&sps2);
|
|
let pps3 = authored_pps(&sps2, 0, 4);
|
|
assert_eq!(ledger.plan(&vps2, &sps2, &pps3), ParamsActionH265::Recreate);
|
|
}
|
|
|
|
#[test]
|
|
fn capacity_overflow_on_any_of_the_three_arrays_recreates_with_just_the_current_triple() {
|
|
let sps = with_vps(&authored_sps(0, 0, 64), 0, 0);
|
|
let vps = VpsSource::for_sps(&sps);
|
|
let mut ledger = ParamsLedgerH265::default();
|
|
|
|
// Fill the PPS capacity under one VPS/SPS pair.
|
|
let first = authored_pps(&sps, 0, 0);
|
|
let action = ledger.plan(&vps, &sps, &first);
|
|
ledger.commit(action, &vps, &sps, &first);
|
|
for pps_id in 1..MAX_STD_PPS as u8 {
|
|
let pps = authored_pps(&sps, pps_id, 0);
|
|
let action = ledger.plan(&vps, &sps, &pps);
|
|
assert!(matches!(action, ParamsActionH265::Add { .. }));
|
|
ledger.commit(action, &vps, &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
|
|
// (and the VPS/SPS with it — the recreate kept only the current triple).
|
|
let overflow = authored_pps(&sps, MAX_STD_PPS as u8, 0);
|
|
let action = ledger.plan(&vps, &sps, &overflow);
|
|
assert_eq!(action, ParamsActionH265::Recreate);
|
|
ledger.commit(action, &vps, &sps, &overflow);
|
|
assert_eq!(
|
|
ledger.plan(&vps, &sps, &first),
|
|
ParamsActionH265::Add {
|
|
add_vps: false,
|
|
add_sps: false,
|
|
add_pps: true
|
|
},
|
|
"sets evicted by a recreate re-add on next activation"
|
|
);
|
|
|
|
// The VPS array overflows the same way, and on its own: MAX_STD_VPS
|
|
// distinct ids fit over one fixed SPS/PPS pair, the next one recreates.
|
|
let mut ledger = ParamsLedgerH265::default();
|
|
let sps = authored_sps(0, 0, 64);
|
|
let pps = authored_pps(&sps, 0, 0);
|
|
for vps_id in 0..MAX_STD_VPS as u8 {
|
|
let vps = standalone_vps(vps_id, 0);
|
|
let action = ledger.plan(&vps, &sps, &pps);
|
|
assert!(matches!(
|
|
action,
|
|
ParamsActionH265::Add { add_vps: true, .. }
|
|
));
|
|
ledger.commit(action, &vps, &sps, &pps);
|
|
}
|
|
let overflow_vps = standalone_vps(MAX_STD_VPS as u8, 0);
|
|
assert_eq!(
|
|
ledger.plan(&overflow_vps, &sps, &pps),
|
|
ParamsActionH265::Recreate,
|
|
"a fifth VPS id overflows the array even though SPS and PPS are current"
|
|
);
|
|
}
|
|
}
|