ci / bun-nix (pull_request) Successful in 38s
apple / swift (pull_request) Successful in 1m31s
apple / screenshots (pull_request) Skipped
ci / web (pull_request) Successful in 2m11s
ci / docs-site (pull_request) Successful in 2m21s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m44s
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m10s
ci / rust-arm64 (pull_request) Successful in 6m29s
android / android (pull_request) Successful in 7m44s
ci / rust (pull_request) Canceled after 9m40s
nix / flake (pull_request) Canceled after 9m38s
The Intel Arc refusal moved one step down the caps query and stopped again: the coincide NV12 entry does not advertise SAMPLED. That sentence is punktfunk's, not the driver's, and the last two times a conclusion was drawn from a sentence of ours the conclusion was wrong. So --probe-decode now prints the driver's own answers instead. For every profile the client can negotiate (H.264 High, H.265 Main and Main 10, AV1 Main 8- and 10-bit) it asks vkGetPhysicalDeviceVideoFormatPropertiesKHR in six usage combinations — the three the image pools really create with, plus DPB|DST without sampling, SAMPLED alone and DST alone, which are what localise a refusal to a half. Each answer is printed as the driver gave it: format, usage and create flags named AND in hex with unrecognised bits called out, image type, tiling. A failed query prints its VkResult rather than vanishing into an empty list. It goes through pf-vkdecode's own query rather than a copy of it, which meant splitting query_formats into a physical-device form — the call never needed the VkDevice the old signature demanded. VideoFormat gains imageType and imageTiling to carry the whole record; VUID-VkImageCreateInfo-pNext-06811 compares both for equality, so they were being assumed rather than read. And because a driver that under-reports usage would be indistinguishable from one that genuinely lacks it, the probe asks a second, independent question — vkGetPhysicalDeviceImageFormatProperties2 over the same profile list — and prints it only where the two disagree. A disagreement is the finding. No behaviour change to any decode path: derivation reads the same fields it did.
585 lines
22 KiB
Rust
585 lines
22 KiB
Rust
//! Decode image pools — the FFmpeg pool model, zero-copy:
|
|
//!
|
|
//! The PICTURE POOL is decoupled from DPB slots. Images outnumber slots by
|
|
//! [`HOLD_HEADROOM`], and a DPB slot binds an image at ACTIVATION time — a
|
|
//! re-activated slot may bind a DIFFERENT free image (spec-legal with
|
|
//! `SEPARATE_REFERENCE_IMAGES`, which the caps derivation requires for coincide
|
|
//! mode). A picture the consumer still holds is therefore NEVER a decode target:
|
|
//! its image simply stays off the free list until the release token returns.
|
|
//! This is the exact contract the presenter already speaks on the AVVkFrame path,
|
|
//! re-implemented without FFmpeg in the middle.
|
|
//!
|
|
//! - **coincide** (RADV): pool images are DPB + decode output + sampled surface
|
|
//! in one (`DPB|DST|SAMPLED`, per-slot images).
|
|
//! - **distinct** (NVIDIA): a separate reference-only DPB array (layered or
|
|
//! per-slot — never delivered, so its slot↔layer mapping stays fixed) plus the
|
|
//! pool as decode outputs (`DST|SAMPLED`).
|
|
//!
|
|
//! Every pool image carries its OWN timeline semaphore (the AVVkFrame contract):
|
|
//! the decoder signals `value+1` when it writes the image; the presenter waits
|
|
//! that value, samples, restores the layout, and signals `value+1` again in the
|
|
//! same submission — the decoder's ledger learns of that write-back at
|
|
//! `release_frame` and waits it before the image's next use.
|
|
|
|
use ash::vk;
|
|
|
|
use crate::caps::DecodeCaps;
|
|
use crate::caps::DecodeProfile;
|
|
use crate::caps::COINCIDE_USAGE;
|
|
use crate::caps::DPB_USAGE;
|
|
use crate::caps::OUTPUT_USAGE;
|
|
use crate::device::find_memory_type_preferring;
|
|
use crate::device::AllocError;
|
|
use crate::device::DecodeDevice;
|
|
|
|
/// Picture-pool headroom on top of the stream's DPB needs: how many decoded
|
|
/// pictures the CONSUMER may hold (delivered, unreleased) before the decoder
|
|
/// reports backpressure. The real client pipeline holds ~4-7 frames at steady
|
|
/// state (two bounded(2) channels, the FrameStore's 1..=3 preroll, the in-flight
|
|
/// present and the retired-frame slot), so 8 gives it a frame of slack; a
|
|
/// consumer holding MORE than this earns the `NoFreeSlot` error, which then
|
|
/// means exactly what it says.
|
|
///
|
|
/// (The 2026-08 .25 field failure taught the sizing lesson the hard way: any
|
|
/// FIXED pool ignoring the stream's DPB depth starves on a clean stream — the
|
|
/// vendored 25fps vector alone keeps `max_dpb_frames + 1 = 8` pictures resident.
|
|
/// Pool size is always `required_slots + HOLD_HEADROOM`.)
|
|
pub const HOLD_HEADROOM: u32 = 8;
|
|
|
|
/// The pure pool shape for one (caps, required-slots) pair.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct PoolPlan {
|
|
/// Distinct-mode reference-only DPB array; 0 images in coincide mode (the
|
|
/// picture pool IS the DPB backing there).
|
|
pub dpb_image_count: u32,
|
|
pub dpb_layers_per_image: u32,
|
|
pub dpb_usage: vk::ImageUsageFlags,
|
|
/// The decoupled picture pool: decode outputs + (coincide) DPB bindings.
|
|
pub picture_count: u32,
|
|
pub picture_usage: vk::ImageUsageFlags,
|
|
pub picture_flags: vk::ImageCreateFlags,
|
|
}
|
|
|
|
/// Decide the pool shape. Pure — unit-tested below.
|
|
///
|
|
/// `required_slots` is the stream's `max_dpb_frames + 1`; the picture pool adds
|
|
/// [`HOLD_HEADROOM`] on top so consumer-held pictures never displace decode
|
|
/// targets. Layered-coincide never reaches here (the caps derivation rejects it).
|
|
pub fn plan_pools(caps: &DecodeCaps, required_slots: u32) -> PoolPlan {
|
|
let picture_count = required_slots + HOLD_HEADROOM;
|
|
let picture_flags = vk::ImageCreateFlags::MUTABLE_FORMAT;
|
|
if caps.coincide {
|
|
PoolPlan {
|
|
dpb_image_count: 0,
|
|
dpb_layers_per_image: 0,
|
|
dpb_usage: vk::ImageUsageFlags::empty(),
|
|
picture_count,
|
|
picture_usage: COINCIDE_USAGE,
|
|
picture_flags,
|
|
}
|
|
} else {
|
|
let (dpb_image_count, dpb_layers_per_image) = if caps.layered_dpb {
|
|
(1, required_slots)
|
|
} else {
|
|
(required_slots, 1)
|
|
};
|
|
PoolPlan {
|
|
dpb_image_count,
|
|
dpb_layers_per_image,
|
|
dpb_usage: DPB_USAGE,
|
|
picture_count,
|
|
picture_usage: OUTPUT_USAGE,
|
|
picture_flags,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One picture-pool image with its sync + occupancy ledger.
|
|
pub(crate) struct Picture {
|
|
pub image: vk::Image,
|
|
/// Full-picture view in the session's picture format (decode dst / DPB
|
|
/// binding).
|
|
pub view: vk::ImageView,
|
|
/// Per-plane views for the presenter's sampler path, in the formats
|
|
/// [`crate::caps::plane_formats`] resolved for the picture format (`R8`/`R8G8`
|
|
/// at 8 bits, the `R10X6` pair at 10).
|
|
pub plane_views: [vk::ImageView; 2],
|
|
/// The image's own timeline semaphore (AVVkFrame contract).
|
|
pub semaphore: vk::Semaphore,
|
|
/// Latest timeline value known signalled-or-enqueued: the decoder's write
|
|
/// signal, bumped to the presenter's write-back (`frame.value + 1`) when a
|
|
/// release token reports the frame was sampled.
|
|
pub value: u64,
|
|
/// A DPB slot currently binds this image (coincide mode).
|
|
pub bound: bool,
|
|
/// A decoded picture awaiting its output verdict lives here.
|
|
pub pending: bool,
|
|
/// Frames over this image not yet released (ready queue + consumer-held).
|
|
pub held: u32,
|
|
}
|
|
|
|
impl Picture {
|
|
/// Free for a new decode target: no slot binds it, no pending picture lives
|
|
/// in it, no unreleased frame reads it.
|
|
pub(crate) fn is_free(&self) -> bool {
|
|
!self.bound && !self.pending && self.held == 0
|
|
}
|
|
}
|
|
|
|
/// The decoupled picture pool. Destroys everything it created on drop
|
|
/// (null-safe); a pool with consumer-held images is retired to the decoder's
|
|
/// graveyard instead of dropped, and dies when its last release token arrives.
|
|
pub(crate) struct PicturePool {
|
|
device: ash::Device,
|
|
memory: Vec<vk::DeviceMemory>,
|
|
/// The picture format every image in this pool was created with (the
|
|
/// caps-resolved `output_format`). Stashed here because it is the ONLY place
|
|
/// that knows it by the time a frame is built: the session's caps are keyed
|
|
/// by profile and a delivered frame outlives its generation's caps entry.
|
|
/// [`crate::decoder::build_frame`] stamps it into every
|
|
/// [`crate::decoder::DecodedVkFrame`] so the consumer can tell an NV12
|
|
/// picture from a P010 or 4:4:4 one — the H.265 path makes the format the
|
|
/// STREAM's, not a constant.
|
|
pub(crate) format: vk::Format,
|
|
pub(crate) pictures: Vec<Picture>,
|
|
}
|
|
|
|
impl PicturePool {
|
|
/// Create `plan.picture_count` single-layer images at `extent` (the
|
|
/// granularity-ALIGNED allocation extent).
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// `dev` wraps live handles ([`crate::DeviceHandles`] contract).
|
|
pub(crate) unsafe fn create(
|
|
dev: &DecodeDevice,
|
|
caps: &DecodeCaps,
|
|
plan: &PoolPlan,
|
|
extent: vk::Extent2D,
|
|
profile: DecodeProfile,
|
|
) -> Result<Self, AllocError> {
|
|
let mut pool = Self {
|
|
device: dev.ash().clone(),
|
|
memory: Vec::new(),
|
|
format: caps.output_format,
|
|
pictures: Vec::new(),
|
|
};
|
|
let families = dev.sharing_families();
|
|
for _ in 0..plan.picture_count {
|
|
// SAFETY: fn contract (live device); every created handle is parked
|
|
// in `pool` so a mid-build failure unwinds through Drop.
|
|
let (image, memory) = unsafe {
|
|
create_video_image(
|
|
dev,
|
|
caps.output_format,
|
|
extent,
|
|
1,
|
|
plan.picture_usage,
|
|
plan.picture_flags,
|
|
&families,
|
|
profile,
|
|
)?
|
|
};
|
|
pool.memory.push(memory);
|
|
// The picture is parked with null handles IMMEDIATELY (Drop ignores
|
|
// nulls), then each view/semaphore is filled as it is created — a
|
|
// failure anywhere unwinds everything created so far.
|
|
pool.pictures.push(Picture {
|
|
image,
|
|
view: vk::ImageView::null(),
|
|
plane_views: [vk::ImageView::null(); 2],
|
|
semaphore: vk::Semaphore::null(),
|
|
value: 0,
|
|
bound: false,
|
|
pending: false,
|
|
held: 0,
|
|
});
|
|
let picture = pool.pictures.len() - 1;
|
|
// SAFETY: `image` was just created with layer 0 in range (holds for
|
|
// all three creates in this block); the plane formats are the ones
|
|
// derive_caps/derive_caps_h265 resolved for THIS picture format and
|
|
// are plane-compatible with it under MUTABLE_FORMAT (caps-gated).
|
|
unsafe {
|
|
pool.pictures[picture].view = create_view(
|
|
&pool.device,
|
|
image,
|
|
caps.output_format,
|
|
vk::ImageAspectFlags::COLOR,
|
|
0,
|
|
)?;
|
|
pool.pictures[picture].plane_views[0] = create_view(
|
|
&pool.device,
|
|
image,
|
|
caps.plane_view_formats[0],
|
|
vk::ImageAspectFlags::PLANE_0,
|
|
0,
|
|
)?;
|
|
pool.pictures[picture].plane_views[1] = create_view(
|
|
&pool.device,
|
|
image,
|
|
caps.plane_view_formats[1],
|
|
vk::ImageAspectFlags::PLANE_1,
|
|
0,
|
|
)?;
|
|
}
|
|
let mut type_info = vk::SemaphoreTypeCreateInfo::default()
|
|
.semaphore_type(vk::SemaphoreType::TIMELINE)
|
|
.initial_value(0);
|
|
let sem_ci = vk::SemaphoreCreateInfo::default().push_next(&mut type_info);
|
|
// SAFETY: live device; timelineSemaphore enabled per the handles
|
|
// contract.
|
|
pool.pictures[picture].semaphore =
|
|
unsafe { pool.device.create_semaphore(&sem_ci, None)? };
|
|
}
|
|
Ok(pool)
|
|
}
|
|
|
|
/// Index of the first free image, if any.
|
|
pub(crate) fn free_index(&self) -> Option<usize> {
|
|
self.pictures.iter().position(Picture::is_free)
|
|
}
|
|
|
|
/// Total frames not yet released across the pool (graveyard retirement key).
|
|
pub(crate) fn held_total(&self) -> u32 {
|
|
self.pictures.iter().map(|p| p.held).sum()
|
|
}
|
|
}
|
|
|
|
impl Drop for PicturePool {
|
|
fn drop(&mut self) {
|
|
// SAFETY: every handle is this pool's own on the (contract-live) device;
|
|
// the owning decoder drains decode work before dropping/retiring, and a
|
|
// retired pool is only dropped once its last release token returned (the
|
|
// presenter's fence wait). Destroys ignore NULL (half-built unwinding).
|
|
unsafe {
|
|
for p in self.pictures.drain(..) {
|
|
self.device.destroy_image_view(p.view, None);
|
|
self.device.destroy_image_view(p.plane_views[0], None);
|
|
self.device.destroy_image_view(p.plane_views[1], None);
|
|
self.device.destroy_semaphore(p.semaphore, None);
|
|
self.device.destroy_image(p.image, None);
|
|
}
|
|
for memory in self.memory.drain(..) {
|
|
self.device.free_memory(memory, None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Distinct-mode reference-only DPB backing (fixed slot↔layer mapping — these
|
|
/// images are never delivered, so nothing consumer-side ever pins them).
|
|
pub(crate) struct DpbPool {
|
|
device: ash::Device,
|
|
images: Vec<vk::Image>,
|
|
memory: Vec<vk::DeviceMemory>,
|
|
dpb_views: Vec<vk::ImageView>,
|
|
dpb_location: Vec<(usize, u32)>,
|
|
}
|
|
|
|
impl DpbPool {
|
|
/// # Safety
|
|
///
|
|
/// `dev` wraps live handles ([`crate::DeviceHandles`] contract).
|
|
pub(crate) unsafe fn create(
|
|
dev: &DecodeDevice,
|
|
caps: &DecodeCaps,
|
|
plan: &PoolPlan,
|
|
extent: vk::Extent2D,
|
|
profile: DecodeProfile,
|
|
) -> Result<Self, AllocError> {
|
|
let mut pool = Self {
|
|
device: dev.ash().clone(),
|
|
images: Vec::new(),
|
|
memory: Vec::new(),
|
|
dpb_views: Vec::new(),
|
|
dpb_location: Vec::new(),
|
|
};
|
|
let families = dev.sharing_families();
|
|
for _ in 0..plan.dpb_image_count {
|
|
// SAFETY: fn contract (live device); parked in `pool` for unwinding.
|
|
let (image, memory) = unsafe {
|
|
create_video_image(
|
|
dev,
|
|
caps.dpb_format,
|
|
extent,
|
|
plan.dpb_layers_per_image,
|
|
plan.dpb_usage,
|
|
vk::ImageCreateFlags::empty(),
|
|
&families,
|
|
profile,
|
|
)?
|
|
};
|
|
pool.images.push(image);
|
|
pool.memory.push(memory);
|
|
}
|
|
let slots = plan.dpb_image_count * plan.dpb_layers_per_image;
|
|
for slot in 0..slots {
|
|
let (image_index, layer) = if plan.dpb_image_count == 1 {
|
|
(0usize, slot)
|
|
} else {
|
|
(slot as usize, 0u32)
|
|
};
|
|
// SAFETY: the image was created above with `layer` in range.
|
|
let view = unsafe {
|
|
create_view(
|
|
&pool.device,
|
|
pool.images[image_index],
|
|
caps.dpb_format,
|
|
vk::ImageAspectFlags::COLOR,
|
|
layer,
|
|
)?
|
|
};
|
|
pool.dpb_views.push(view);
|
|
pool.dpb_location.push((image_index, layer));
|
|
}
|
|
Ok(pool)
|
|
}
|
|
|
|
/// The DPB binding view of `slot`.
|
|
pub(crate) fn dpb_view(&self, slot: u8) -> vk::ImageView {
|
|
self.dpb_views[usize::from(slot)]
|
|
}
|
|
|
|
/// The image + array layer behind DPB `slot` (barrier targeting).
|
|
pub(crate) fn dpb_target(&self, slot: u8) -> (vk::Image, u32) {
|
|
let (image_index, layer) = self.dpb_location[usize::from(slot)];
|
|
(self.images[image_index], layer)
|
|
}
|
|
}
|
|
|
|
impl Drop for DpbPool {
|
|
fn drop(&mut self) {
|
|
// SAFETY: own handles on the contract-live device; the owning decoder
|
|
// drains decode work before dropping state (nothing consumer-side ever
|
|
// references these). Destroys ignore NULL.
|
|
unsafe {
|
|
for view in self.dpb_views.drain(..) {
|
|
self.device.destroy_image_view(view, None);
|
|
}
|
|
for image in self.images.drain(..) {
|
|
self.device.destroy_image(image, None);
|
|
}
|
|
for memory in self.memory.drain(..) {
|
|
self.device.free_memory(memory, None);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// One OPTIMAL-tiling video image bound to fresh DEVICE_LOCAL memory, profile-listed
|
|
/// (mirrors the encoder's `make_video_image`, minus its `&mut` profile-list plumbing).
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// `dev` wraps live handles.
|
|
#[allow(clippy::too_many_arguments)]
|
|
unsafe fn create_video_image(
|
|
dev: &DecodeDevice,
|
|
format: vk::Format,
|
|
extent: vk::Extent2D,
|
|
layers: u32,
|
|
usage: vk::ImageUsageFlags,
|
|
flags: vk::ImageCreateFlags,
|
|
families: &[u32],
|
|
decode_profile: DecodeProfile,
|
|
) -> Result<(vk::Image, vk::DeviceMemory), AllocError> {
|
|
let mut chain = decode_profile.chain();
|
|
let profile = chain.wire();
|
|
let mut profile_list =
|
|
vk::VideoProfileListInfoKHR::default().profiles(std::slice::from_ref(profile));
|
|
let mut ci = vk::ImageCreateInfo::default()
|
|
.flags(flags)
|
|
.image_type(vk::ImageType::TYPE_2D)
|
|
.format(format)
|
|
.extent(vk::Extent3D {
|
|
width: extent.width,
|
|
height: extent.height,
|
|
depth: 1,
|
|
})
|
|
.mip_levels(1)
|
|
.array_layers(layers)
|
|
.samples(vk::SampleCountFlags::TYPE_1)
|
|
.tiling(vk::ImageTiling::OPTIMAL)
|
|
.usage(usage)
|
|
.initial_layout(vk::ImageLayout::UNDEFINED)
|
|
.push_next(&mut profile_list);
|
|
ci = if families.len() >= 2 {
|
|
ci.sharing_mode(vk::SharingMode::CONCURRENT)
|
|
.queue_family_indices(families)
|
|
} else {
|
|
ci.sharing_mode(vk::SharingMode::EXCLUSIVE)
|
|
};
|
|
// SAFETY: live device; `ci` roots a chain of locals outliving the call.
|
|
let image = unsafe { dev.ash().create_image(&ci, None)? };
|
|
// SAFETY: `image` was just created on this device.
|
|
let req = unsafe { dev.ash().get_image_memory_requirements(image) };
|
|
let props = dev.memory_properties();
|
|
// DEVICE_LOCAL preferred, any advertised type accepted (same rationale as the
|
|
// session bindings: `memoryTypeBits` is the driver's placement contract).
|
|
let type_index = match find_memory_type_preferring(
|
|
&props,
|
|
req.memory_type_bits,
|
|
vk::MemoryPropertyFlags::DEVICE_LOCAL,
|
|
) {
|
|
Ok(index) => index,
|
|
Err(e) => {
|
|
// SAFETY: destroying the just-created, never-bound image.
|
|
unsafe { dev.ash().destroy_image(image, None) };
|
|
return Err(e);
|
|
}
|
|
};
|
|
let alloc = vk::MemoryAllocateInfo::default()
|
|
.allocation_size(req.size)
|
|
.memory_type_index(type_index);
|
|
// SAFETY: live device; unwind destroys the unbound image so the error path
|
|
// leaks nothing.
|
|
let memory = match unsafe { dev.ash().allocate_memory(&alloc, None) } {
|
|
Ok(m) => m,
|
|
Err(e) => {
|
|
// SAFETY: destroying the just-created, never-bound image.
|
|
unsafe { dev.ash().destroy_image(image, None) };
|
|
return Err(e.into());
|
|
}
|
|
};
|
|
// SAFETY: fresh image + fresh memory of the required size.
|
|
if let Err(e) = unsafe { dev.ash().bind_image_memory(image, memory, 0) } {
|
|
// SAFETY: unwinding the two objects created above.
|
|
unsafe {
|
|
dev.ash().destroy_image(image, None);
|
|
dev.ash().free_memory(memory, None);
|
|
}
|
|
return Err(e.into());
|
|
}
|
|
Ok((image, memory))
|
|
}
|
|
|
|
/// One single-layer 2D view (`base_array_layer = layer`, identity swizzle).
|
|
///
|
|
/// # Safety
|
|
///
|
|
/// `image` is live on `device` with `layer` in range; `format`/`aspect` are
|
|
/// compatible with the image's creation (same format for COLOR, plane-compatible
|
|
/// under MUTABLE_FORMAT for the plane aspects).
|
|
unsafe fn create_view(
|
|
device: &ash::Device,
|
|
image: vk::Image,
|
|
format: vk::Format,
|
|
aspect: vk::ImageAspectFlags,
|
|
layer: u32,
|
|
) -> Result<vk::ImageView, vk::Result> {
|
|
let ci = vk::ImageViewCreateInfo::default()
|
|
.image(image)
|
|
.view_type(vk::ImageViewType::TYPE_2D)
|
|
.format(format)
|
|
.subresource_range(vk::ImageSubresourceRange {
|
|
aspect_mask: aspect,
|
|
base_mip_level: 0,
|
|
level_count: 1,
|
|
base_array_layer: layer,
|
|
layer_count: 1,
|
|
});
|
|
// SAFETY: the fn-level contract restates exactly what create_image_view needs.
|
|
unsafe { device.create_image_view(&ci, None) }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::caps::derive_caps;
|
|
use crate::caps::RawH264Caps;
|
|
use crate::caps::VideoFormat;
|
|
use crate::caps::NV12;
|
|
|
|
fn caps(coincide: bool, layered: bool) -> DecodeCaps {
|
|
// Every entry advertises its role's full usage plus MUTABLE_FORMAT — the
|
|
// derivation gates on those; this module's decision table is downstream.
|
|
let entry = |usage: vk::ImageUsageFlags| VideoFormat {
|
|
format: NV12,
|
|
image_usage: usage,
|
|
image_create_flags: vk::ImageCreateFlags::MUTABLE_FORMAT,
|
|
..Default::default()
|
|
};
|
|
let raw = RawH264Caps {
|
|
capability_flags: if layered {
|
|
vk::VideoCapabilityFlagsKHR::empty()
|
|
} else {
|
|
vk::VideoCapabilityFlagsKHR::SEPARATE_REFERENCE_IMAGES
|
|
},
|
|
decode_flags: if coincide {
|
|
vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_COINCIDE
|
|
} else {
|
|
vk::VideoDecodeCapabilityFlagsKHR::DPB_AND_OUTPUT_DISTINCT
|
|
},
|
|
dpb_formats: vec![entry(DPB_USAGE)],
|
|
output_formats: vec![entry(OUTPUT_USAGE)],
|
|
coincide_formats: vec![entry(COINCIDE_USAGE)],
|
|
..Default::default()
|
|
};
|
|
derive_caps(&raw).unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn coincide_pools_are_headroomed_dual_use_pictures_with_no_dpb_array() {
|
|
let plan = plan_pools(&caps(true, false), 8);
|
|
assert_eq!(
|
|
plan.dpb_image_count, 0,
|
|
"the picture pool IS the DPB backing"
|
|
);
|
|
assert_eq!(
|
|
plan.picture_count,
|
|
8 + HOLD_HEADROOM,
|
|
"the stream's DPB depth PLUS the consumer-hold headroom — a pool \
|
|
sized to either alone starves (.25 field failure)"
|
|
);
|
|
assert_eq!(
|
|
plan.picture_usage, COINCIDE_USAGE,
|
|
"pool pictures are DPB + decode dst + sampled surface in one"
|
|
);
|
|
assert_eq!(plan.picture_flags, vk::ImageCreateFlags::MUTABLE_FORMAT);
|
|
}
|
|
|
|
#[test]
|
|
fn distinct_keeps_a_fixed_dpb_array_and_headrooms_the_output_pool() {
|
|
let plan = plan_pools(&caps(false, true), 17);
|
|
assert_eq!(
|
|
(plan.dpb_image_count, plan.dpb_layers_per_image),
|
|
(1, 17),
|
|
"layered: one array, one layer per slot"
|
|
);
|
|
assert_eq!(plan.dpb_usage, DPB_USAGE);
|
|
assert_eq!(plan.picture_count, 17 + HOLD_HEADROOM);
|
|
assert_eq!(plan.picture_usage, OUTPUT_USAGE);
|
|
|
|
let plan = plan_pools(&caps(false, false), 3);
|
|
assert_eq!(
|
|
(plan.dpb_image_count, plan.dpb_layers_per_image),
|
|
(3, 1),
|
|
"separate reference images: one image per slot"
|
|
);
|
|
assert_eq!(plan.picture_count, 3 + HOLD_HEADROOM);
|
|
}
|
|
|
|
#[test]
|
|
fn picture_occupancy_frees_only_when_unbound_unpending_and_released() {
|
|
let mut p = Picture {
|
|
image: vk::Image::null(),
|
|
view: vk::ImageView::null(),
|
|
plane_views: [vk::ImageView::null(); 2],
|
|
semaphore: vk::Semaphore::null(),
|
|
value: 0,
|
|
bound: true,
|
|
pending: true,
|
|
held: 2,
|
|
};
|
|
assert!(!p.is_free());
|
|
p.bound = false;
|
|
assert!(!p.is_free(), "pending pictures are not decode targets");
|
|
p.pending = false;
|
|
assert!(!p.is_free(), "held frames are not decode targets");
|
|
p.held = 1;
|
|
assert!(!p.is_free());
|
|
p.held = 0;
|
|
assert!(p.is_free());
|
|
}
|
|
}
|