Compare commits

..

No commits in common. "f7c67f987274bdf49fe3ed73ee7b74c2de3b1760" and "d901fe250bc603633cd2e42c087a189b7cf868f9" have entirely different histories.

22 changed files with 243 additions and 4409 deletions

View file

@ -1,6 +1,5 @@
use std::{
collections::{BTreeMap, HashMap},
mem::ManuallyDrop,
sync::Arc,
};
@ -486,13 +485,6 @@ pub fn egui_pass(
textures_indices.push(idx as u32);
}
const TEXTURE_DESCRIPTOR_CAPACITY: usize = 10;
if textures.len() > TEXTURE_DESCRIPTOR_CAPACITY {
return Err(crate::Error::Todo(
"egui frame references more textures than the descriptor array supports",
));
}
let num_draw_calls = draw_calls.len();
let vertices_size = vertices.len() * size_of::<Vertex>();
let indices_size = indices.len() * size_of::<u32>();
@ -566,110 +558,57 @@ pub fn egui_pass(
(staging, vertices, indices, draw_calls, texture_ids)
};
// This map contains only unique textures referenced by these meshes. Its insertion order
// is the exact index stored in `texture_ids`, matching `texture[index]` in egui.slang.
let descriptor_resources = textures
let descriptor_infos = textures
.values()
.map(|entry| {
let texture = texture_handler.get_texture(entry.id).unwrap();
let view = Arc::new(ManuallyDrop::into_inner(
texture
vk::DescriptorImageInfo {
sampler: samplers.get_sampler(entry.as_sampler_desc()).unwrap(),
image_view: texture
.create_view(
ImageViewDesc::color_2d()
.with_mip_range(0..1)
.with_layer_range(0..1),
)
.unwrap(),
));
let sampler = samplers.get_sampler_owned(entry.as_sampler_desc()).unwrap();
(view, sampler)
.unwrap()
.raw(),
image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
}
})
.collect::<Vec<_>>();
let texture_ids = Arc::new(texture_ids);
// Each pass gets its own managed set, so later passes/frames cannot mutate the contents
// captured by this callback.
let descriptor_pool = crate::pipeline::DescriptorPool::new(
dev.clone(),
crate::pipeline::DescriptorPoolDesc {
flags: vk::DescriptorPoolCreateFlags::UPDATE_AFTER_BIND,
name: Some("egui-pass-descriptor-pool".into()),
sizes: &[
vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: TEXTURE_DESCRIPTOR_CAPACITY as u32,
},
vk::DescriptorPoolSize {
ty: vk::DescriptorType::STORAGE_BUFFER,
descriptor_count: 1,
},
],
max_sets: 1,
},
)?;
let mut descriptor_set = descriptor_pool
.allocate_managed(crate::pipeline::DescriptorSetAllocDesc {
name: None,
layout: &egui_state.descriptor_layout,
})
.map_err(|_| crate::Error::Todo("egui managed descriptor allocation failed"))?;
{
let mut writer = descriptor_set
.writer()
.map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?;
writer
.clear_binding(EguiState::TEXTURE_BINDING)
.map_err(|_| crate::Error::Todo("egui descriptor binding clear failed"))?;
// Managed bindings are partially bound, but Vulkan descriptor contents do not have an
// unset operation. Populate every unused slot with a live descriptor to avoid stale
// shader-array entries while preserving the mesh texture indices above.
if let Some((first_view, first_sampler)) = descriptor_resources.first() {
for array_element in descriptor_resources.len()..TEXTURE_DESCRIPTOR_CAPACITY {
writer
.write_image(
EguiState::TEXTURE_BINDING,
array_element as u32,
&[crate::pipeline::ImageDescriptor {
view: first_view,
sampler: Some(first_sampler),
layout: vk::ImageLayout::GENERAL,
}],
)
.map_err(|_| crate::Error::Todo("egui descriptor image padding failed"))?;
}
}
for (array_element, (view, sampler)) in descriptor_resources.iter().enumerate() {
writer
.write_image(
EguiState::TEXTURE_BINDING,
array_element as u32,
&[crate::pipeline::ImageDescriptor {
view,
sampler: Some(sampler),
layout: vk::ImageLayout::GENERAL,
}],
)
.map_err(|_| crate::Error::Todo("egui descriptor image write failed"))?;
}
writer
.write_buffer(
EguiState::UNIFORM_BINDING,
0,
&[crate::pipeline::BufferDescriptor {
buffer: &texture_ids,
let uniform_info = vk::DescriptorBufferInfo {
buffer: texture_ids.raw(),
offset: 0,
range: texture_ids.len(),
}],
)
.map_err(|_| crate::Error::Todo("egui descriptor buffer write failed"))?;
writer
.commit()
.map_err(|_| crate::Error::Todo("egui descriptor commit failed"))?;
};
let descriptor_writes = descriptor_infos
.iter()
.enumerate()
.map(|(i, info)| {
vk::WriteDescriptorSet::default()
.image_info(core::slice::from_ref(info))
.descriptor_count(1)
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
.dst_set(egui_state.descriptor_set)
.dst_binding(EguiState::TEXTURE_BINDING)
.dst_array_element(i as u32)
})
.chain(core::iter::once({
vk::WriteDescriptorSet::default()
.buffer_info(core::slice::from_ref(&uniform_info))
.descriptor_count(1)
.dst_binding(EguiState::UNIFORM_BINDING)
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
.dst_array_element(0)
.dst_set(egui_state.descriptor_set)
}))
.collect::<Vec<_>>();
unsafe {
dev.dev().update_descriptor_sets(&descriptor_writes, &[]);
}
let descriptor_set_raw = descriptor_set
.raw()
.map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?;
let descriptor_set_owner = Arc::new(descriptor_set);
let to_remove_tex_ids = output
.textures_delta
@ -692,15 +631,10 @@ pub fn egui_pass(
let record: Box<RecordFn> = Box::new({
let pipeline = egui_state.pipeline.clone();
let descriptor_set_owner = descriptor_set_owner.clone();
let descriptor_set = egui_state.descriptor_set;
let screen_rect = egui.screen_rect();
move |ctx: &RenderContext| -> crate::Result<()> {
// Keep this managed wrapper alive through recording, and validate its generation
// immediately before binding the raw handle captured for this frame.
let _descriptor_set = descriptor_set_owner
.raw()
.map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?;
let cmd = &ctx.cmd;
let staging = ctx.get_buffer(draw_staging).unwrap();
let vertices = ctx.get_buffer(vertices).unwrap();
@ -808,7 +742,7 @@ pub fn egui_pass(
cmd.bind_descriptor_sets(
pipeline.layout(),
vk::PipelineBindPoint::GRAPHICS,
&[descriptor_set_raw],
&[descriptor_set],
);
cmd.draw_indexed_indirect(
draw_calls.raw(),

View file

@ -1,4 +1,8 @@
#![feature(negative_impls, map_try_insert, debug_closure_helpers)]
#![feature(
negative_impls,
map_try_insert,
debug_closure_helpers,
)]
use std::{collections::HashMap, sync::Arc};
@ -18,14 +22,15 @@ pub mod util;
pub(crate) use renderer_resources::{buffers, images, pipeline};
pub use renderer_device::{device, instance, queue, sync};
pub use renderer_device::{
compatible_extension_properties, extension_intersection,
get_physical_device_features, get_physical_device_properties, make_extension,
DebugUtils, DebugUtilsCreateInfo, Device, DeviceOwned, DeviceRef, Error,
ExtendsDeviceFeatures2Debug, ExtendsDeviceProperties2Debug, Extension, Instance,
PhysicalDeviceFeatures, PhysicalDeviceInfo, PhysicalDeviceProperties, Result,
SurfaceCapabilities, compatible_extension_properties, extension_intersection,
get_physical_device_features, get_physical_device_properties, make_extension,
SurfaceCapabilities,
};
pub use renderer_device::{device, instance, queue, sync};
pub use renderer_macros::DeviceRef;
mod texture {
@ -70,9 +75,11 @@ mod texture {
use util::Rgba;
pub struct SamplerCache {
device: Device,
samplers: HashMap<pipeline::SamplerDesc, Arc<pipeline::Sampler>>,
samplers: HashMap<pipeline::SamplerDesc, pipeline::Sampler>,
}
impl SamplerCache {
@ -84,22 +91,16 @@ impl SamplerCache {
}
pub fn get_sampler(&mut self, desc: pipeline::SamplerDesc) -> VkResult<vk::Sampler> {
Ok(self.get_sampler_owned(desc)?.raw())
}
pub(crate) fn get_sampler_owned(
&mut self,
desc: pipeline::SamplerDesc,
) -> VkResult<Arc<pipeline::Sampler>> {
use std::collections::hash_map::Entry;
match self.samplers.entry(desc) {
Entry::Occupied(entry) => Ok(entry.get().clone()),
let entry = match self.samplers.entry(desc) {
Entry::Occupied(entry) => entry.get().raw(),
Entry::Vacant(entry) => {
let sampler = Arc::new(pipeline::Sampler::new(self.device.clone(), entry.key())?);
entry.insert(sampler.clone());
Ok(sampler)
}
let sampler = pipeline::Sampler::new(self.device.clone(), entry.key())?;
entry.insert(sampler).raw()
}
};
Ok(entry)
}
}
@ -107,6 +108,9 @@ impl SamplerCache {
pub struct EguiState {
textures: HashMap<egui::TextureId, EguiTextureInfo>,
#[allow(unused)]
descriptor_pool: pipeline::DescriptorPool,
descriptor_set: vk::DescriptorSet,
#[allow(unused)]
descriptor_layout: pipeline::DescriptorSetLayout,
pipeline: Arc<pipeline::Pipeline>,
}
@ -151,7 +155,26 @@ impl EguiState {
const TEXTURE_BINDING: u32 = 0;
const UNIFORM_BINDING: u32 = 1;
pub fn new(device: Device) -> Result<Self> {
let descriptor_layout = util::timed("Create Descriptor Set", || {
let (descriptor_pool, descriptor_layout, sets) =
util::timed("Create Descriptor Set", || {
let descriptor_pool = pipeline::DescriptorPool::new(
device.clone(),
pipeline::DescriptorPoolDesc {
flags: vk::DescriptorPoolCreateFlags::UPDATE_AFTER_BIND,
name: Some("egui-descriptorpool".into()),
sizes: &[
vk::DescriptorPoolSize {
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
descriptor_count: 10,
},
vk::DescriptorPoolSize {
ty: vk::DescriptorType::STORAGE_BUFFER,
descriptor_count: 1,
},
],
max_sets: 1,
},
)?;
let descriptor_layout = pipeline::DescriptorSetLayout::new(
device.clone(),
pipeline::DescriptorSetLayoutDesc {
@ -178,7 +201,13 @@ impl EguiState {
],
},
)?;
Result::Ok(descriptor_layout)
let sets = descriptor_pool.allocate(&[pipeline::DescriptorSetAllocDesc {
name: None,
layout: &descriptor_layout,
}])?;
Result::Ok((descriptor_pool, descriptor_layout, sets))
})?;
let pipeline_layout = Arc::new(pipeline::PipelineLayout::new(
@ -303,7 +332,9 @@ impl EguiState {
Ok(Self {
textures: HashMap::new(),
descriptor_pool,
descriptor_layout,
descriptor_set: sets[0],
pipeline: Arc::new(pipeline),
})
}
@ -443,7 +474,9 @@ impl Renderer2 {
pub use vk::Extent2D;
use crate::instance::InstanceDesc;
use crate::{
instance::InstanceDesc,
};
pub mod utils {
#![allow(dead_code)]

View file

@ -129,7 +129,7 @@ where
R: IntoImageSubresourceLayers,
{
fn side_effects(&self, mut map: SideEffectMap) {
let block_size = self.dst.format().block_size();
let block_size = self.dst.format().block_size() as u64;
let texels_per_block = self.dst.format().texels_per_block() as u64;
let src_id = self.src.id();
@ -144,7 +144,7 @@ where
let texel_count = if src_region.row_size != 0 {
let texel_count = src_region.row_size as u64 * src_region.row_count as u64;
debug_assert!(
texel_count.is_multiple_of(texels_per_block),
texel_count % texels_per_block == 0,
"Buffer copy region must be a multiple of the block size"
);
texel_count
@ -217,7 +217,7 @@ where
R: IntoImageSubresourceLayers,
{
fn side_effects(&self, mut map: SideEffectMap) {
let block_size = self.src.format().block_size();
let block_size = self.src.format().block_size() as u64;
let texels_per_block = self.src.format().texels_per_block() as u64;
let src_id = self.src.id();
@ -232,7 +232,7 @@ where
let texel_count = if dst_region.row_size != 0 {
let texel_count = dst_region.row_size as u64 * dst_region.row_count as u64;
debug_assert!(
texel_count.is_multiple_of(texels_per_block),
texel_count % texels_per_block == 0,
"Buffer copy region must be a multiple of the block size"
);
texel_count

View file

@ -16,7 +16,6 @@
## Key types and ownership
- `Device(Arc<DeviceInner>)` is the cloneable owning handle. It dereferences to `DeviceInner`; equality is `Arc::ptr_eq`.
- `DeviceInner` keeps the raw `ash::Device`, allocator mutex, adapter and instance, queues, synchronization threadpool, extension loaders, and inline `DevicePools`.
- `Device::enabled_features()` returns the feature snapshot requested for logical-device creation; it is distinct from `Device::features()`, which reports adapter-supported features. Descriptor capability validation must use the enabled snapshot, not support data.
- `PhysicalDeviceInfo::create_logical_device` selects queues, enables requested/supported extensions and features, initializes the allocator/cache, and constructs `DeviceInner` with `Arc::new_cyclic`.
- `DeviceInner::dev`, `instance`, `queues`, `physical_device`, `main_queue`, `compute_queue`, and `transfer_queue` are the main convenience accessors for downstream code.
- `DevicePools` contains a `PipelineCache` wrapper plus pools for raw fences and binary/timeline semaphore newtypes. A `Pool<T>` stores bare handles in `Mutex<Vec<T>>` and a `Weak<DeviceInner>`; `get()` reuses a handle or creates one through `Pooled`.
@ -62,7 +61,6 @@
- `_drop: DeviceDrop` is the private shutdown path: it waits for the device to idle, exports and destroys the pipeline cache to `pipeline_cache.bin`, then destroys the Vulkan device. The cache is also exposed as a copyable wrapper through `DevicePools` for pipeline creation.
- `Device` cloning is cheap and prolongs the full device lifetime; any live `DeviceObject`, `PoolObject`, sync wrapper, or other owner retaining a `Device` can delay shutdown.
- Device-owned wrapper structs live outside `DeviceInner` and its pools. Pools hold bare handles, never `DeviceObject` or `PoolObject` wrappers.
- Keep enabled-feature state distinct from adapter-supported feature queries: `enabled_features()` records what the logical device was created with, while `features()` is support information and must not be treated as enabled capability.
- `Pool::device()` upgrades its `Weak` and panics if the pool outlives its device; don't retain pools independently.
- `PoolObject` uses `ManuallyDrop` to transfer its inner handle to `Recycle` exactly once. New `Recycle` implementations must route to the matching pool and handle type.
- The derive parser requires a named-field struct with exactly one `#[device]` field and does not add generic bounds itself; generic field types must satisfy `DeviceRef` through the input declaration. Its generated paths require the crate self-alias in `lib.rs`.

View file

@ -6,7 +6,11 @@ use std::{
sync::{Arc, Weak},
};
use ash::{ext, khr, prelude::VkResult, vk};
use ash::{
ext, khr,
prelude::VkResult,
vk,
};
use parking_lot::Mutex;
use raw_window_handle::RawDisplayHandle;
@ -65,6 +69,8 @@ impl From<String> for DebugName {
}
}
}
use crate::{
Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result,
device::asdf::traits::ExternallyManagedObject,
@ -156,8 +162,6 @@ pub struct DeviceInner {
pub sync_threadpool: sync::SyncThreadpool,
pub device_extensions: DeviceExtensions,
pub pools: DevicePools,
/// Features enabled when this logical device was created.
pub enabled_features: PhysicalDeviceFeatures,
#[allow(dead_code)]
pub enabled_extensions: Vec<&'static CStr>,
@ -406,8 +410,6 @@ impl PhysicalDeviceInfo {
let create_info = vk::DeviceCreateInfo::default()
.queue_create_infos(&queue_create_infos)
.enabled_extension_names(&extensions);
// Preserve the values before push_next links the feature structs together for Vulkan.
let enabled_features = enabled_features_snapshot(&features);
let create_info = features.push_to_device_create_info(create_info);
let device = unsafe {
@ -462,7 +464,6 @@ impl PhysicalDeviceInfo {
queues: device_queues,
device_extensions,
enabled_extensions,
enabled_features,
sync_threadpool: sync::SyncThreadpool::new(),
pools: DevicePools::new(inner.clone(), pipeline_cache),
_drop: DeviceDrop {
@ -498,10 +499,6 @@ impl PhysicalDeviceInfo {
}
}
fn enabled_features_snapshot(features: &PhysicalDeviceFeatures) -> PhysicalDeviceFeatures {
features.clone()
}
#[derive(Debug)]
pub struct DevicePools {
pub pipeline_cache: PipelineCache,
@ -597,12 +594,6 @@ impl DeviceInner {
pub fn features(&self) -> &crate::PhysicalDeviceFeatures {
&self.adapter.features
}
/// Returns the feature values actually requested for logical-device creation.
///
/// This is distinct from [`Self::features`], which reports adapter-supported features.
pub fn enabled_features(&self) -> &crate::PhysicalDeviceFeatures {
&self.enabled_features
}
pub fn properties(&self) -> &crate::PhysicalDeviceProperties {
&self.adapter.properties
}
@ -915,8 +906,8 @@ pub mod asdf {
use ash::vk;
use ash::vk::Handle;
use crate::Device;
use crate::device::{DebugName, DeviceInner};
use crate::Device;
pub mod traits {
/// A trait describing an object owned by some manager-type, which is
@ -1031,7 +1022,10 @@ pub mod asdf {
let inner: &T = &self.inner;
fmt.field_with("device", |f| {
write!(f, "0x{:x}", self.inner.owner.raw.handle().as_raw())
write!(
f,
"0x{:x}", self.inner.owner.raw.handle().as_raw()
)
})
.field_with("handle", |f| write!(f, "{inner:?}"))
.field("name", &self.name);
@ -1138,21 +1132,4 @@ pub mod asdf {
let _pool_owned: DeviceObject<vk::Semaphore> =
DeviceObject::new(summon::<Device>(), summon::<vk::Semaphore>());
}
#[test]
fn enabled_feature_snapshot_is_independent_of_adapter_support() {
let requested = crate::PhysicalDeviceFeatures {
core12: vk::PhysicalDeviceVulkan12Features::default()
.descriptor_binding_partially_bound(true),
..Default::default()
};
let snapshot = super::enabled_features_snapshot(&requested);
let supported = crate::PhysicalDeviceFeatures::default();
assert_eq!(snapshot.core12.descriptor_binding_partially_bound, vk::TRUE);
assert_eq!(
supported.core12.descriptor_binding_partially_bound,
vk::FALSE
);
}
}

View file

@ -261,19 +261,25 @@ impl Instance {
raw: vk::SurfaceKHR,
present_mode: Option<vk::PresentModeKHR>,
) -> crate::Result<SurfaceCapabilities> {
let formats = unsafe { functor.get_physical_device_surface_formats(pdev, raw)? };
let formats = unsafe {
functor.get_physical_device_surface_formats(pdev, raw)?
};
let present_modes =
unsafe { functor.get_physical_device_surface_present_modes(pdev, raw)? };
let present_modes = unsafe {
functor.get_physical_device_surface_present_modes(pdev, raw)?
};
let generic_caps = unsafe { functor.get_physical_device_surface_capabilities(pdev, raw)? };
let generic_caps = unsafe {
functor.get_physical_device_surface_capabilities(pdev, raw)?
};
let (capabilities, present_modes) = if let Some(ref functor) =
self.extensions.get_surface_capabilities2
&& self.extensions.surface_maintenance1.is_some()
{
let mut capabilities = vk::SurfaceCapabilities2KHR::default();
let mut surface_info = vk::PhysicalDeviceSurfaceInfo2KHR::default().surface(raw);
let mut surface_info =
vk::PhysicalDeviceSurfaceInfo2KHR::default().surface(raw);
let present_mode = present_mode.unwrap_or(present_modes[0]);
let mut surface_present_mode =
@ -312,7 +318,10 @@ impl Instance {
})
}
pub fn expose_adapter(&self, pdev: vk::PhysicalDevice) -> crate::Result<PhysicalDeviceInfo> {
pub fn expose_adapter(
&self,
pdev: vk::PhysicalDevice,
) -> crate::Result<PhysicalDeviceInfo> {
let properties = get_physical_device_properties(&self.inner, pdev)?;
let features = get_physical_device_features(&self.inner, pdev, &properties);

View file

@ -168,7 +168,7 @@ pub struct SurfaceCapabilities {
pub present_modes: Vec<vk::PresentModeKHR>,
}
#[derive(Default, Debug, Clone)]
#[derive(Default, Debug)]
pub struct PhysicalDeviceFeatures {
pub core: vk::PhysicalDeviceFeatures,
pub core11: vk::PhysicalDeviceVulkan11Features<'static>,

View file

@ -7,7 +7,10 @@ use std::{
use crate::device::{
Pool, PoolObject, Pooled, Recycle,
asdf::{DeviceObject, traits::ExternallyManagedObject as ExternallyManagedObjectTrait},
asdf::{
DeviceObject,
traits::ExternallyManagedObject as ExternallyManagedObjectTrait,
},
};
use crate::{Result, device::DeviceInner};
@ -156,8 +159,12 @@ impl SyncThreadpool {
}
pub enum Fence {
Dedicated { fence: DeviceObject<vk::Fence> },
Pooled { fence: PoolObject<vk::Fence> },
Dedicated {
fence: DeviceObject<vk::Fence>,
},
Pooled {
fence: PoolObject<vk::Fence>,
},
}
impl Pooled for vk::Fence {
@ -272,14 +279,12 @@ pub enum SemaphoreInner {
impl Recycle for SemaphoreInner {
fn recycle(self, owner: &Device) {
match self {
SemaphoreInner::Binary(semaphore) => owner
.pools
.binary_semaphores
.push(BinarySemaphore(semaphore)),
SemaphoreInner::Timeline(semaphore) => owner
.pools
.timeline_semaphores
.push(TimelineSemaphore(semaphore)),
SemaphoreInner::Binary(semaphore) => {
owner.pools.binary_semaphores.push(BinarySemaphore(semaphore))
}
SemaphoreInner::Timeline(semaphore) => {
owner.pools.timeline_semaphores.push(TimelineSemaphore(semaphore))
}
}
}
}

View file

@ -9,6 +9,3 @@ gpu-allocator.workspace = true
renderer_device = { path = "../renderer_device" }
renderer_resources = { path = "../renderer_resources" }
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
[dev-dependencies]
renderer_resources = { path = "../renderer_resources", features = ["testing"] }

View file

@ -219,10 +219,6 @@ impl Command for Draw {
}
impl RecordedCommand for Draw {
fn preflight(&self) -> Result<(), RecordError> {
validate_managed_descriptor_sets(&self.descriptor_sets).map(|_| ())
}
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
for vertex in &self.vertices {
let buffers = [recorder.buffer(vertex.slice.buffer.id())?];
@ -262,12 +258,7 @@ impl RecordedCommand for Draw {
);
}
}
let descriptor_sets = validate_managed_descriptor_sets(&self.descriptor_sets)?;
let dynamic_offsets = self
.descriptor_sets
.iter()
.flat_map(|set| set.dynamic_offsets.iter().copied())
.collect::<Vec<_>>();
let descriptor_sets: Vec<_> = self.descriptor_sets.iter().map(|set| set.raw).collect();
if !descriptor_sets.is_empty() {
unsafe {
recorder.device.cmd_bind_descriptor_sets(
@ -276,7 +267,7 @@ impl RecordedCommand for Draw {
self.pipeline_layout,
0,
&descriptor_sets,
&dynamic_offsets,
&[],
);
}
}
@ -335,31 +326,6 @@ impl RecordedCommand for Draw {
}
}
fn validate_managed_descriptor_sets(
sets: &[GraphDescriptorSet],
) -> Result<Vec<vk::DescriptorSet>, RecordError> {
sets.iter()
.map(|set| {
if let Some(managed) = &set.managed {
let raw = managed.raw().map_err(|error| match error {
renderer_resources::descriptors::DescriptorSetError::StaleSet { .. } => {
RecordError::StaleDescriptorSet
}
error => RecordError::DescriptorSet(error.to_string()),
})?;
if raw != set.raw {
return Err(RecordError::DescriptorSet(
"managed descriptor set handle changed after import".into(),
));
}
Ok(raw)
} else {
Ok(set.raw)
}
})
.collect()
}
pub fn resource_id(buffer: Buffer) -> ResourceId {
buffer.id()
}

File diff suppressed because it is too large Load diff

View file

@ -9,7 +9,7 @@ pub mod resources;
pub use graph::{CompiledGraph, GraphError, RenderGraph, ResourceInterference};
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
pub use resources::{
Buffer, BufferRange, BufferSlice, DescriptorResourceAccess, DescriptorResourceIdentity,
GraphBufferDesc, GraphDescriptorSet, GraphTextureDesc, IndexBinding, IndexFormat,
IndirectBinding, PushConstantData, ResourceId, ResourceState, Texture, VertexBinding,
Buffer, BufferRange, BufferSlice, DescriptorResourceAccess, GraphBufferDesc,
GraphDescriptorSet, GraphTextureDesc, IndexBinding, IndexFormat, IndirectBinding,
PushConstantData, ResourceId, Texture, VertexBinding,
};

View file

@ -100,11 +100,6 @@ pub struct CommandList {
}
pub trait RecordedCommand {
/// Validate wrapper-backed state before any command in the graph is emitted.
fn preflight(&self) -> Result<(), RecordError> {
Ok(())
}
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError>;
}
@ -130,13 +125,6 @@ impl CommandList {
pub fn is_empty(&self) -> bool {
self.commands.is_empty()
}
pub(crate) fn preflight(&self) -> Result<(), RecordError> {
for command in &self.commands {
command.preflight()?;
}
Ok(())
}
}
impl Default for CommandList {
@ -151,8 +139,6 @@ pub enum RecordError {
MissingImage(ResourceId),
UnresolvedCreatedBuffer(ResourceId),
UnresolvedCreatedImage(ResourceId),
DescriptorSet(String),
StaleDescriptorSet,
}
/// Vulkan recording context. Handle maps are populated by the compiled graph.

View file

@ -4,7 +4,6 @@ use ash::vk;
use renderer_resources::{
buffers::Buffer as BufferResource,
images::{Image as ImageResource, ImageSubresourceExt},
pipeline::DescriptorSet,
};
/// Minimal graph-level buffer description. Buffer usage, queue sharing, and
@ -186,30 +185,15 @@ pub enum DescriptorResourceAccess {
}
/// A Vulkan descriptor set accompanied by graph-visible resource accesses.
/// Managed instances retain their descriptor wrapper and dynamic offsets;
/// raw instances created through `track_descriptor_set` are untracked and
/// require caller-managed resource lifetime and access declarations.
///
/// Callers must list every resource used by the set with its accurate range,
/// pipeline stage, access mask, and (for textures) image layout. The graph can
/// track these declarations, but cannot inspect or verify raw descriptor-set
/// contents.
#[derive(Debug, Clone)]
pub struct GraphDescriptorSet {
pub(crate) raw: vk::DescriptorSet,
pub raw: vk::DescriptorSet,
pub accesses: Vec<DescriptorResourceAccess>,
/// Retained managed set, when this packet was created by transitive import.
pub(crate) managed: Option<Arc<DescriptorSet>>,
/// Offsets ordered by the descriptor set's binding and array-element order.
pub(crate) dynamic_offsets: Vec<u32>,
}
impl GraphDescriptorSet {
pub fn dynamic_offsets(&self) -> &[u32] {
&self.dynamic_offsets
}
}
/// A resource identity used to supply explicit initial state during descriptor import.
#[derive(Debug, Clone)]
pub enum DescriptorResourceIdentity {
Buffer(Arc<BufferResource>),
Texture(Arc<ImageResource>),
}
#[derive(Debug, Clone)]

View file

@ -4,11 +4,10 @@
`renderer_resources` owns Vulkan buffer, image, view, descriptor, sampler, shader-module, and pipeline construction on the `renderer_device` layer. It handles resource memory and handle lifetimes, but contains no window, swapchain, or presentation orchestration; those remain in `renderer`. It must not depend on `renderer`.
## Module map
- `lib.rs`: re-exports `Device`, `Error`, `Result`, `device`, and `make_extension` from `renderer_device`; declares the public resource modules, including descriptor metadata and validation.
- `lib.rs`: re-exports `Device`, `Error`, `Result`, `device`, and `make_extension` from `renderer_device`; declares four public resource modules.
- `buffers.rs`: `BufferDesc` and `Buffer`; Vulkan buffer creation, GPU allocation/binding, raw access, and host mapping.
- `images.rs`: `ImageDesc`, `Image`, `ImageViewDesc`, `ImageView`, subresource ranges, extents, offsets, and queue ownership. `ImageInner` distinguishes allocated images from borrowed swapchain images.
- `pipeline.rs`: shader-stage and graphics/compute pipeline descriptions; `DescriptorPool`, `DescriptorSetLayout`, `PipelineLayout`, `Sampler`, `ShaderModule`, and `Pipeline`.
- `descriptors.rs`: managed descriptor binding/resource metadata, validation plans, errors, and pool-generation helpers; it remains independent of render-graph types.
- `util.rs`: `eq_f32`, `ord_f32`, `hash_f32`, `F32`, and the crate-private `weak_vec` module / `WeakVec` used by image view tracking.
## Key types and API
@ -26,13 +25,8 @@
- Fixed-function state records are `VertexInputState`, `TessellationState`, `InputAssemblyState`, `ViewportState`, `DepthBiasState`, `RasterizationState`, `MultisampleState`, `DepthBounds`, `DepthState`, `StencilState`, `DepthStencilState`, `ColorBlendState`, `RenderingState`, and `DynamicState`.
- `Pipeline` wraps graphics or compute Vulkan pipelines and retains its `PipelineLayout` in an `Arc`. Creation uses `device.pools.pipeline_cache.raw`.
- `DescriptorPool` serializes allocation/reset with a mutex; descriptor set layouts and pipeline layouts own their Vulkan handles.
- `DescriptorPool::allocate_untracked` returns raw descriptor-set handles without retaining layouts or bound resources. `allocate_managed` returns a generation-checked `DescriptorSet` that retains its pool, layout facts, and written resource owners.
- A managed `DescriptorSet` retains its pool-allocation token, layout facts, and the `Arc<Buffer>`, `Arc<ImageView>`, `Arc<Sampler>`, or `Arc<BufferView>` owners supplied for its written descriptors. Image views in turn retain their parent images.
- `DescriptorSet::raw()` is fallible and checks the pool generation before exposing the Vulkan handle. Generation checks also apply to binding metadata and writer operations; they detect sets invalidated by reset but cannot revoke escaped raw handles or make concurrent reset/use safe.
- `DescriptorSet::writer` stages buffer, image, sampler, and texel-buffer updates. `DescriptorWriter` validates the descriptor type, slot bounds, ranges, resource facts, and required enabled features before Vulkan updates; retained owners are replaced only after the Vulkan writes succeed. Partially-bound bindings may keep unwritten slots; other bindings must be complete at commit.
- Managed writes support `UNIFORM_BUFFER`, `UNIFORM_BUFFER_DYNAMIC`, `STORAGE_BUFFER`, `STORAGE_BUFFER_DYNAMIC`, `SAMPLED_IMAGE`, `STORAGE_IMAGE`, `COMBINED_IMAGE_SAMPLER`, `SAMPLER`, `UNIFORM_TEXEL_BUFFER`, and `STORAGE_TEXEL_BUFFER`. Input attachments, acceleration structures, inline uniform blocks, and all other descriptor types are rejected until explicitly implemented.
- Managed image writes reject swapchain-backed images (including combined image samplers) before updating Vulkan descriptors, because retaining an `ImageView` does not retain its swapchain. Use the untracked route only when the caller separately guarantees the external image/swapchain lifetime.
- `DescriptorSetLayout::new` chains per-binding descriptor flags when the device API/extension supports the Vulkan structure and validates requested flags against enabled descriptor-indexing features.
- `DescriptorPool::allocate` returns raw descriptor-set handles from the provided `DescriptorSetLayout` values and optionally assigns names from `DescriptorSetAllocDesc`.
- `DescriptorSetLayout::new` applies per-binding descriptor flags when the device supports Vulkan 1.2 or `VK_EXT_descriptor_indexing`.
- `PipelineLayoutDesc` references descriptor-set layouts and push-constant ranges; `PipelineLayout` owns the created Vulkan layout.
- `SamplerDesc` describes filtering, addressing, anisotropy, compare, LOD, and border settings; `Sampler::new` creates the Vulkan sampler.
- `ShaderModule::new_from_path` reads a shader file relative to the process working directory; file-open failures map to `Error::AssetMissing`, while read/Vulkan failures use their corresponding `Error` conversions. `new_from_memory` accepts SPIR-V words.
@ -51,13 +45,10 @@
- `ImageView` retains `Arc<Image>` so the source image outlives the view. `Image::create_view` returns `ManuallyDrop<ImageView>`; callers must transfer/drop it deliberately.
- `Image` tracks views through `WeakVec<ImageView>` to avoid a strong-reference cycle. The weak list does not keep views alive.
- `ImageView`'s `DeviceObject` owns and destroys the Vulkan view; its parent `Arc<Image>` must remain alive through view destruction.
- `Image::create_view` returns `ManuallyDrop<ImageView>`; when a managed descriptor needs an owned view, deliberately transfer it with `ManuallyDrop::into_inner` and place it in an `Arc`.
- `ImageDesc` and `BufferDesc` omit `alloc_scheme` from both `Hash` and `Eq`; their `Hash` implementations omit `name`, and `Eq` also omits `name`. Do not treat these descriptors as complete resource identities without accounting for these fields.
- Descriptor hashing/equality covers resource shape, usage, sharing, and memory location but not every construction input. Update both implementations together if adding descriptor-keyed resource deduplication.
- `Pipeline` retains its layout with `Arc`; descriptor-pool operations are mutex-serialized. Keep these lifetime and synchronization guarantees when changing constructors or wrappers.
- Descriptor indexing validation uses `Device::enabled_features()` (the feature snapshot requested for logical-device creation), not `Device::features()` (adapter-supported features). Managed layouts validate binding-flag and descriptor-type-specific feature requirements, and managed writes validate descriptor requirements against the same enabled snapshot.
- `DescriptorPool::allocate_untracked` (and its backwards-compatible `allocate` alias) is a raw escape hatch: it retains no layout or binding owners, owns no managed-set token, and is not eligible for transitive managed import. Any raw handles are the caller's responsibility.
- Managed pool `reset` and `destroy` are unsafe caller-synchronized operations. Before reset/destruction, callers must ensure relevant GPU work has completed and exclude all managed-set use, including allocation, writes, graph recording/import, and command-buffer recording/binding. Final pool destruction/drop has the same synchronization requirement. Generation checks are defense-in-depth and do not close concurrent reset/use races; wrapper drops do not prove GPU retirement. Any raw handle returned by `raw()` escapes these checks and remains the caller's responsibility. Raw/untracked reset and use are likewise caller-synchronized.
- `DescriptorPool::allocate` returns raw `vk::DescriptorSet` handles; keep the owning pool alive while those sets are in use. Descriptor sets are not wrapped in owning resource objects here.
- Descriptor-set layout binding flags are chained into layout creation when the device API version or descriptor-indexing extension supports them.
- Pipeline cache lifetime and teardown belong to `renderer_device`; use the device's existing cache rather than adding a resource-crate cache.
- Renderer shader assets remain under `crates/renderer/shaders`; this crate does not own those assets.

View file

@ -3,9 +3,6 @@ name = "renderer_resources"
version = "0.1.0"
edition = "2024"
[features]
testing = []
[dependencies]
renderer_device = { path = "../renderer_device" }
ash = { workspace = true }

View file

@ -1,4 +1,4 @@
use std::{borrow::Cow, sync::Arc};
use std::borrow::Cow;
use ash::vk;
use gpu_allocator::vulkan::AllocationScheme;
@ -73,34 +73,19 @@ impl Default for BufferDesc {
#[derive(Debug)]
pub struct Buffer {
buffer: BufferHandle,
buffer: DeviceObject<vk::Buffer>,
pub desc: BufferDesc,
alloc: Allocation,
}
#[derive(Debug)]
enum BufferHandle {
Owned(DeviceObject<vk::Buffer>),
#[cfg(feature = "testing")]
Testing(vk::Buffer),
}
impl Eq for Buffer {}
impl PartialEq for Buffer {
fn eq(&self, other: &Self) -> bool {
self.raw() == other.raw()
*self.buffer == *other.buffer
}
}
impl Buffer {
fn owned(&self) -> Option<&DeviceObject<vk::Buffer>> {
match &self.buffer {
BufferHandle::Owned(buffer) => Some(buffer),
#[cfg(feature = "testing")]
BufferHandle::Testing(_) => None,
}
}
pub fn new(device: Device, desc: BufferDesc) -> crate::Result<Self> {
let (buffer, requirements) = Self::new_raw(device.clone(), &desc)?;
@ -128,11 +113,7 @@ impl Buffer {
}
Ok(Self {
buffer: BufferHandle::Owned(DeviceObject::new_debug_named(
device.clone(),
buffer,
desc.name.clone(),
)),
buffer: DeviceObject::new_debug_named(device.clone(), buffer, desc.name.clone()),
desc,
alloc: Allocation::Owned(DeviceObject::new(device, alloc)),
})
@ -182,78 +163,9 @@ impl Buffer {
}
pub fn raw(&self) -> vk::Buffer {
match &self.buffer {
BufferHandle::Owned(buffer) => **buffer,
#[cfg(feature = "testing")]
BufferHandle::Testing(buffer) => *buffer,
}
*self.buffer
}
pub fn len(&self) -> u64 {
self.desc.size
}
/// Creates a non-owning buffer fixture for device-free integration tests.
///
/// This constructor is only available with the crate's `testing` feature. The raw handle is
/// never destroyed and must not be used for Vulkan calls.
#[cfg(feature = "testing")]
#[doc(hidden)]
pub fn for_testing(raw: vk::Buffer, size: u64) -> Self {
Self {
buffer: BufferHandle::Testing(raw),
desc: BufferDesc {
size,
..Default::default()
},
alloc: Allocation::Unmanaged,
}
}
}
/// Byte range selected by a buffer view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BufferRange {
pub offset: u64,
pub size: u64,
}
/// An owned Vulkan buffer view, retaining the buffer it refers to.
#[derive(Debug)]
pub struct BufferView {
raw: vk::BufferView,
buffer: Arc<Buffer>,
range: BufferRange,
}
impl BufferView {
pub fn new(buffer: Arc<Buffer>, format: vk::Format, range: BufferRange) -> crate::Result<Self> {
let info = vk::BufferViewCreateInfo::default()
.buffer(buffer.raw())
.format(format)
.offset(range.offset)
.range(range.size);
let Some(buffer_handle) = buffer.owned() else {
return Err(crate::Error::Unspecified);
};
let raw = unsafe { buffer_handle.device().raw.create_buffer_view(&info, None)? };
Ok(Self { raw, buffer, range })
}
pub fn raw(&self) -> vk::BufferView {
self.raw
}
pub fn buffer(&self) -> &Arc<Buffer> {
&self.buffer
}
pub fn range(&self) -> BufferRange {
self.range
}
}
impl Drop for BufferView {
fn drop(&mut self) {
if let Some(buffer) = self.buffer.owned() {
unsafe { buffer.device().raw.destroy_buffer_view(self.raw, None) };
}
}
}

View file

@ -1,987 +0,0 @@
//! Graph-independent descriptor resource metadata and write validation.
use std::sync::Arc;
use ash::vk;
use renderer_device::PhysicalDeviceFeatures;
use crate::{
buffers::{Buffer, BufferView},
images::ImageView,
pipeline::Sampler,
};
/// An owned resource entry suitable for retention by a tracked descriptor set.
#[derive(Debug, Clone)]
pub enum DescriptorResourceBinding {
Buffer {
buffer: Arc<Buffer>,
offset: u64,
range: u64,
dynamic: bool,
},
Image {
view: Arc<ImageView>,
sampler: Option<Arc<Sampler>>,
range: vk::ImageSubresourceRange,
layout: vk::ImageLayout,
},
Sampler {
sampler: Arc<Sampler>,
},
TexelBuffer {
view: Arc<BufferView>,
range: crate::buffers::BufferRange,
},
}
/// Errors produced by managed descriptor-set operations.
#[derive(Debug)]
pub enum DescriptorSetError {
Vulkan(crate::Error),
Validation(DescriptorValidationError),
StaleSet {
allocated_generation: u64,
pool_generation: u64,
},
IncompleteBinding {
binding: u32,
element: u32,
},
PoolStillRetained,
PoolGenerationExhausted,
}
pub type DescriptorSetResult<T> = Result<T, DescriptorSetError>;
impl std::fmt::Display for DescriptorSetError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Vulkan(error) => error.fmt(f),
Self::Validation(error) => error.fmt(f),
Self::StaleSet {
allocated_generation,
pool_generation,
} => write!(
f,
"descriptor set is stale (allocated at generation {allocated_generation}, pool is at generation {pool_generation})"
),
Self::IncompleteBinding { binding, element } => write!(
f,
"descriptor binding {binding} has unwritten array element {element}"
),
Self::PoolStillRetained => {
f.write_str("managed descriptor sets still retain this descriptor pool")
}
Self::PoolGenerationExhausted => f.write_str("descriptor pool generation exhausted"),
}
}
}
impl std::error::Error for DescriptorSetError {}
impl From<crate::Error> for DescriptorSetError {
fn from(error: crate::Error) -> Self {
Self::Vulkan(error)
}
}
impl From<DescriptorValidationError> for DescriptorSetError {
fn from(error: DescriptorValidationError) -> Self {
Self::Validation(error)
}
}
pub(crate) fn check_pool_generation(
allocated_generation: u64,
pool_generation: u64,
) -> DescriptorSetResult<()> {
if allocated_generation == pool_generation {
Ok(())
} else {
Err(DescriptorSetError::StaleSet {
allocated_generation,
pool_generation,
})
}
}
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PoolResetError<E> {
Reset(E),
GenerationExhausted,
}
pub(crate) fn pool_generation_after_reset<E>(
generation: u64,
reset: impl FnOnce() -> Result<(), E>,
) -> Result<u64, PoolResetError<E>> {
let next = generation
.checked_add(1)
.ok_or(PoolResetError::GenerationExhausted)?;
reset().map_err(PoolResetError::Reset)?;
Ok(next)
}
pub(crate) fn replace_binding_entries<T: Clone>(
current: &std::collections::BTreeMap<(u32, u32), T>,
writes: impl IntoIterator<Item = ((u32, u32), T)>,
) -> std::collections::BTreeMap<(u32, u32), T> {
let mut replacement = current.clone();
replacement.extend(writes);
replacement
}
pub(crate) fn clear_binding_entries<T>(
entries: &mut std::collections::BTreeMap<(u32, u32), T>,
binding: u32,
) {
entries.retain(|(entry_binding, _), _| *entry_binding != binding);
}
pub(crate) fn validate_binding_completeness<T>(
layout: &[crate::pipeline::DescriptorSetLayoutBindingDesc],
entries: &std::collections::BTreeMap<(u32, u32), T>,
) -> DescriptorSetResult<()> {
for binding in layout {
if binding
.flags
.unwrap_or_default()
.contains(vk::DescriptorBindingFlags::PARTIALLY_BOUND)
{
continue;
}
for element in 0..binding.count {
if !entries.contains_key(&(binding.binding, element)) {
return Err(DescriptorSetError::IncompleteBinding {
binding: binding.binding,
element,
});
}
}
}
Ok(())
}
/// An owned descriptor write. Texel-buffer kind is carried by `descriptor_type`.
#[derive(Debug, Clone)]
pub struct TrackedDescriptorWrite {
pub binding: u32,
pub array_element: u32,
pub descriptor_type: vk::DescriptorType,
pub stages: vk::ShaderStageFlags,
pub resource: DescriptorResourceBinding,
}
/// Resource facts attached to a raw Vulkan write, independent of Vulkan handles.
#[derive(Debug, Clone, Copy)]
pub enum RawDescriptorResourceFacts {
Buffer {
buffer_len: u64,
offset: u64,
range: u64,
dynamic: bool,
},
Image {
swapchain_backed: bool,
has_view: bool,
has_sampler: bool,
},
Sampler {
has_sampler: bool,
},
TexelBuffer {
buffer_len: u64,
offset: u64,
range: u64,
},
}
/// The descriptor-specific facts needed to validate a pending write.
#[derive(Debug, Clone, Copy)]
pub struct RawDescriptorWriteFacts {
pub binding: u32,
pub array_element: u32,
pub descriptor_type: vk::DescriptorType,
pub resource: RawDescriptorResourceFacts,
}
/// A validated write, with whole-size ranges normalized to concrete lengths.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ValidatedDescriptorWrite {
pub binding: u32,
pub array_element: u32,
pub descriptor_type: vk::DescriptorType,
pub normalized_range: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DescriptorValidationError {
UnknownBinding(u32),
ArrayElementOutOfBounds {
binding: u32,
element: u32,
count: u32,
},
TypeMismatch {
binding: u32,
expected: vk::DescriptorType,
actual: vk::DescriptorType,
},
UnsupportedDescriptorType(vk::DescriptorType),
ResourceTypeMismatch(vk::DescriptorType),
BufferRangeOutOfBounds,
InvalidDynamicAlignment,
MisalignedDynamicOffset,
MissingImageView,
MissingImageSampler,
MissingSampler,
SwapchainImage,
BindingNotPartiallyBound(u32),
FeatureNotEnabled {
binding: u32,
feature: &'static str,
},
}
impl std::fmt::Display for DescriptorValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnknownBinding(binding) => write!(
f,
"descriptor binding {binding} does not exist in the layout"
),
Self::ArrayElementOutOfBounds {
binding,
element,
count,
} => write!(
f,
"descriptor array element {element} is outside binding {binding}'s count {count}"
),
Self::TypeMismatch {
binding,
expected,
actual,
} => write!(
f,
"descriptor type {actual:?} does not match binding {binding}'s type {expected:?}"
),
Self::UnsupportedDescriptorType(kind) => write!(
f,
"descriptor type {kind:?} is unsupported for tracked writes"
),
Self::ResourceTypeMismatch(kind) => write!(
f,
"descriptor resource facts do not match descriptor type {kind:?}"
),
Self::BufferRangeOutOfBounds => {
f.write_str("descriptor buffer range exceeds the buffer length")
}
Self::InvalidDynamicAlignment => {
f.write_str("dynamic buffer descriptor has a zero alignment")
}
Self::MisalignedDynamicOffset => {
f.write_str("dynamic buffer descriptor offset is not aligned")
}
Self::MissingImageView => f.write_str("image descriptor requires an image view"),
Self::MissingImageSampler => {
f.write_str("combined image sampler descriptor requires a sampler")
}
Self::MissingSampler => f.write_str("sampler descriptor requires a sampler"),
Self::SwapchainImage => {
f.write_str("managed descriptor writes cannot reference swapchain-backed images")
}
Self::BindingNotPartiallyBound(binding) => write!(
f,
"descriptor binding {binding} cannot be cleared because it is not partially bound"
),
Self::FeatureNotEnabled { binding, feature } => write!(
f,
"binding {binding} requires an enabled descriptor-indexing feature: {feature}"
),
}
}
}
impl std::error::Error for DescriptorValidationError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DescriptorWritePlan {
pub writes: Vec<ValidatedDescriptorWrite>,
}
/// Checks all descriptor-indexing flags against the logical-device enabled-feature snapshot.
pub fn validate_descriptor_binding_features(
bindings: &[crate::pipeline::DescriptorSetLayoutBindingDesc],
enabled: &PhysicalDeviceFeatures,
) -> Result<(), DescriptorValidationError> {
for binding in bindings {
if !is_supported_descriptor_type(binding.kind) {
return Err(DescriptorValidationError::UnsupportedDescriptorType(
binding.kind,
));
}
let flags = binding.flags.unwrap_or_default();
let feature_check = |enabled: bool, name| {
if enabled {
Ok(())
} else {
Err(DescriptorValidationError::FeatureNotEnabled {
binding: binding.binding,
feature: name,
})
}
};
if flags.contains(vk::DescriptorBindingFlags::PARTIALLY_BOUND) {
feature_check(
enabled.core12.descriptor_binding_partially_bound != vk::FALSE,
"descriptorBindingPartiallyBound",
)?;
}
if flags.contains(vk::DescriptorBindingFlags::VARIABLE_DESCRIPTOR_COUNT) {
feature_check(
enabled.core12.descriptor_binding_variable_descriptor_count != vk::FALSE,
"descriptorBindingVariableDescriptorCount",
)?;
feature_check(
enabled.core12.runtime_descriptor_array != vk::FALSE,
"runtimeDescriptorArray",
)?;
}
if flags.contains(vk::DescriptorBindingFlags::UPDATE_UNUSED_WHILE_PENDING) {
feature_check(
enabled
.core12
.descriptor_binding_update_unused_while_pending
!= vk::FALSE,
"descriptorBindingUpdateUnusedWhilePending",
)?;
feature_check(
descriptor_type_update_after_bind_enabled(binding.kind, enabled),
descriptor_type_update_after_bind_feature(binding.kind),
)?;
}
if flags.contains(vk::DescriptorBindingFlags::UPDATE_AFTER_BIND) {
feature_check(
descriptor_type_update_after_bind_enabled(binding.kind, enabled),
descriptor_type_update_after_bind_feature(binding.kind),
)?;
}
}
Ok(())
}
/// Validates layout membership, array bounds, supported type, owner facts, and buffer ranges.
/// Dynamic alignment values are Vulkan's `min*BufferOffsetAlignment` limits.
pub fn validate_descriptor_writes(
layout: &[crate::pipeline::DescriptorSetLayoutBindingDesc],
writes: &[RawDescriptorWriteFacts],
enabled_features: &PhysicalDeviceFeatures,
min_uniform_buffer_offset_alignment: u64,
min_storage_buffer_offset_alignment: u64,
) -> Result<DescriptorWritePlan, DescriptorValidationError> {
validate_descriptor_binding_features(layout, enabled_features)?;
let mut validated = Vec::with_capacity(writes.len());
for write in writes {
let binding = layout
.iter()
.find(|binding| binding.binding == write.binding)
.ok_or(DescriptorValidationError::UnknownBinding(write.binding))?;
if write.array_element >= binding.count {
return Err(DescriptorValidationError::ArrayElementOutOfBounds {
binding: write.binding,
element: write.array_element,
count: binding.count,
});
}
if write.descriptor_type != binding.kind {
return Err(DescriptorValidationError::TypeMismatch {
binding: write.binding,
expected: binding.kind,
actual: write.descriptor_type,
});
}
if !is_supported_descriptor_type(write.descriptor_type) {
return Err(DescriptorValidationError::UnsupportedDescriptorType(
write.descriptor_type,
));
}
let normalized_range = match (write.descriptor_type, write.resource) {
(
vk::DescriptorType::UNIFORM_BUFFER
| vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC
| vk::DescriptorType::STORAGE_BUFFER
| vk::DescriptorType::STORAGE_BUFFER_DYNAMIC,
RawDescriptorResourceFacts::Buffer {
buffer_len,
offset,
range,
dynamic,
},
) => {
let range = normalize_buffer_range(buffer_len, offset, range)?;
if dynamic
!= (write.descriptor_type == vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC
|| write.descriptor_type == vk::DescriptorType::STORAGE_BUFFER_DYNAMIC)
{
return Err(DescriptorValidationError::ResourceTypeMismatch(
write.descriptor_type,
));
}
if dynamic {
let alignment =
if write.descriptor_type == vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC {
min_uniform_buffer_offset_alignment
} else {
min_storage_buffer_offset_alignment
};
if alignment == 0 {
return Err(DescriptorValidationError::InvalidDynamicAlignment);
}
if offset % alignment != 0 {
return Err(DescriptorValidationError::MisalignedDynamicOffset);
}
}
Some(range)
}
(
vk::DescriptorType::UNIFORM_TEXEL_BUFFER | vk::DescriptorType::STORAGE_TEXEL_BUFFER,
RawDescriptorResourceFacts::TexelBuffer {
buffer_len,
offset,
range,
},
) => Some(normalize_buffer_range(buffer_len, offset, range)?),
(
vk::DescriptorType::SAMPLED_IMAGE | vk::DescriptorType::STORAGE_IMAGE,
RawDescriptorResourceFacts::Image {
swapchain_backed,
has_view,
..
},
) => {
check_image_facts(swapchain_backed, has_view, false, false)?;
None
}
(
vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
RawDescriptorResourceFacts::Image {
swapchain_backed,
has_view,
has_sampler,
},
) => {
check_image_facts(swapchain_backed, has_view, has_sampler, true)?;
None
}
(vk::DescriptorType::SAMPLER, RawDescriptorResourceFacts::Sampler { has_sampler }) => {
if !has_sampler {
return Err(DescriptorValidationError::MissingSampler);
}
None
}
_ => {
return Err(DescriptorValidationError::ResourceTypeMismatch(
write.descriptor_type,
));
}
};
validated.push(ValidatedDescriptorWrite {
binding: write.binding,
array_element: write.array_element,
descriptor_type: write.descriptor_type,
normalized_range,
});
}
Ok(DescriptorWritePlan { writes: validated })
}
fn normalize_buffer_range(
buffer_len: u64,
offset: u64,
range: u64,
) -> Result<u64, DescriptorValidationError> {
if offset > buffer_len {
return Err(DescriptorValidationError::BufferRangeOutOfBounds);
}
let range = if range == vk::WHOLE_SIZE {
buffer_len - offset
} else {
range
};
if offset.checked_add(range).is_none_or(|end| end > buffer_len) {
return Err(DescriptorValidationError::BufferRangeOutOfBounds);
}
Ok(range)
}
fn check_image_facts(
swapchain_backed: bool,
has_view: bool,
has_sampler: bool,
sampler_required: bool,
) -> Result<(), DescriptorValidationError> {
if swapchain_backed {
return Err(DescriptorValidationError::SwapchainImage);
}
if !has_view {
return Err(DescriptorValidationError::MissingImageView);
}
if sampler_required && !has_sampler {
return Err(DescriptorValidationError::MissingImageSampler);
}
Ok(())
}
fn is_supported_descriptor_type(kind: vk::DescriptorType) -> bool {
matches!(
kind,
vk::DescriptorType::UNIFORM_BUFFER
| vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC
| vk::DescriptorType::STORAGE_BUFFER
| vk::DescriptorType::STORAGE_BUFFER_DYNAMIC
| vk::DescriptorType::SAMPLED_IMAGE
| vk::DescriptorType::STORAGE_IMAGE
| vk::DescriptorType::COMBINED_IMAGE_SAMPLER
| vk::DescriptorType::SAMPLER
| vk::DescriptorType::UNIFORM_TEXEL_BUFFER
| vk::DescriptorType::STORAGE_TEXEL_BUFFER
)
}
fn descriptor_type_update_after_bind_enabled(
kind: vk::DescriptorType,
features: &PhysicalDeviceFeatures,
) -> bool {
let core12 = &features.core12;
match kind {
vk::DescriptorType::UNIFORM_BUFFER | vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC => {
core12.descriptor_binding_uniform_buffer_update_after_bind != vk::FALSE
}
vk::DescriptorType::SAMPLED_IMAGE
| vk::DescriptorType::COMBINED_IMAGE_SAMPLER
| vk::DescriptorType::SAMPLER => {
core12.descriptor_binding_sampled_image_update_after_bind != vk::FALSE
}
vk::DescriptorType::STORAGE_IMAGE => {
core12.descriptor_binding_storage_image_update_after_bind != vk::FALSE
}
vk::DescriptorType::STORAGE_BUFFER | vk::DescriptorType::STORAGE_BUFFER_DYNAMIC => {
core12.descriptor_binding_storage_buffer_update_after_bind != vk::FALSE
}
vk::DescriptorType::UNIFORM_TEXEL_BUFFER => {
core12.descriptor_binding_uniform_texel_buffer_update_after_bind != vk::FALSE
}
vk::DescriptorType::STORAGE_TEXEL_BUFFER => {
core12.descriptor_binding_storage_texel_buffer_update_after_bind != vk::FALSE
}
_ => false,
}
}
fn descriptor_type_update_after_bind_feature(kind: vk::DescriptorType) -> &'static str {
match kind {
vk::DescriptorType::UNIFORM_BUFFER | vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC => {
"descriptorBindingUniformBufferUpdateAfterBind"
}
vk::DescriptorType::SAMPLED_IMAGE
| vk::DescriptorType::COMBINED_IMAGE_SAMPLER
| vk::DescriptorType::SAMPLER => "descriptorBindingSampledImageUpdateAfterBind",
vk::DescriptorType::STORAGE_IMAGE => "descriptorBindingStorageImageUpdateAfterBind",
vk::DescriptorType::STORAGE_BUFFER | vk::DescriptorType::STORAGE_BUFFER_DYNAMIC => {
"descriptorBindingStorageBufferUpdateAfterBind"
}
vk::DescriptorType::UNIFORM_TEXEL_BUFFER => {
"descriptorBindingUniformTexelBufferUpdateAfterBind"
}
vk::DescriptorType::STORAGE_TEXEL_BUFFER => {
"descriptorBindingStorageTexelBufferUpdateAfterBind"
}
_ => "descriptor type update-after-bind feature",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pipeline::DescriptorSetLayoutBindingDesc;
fn binding(
binding: u32,
kind: vk::DescriptorType,
count: u32,
) -> DescriptorSetLayoutBindingDesc {
DescriptorSetLayoutBindingDesc {
binding,
kind,
count,
stage: vk::ShaderStageFlags::FRAGMENT,
flags: None,
}
}
fn write(
binding: u32,
kind: vk::DescriptorType,
resource: RawDescriptorResourceFacts,
) -> RawDescriptorWriteFacts {
RawDescriptorWriteFacts {
binding,
array_element: 0,
descriptor_type: kind,
resource,
}
}
fn validate(
layout: &[DescriptorSetLayoutBindingDesc],
writes: &[RawDescriptorWriteFacts],
) -> Result<DescriptorWritePlan, DescriptorValidationError> {
validate_descriptor_writes(layout, writes, &PhysicalDeviceFeatures::default(), 16, 32)
}
#[test]
fn validates_supported_buffer_image_sampler_and_texel_writes() {
let layout = [
binding(0, vk::DescriptorType::UNIFORM_BUFFER, 1),
binding(1, vk::DescriptorType::SAMPLED_IMAGE, 1),
binding(2, vk::DescriptorType::SAMPLER, 1),
binding(3, vk::DescriptorType::UNIFORM_TEXEL_BUFFER, 1),
binding(4, vk::DescriptorType::COMBINED_IMAGE_SAMPLER, 1),
];
let writes = [
write(
0,
vk::DescriptorType::UNIFORM_BUFFER,
RawDescriptorResourceFacts::Buffer {
buffer_len: 128,
offset: 16,
range: vk::WHOLE_SIZE,
dynamic: false,
},
),
write(
1,
vk::DescriptorType::SAMPLED_IMAGE,
RawDescriptorResourceFacts::Image {
swapchain_backed: false,
has_view: true,
has_sampler: false,
},
),
write(
2,
vk::DescriptorType::SAMPLER,
RawDescriptorResourceFacts::Sampler { has_sampler: true },
),
write(
3,
vk::DescriptorType::UNIFORM_TEXEL_BUFFER,
RawDescriptorResourceFacts::TexelBuffer {
buffer_len: 64,
offset: 0,
range: 32,
},
),
write(
4,
vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
RawDescriptorResourceFacts::Image {
swapchain_backed: false,
has_view: true,
has_sampler: true,
},
),
];
let plan = validate(&layout, &writes).unwrap();
assert_eq!(plan.writes[0].normalized_range, Some(112));
assert_eq!(plan.writes[1].normalized_range, None);
assert_eq!(plan.writes[2].normalized_range, None);
assert_eq!(plan.writes[3].normalized_range, Some(32));
assert_eq!(plan.writes[4].normalized_range, None);
}
#[test]
fn reports_binding_element_type_and_buffer_range_errors() {
let layout = [binding(4, vk::DescriptorType::UNIFORM_BUFFER, 2)];
let facts = RawDescriptorResourceFacts::Buffer {
buffer_len: 32,
offset: 0,
range: 16,
dynamic: false,
};
assert_eq!(
validate(
&layout,
&[write(5, vk::DescriptorType::UNIFORM_BUFFER, facts)]
),
Err(DescriptorValidationError::UnknownBinding(5))
);
let mut out_of_bounds = write(4, vk::DescriptorType::UNIFORM_BUFFER, facts);
out_of_bounds.array_element = 2;
assert_eq!(
validate(&layout, &[out_of_bounds]),
Err(DescriptorValidationError::ArrayElementOutOfBounds {
binding: 4,
element: 2,
count: 2
})
);
assert!(matches!(
validate(
&layout,
&[write(4, vk::DescriptorType::STORAGE_BUFFER, facts)]
),
Err(DescriptorValidationError::TypeMismatch { .. })
));
let outside = RawDescriptorResourceFacts::Buffer {
buffer_len: 32,
offset: 24,
range: 16,
dynamic: false,
};
assert_eq!(
validate(
&layout,
&[write(4, vk::DescriptorType::UNIFORM_BUFFER, outside)]
),
Err(DescriptorValidationError::BufferRangeOutOfBounds)
);
}
#[test]
fn rejects_unsupported_and_swapchain_descriptors() {
let input_attachment = [binding(0, vk::DescriptorType::INPUT_ATTACHMENT, 1)];
assert_eq!(
validate(
&input_attachment,
&[write(
0,
vk::DescriptorType::INPUT_ATTACHMENT,
RawDescriptorResourceFacts::Sampler { has_sampler: false },
)]
),
Err(DescriptorValidationError::UnsupportedDescriptorType(
vk::DescriptorType::INPUT_ATTACHMENT
))
);
let image_layout = [binding(0, vk::DescriptorType::COMBINED_IMAGE_SAMPLER, 1)];
assert_eq!(
validate(
&image_layout,
&[write(
0,
vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
RawDescriptorResourceFacts::Image {
swapchain_backed: true,
has_view: true,
has_sampler: true,
},
)]
),
Err(DescriptorValidationError::SwapchainImage)
);
}
#[test]
fn checks_binding_flags_against_enabled_features_not_adapter_support() {
let layout = [DescriptorSetLayoutBindingDesc {
flags: Some(vk::DescriptorBindingFlags::PARTIALLY_BOUND),
..binding(7, vk::DescriptorType::SAMPLED_IMAGE, 4)
}];
let mut enabled = PhysicalDeviceFeatures::default();
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Err(DescriptorValidationError::FeatureNotEnabled {
binding: 7,
feature: "descriptorBindingPartiallyBound",
})
);
enabled.core12.descriptor_binding_partially_bound = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Ok(())
);
}
#[test]
fn validates_dynamic_buffer_alignment_and_flags() {
let dynamic = [binding(0, vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC, 1)];
let misaligned = RawDescriptorResourceFacts::Buffer {
buffer_len: 128,
offset: 4,
range: 16,
dynamic: true,
};
assert_eq!(
validate(
&dynamic,
&[write(
0,
vk::DescriptorType::UNIFORM_BUFFER_DYNAMIC,
misaligned
)]
),
Err(DescriptorValidationError::MisalignedDynamicOffset)
);
}
#[test]
fn generation_and_binding_map_helpers_track_invalidation_and_replacement() {
assert!(check_pool_generation(3, 3).is_ok());
assert!(matches!(
check_pool_generation(3, 4),
Err(DescriptorSetError::StaleSet {
allocated_generation: 3,
pool_generation: 4
})
));
let layout = [
DescriptorSetLayoutBindingDesc {
flags: Some(vk::DescriptorBindingFlags::PARTIALLY_BOUND),
..binding(0, vk::DescriptorType::SAMPLED_IMAGE, 3)
},
binding(1, vk::DescriptorType::SAMPLER, 1),
];
let mut entries = std::collections::BTreeMap::new();
entries.insert((0, 0), "old");
entries.insert((1, 0), "sampler");
assert!(validate_binding_completeness(&layout, &entries).is_ok());
let replaced = replace_binding_entries(&entries, [((0, 0), "new"), ((0, 2), "image")]);
assert_eq!(replaced.get(&(0, 0)), Some(&"new"));
assert_eq!(replaced.get(&(0, 1)), None);
assert_eq!(replaced.get(&(0, 2)), Some(&"image"));
assert_eq!(entries.get(&(0, 0)), Some(&"old"));
let incomplete = [binding(2, vk::DescriptorType::SAMPLER, 2)];
assert!(matches!(
validate_binding_completeness(&incomplete, &entries),
Err(DescriptorSetError::IncompleteBinding {
binding: 2,
element: 0
})
));
let unchanged = pool_generation_after_reset(4, || Err("reset failed"));
assert_eq!(unchanged, Err(PoolResetError::Reset("reset failed")));
assert_eq!(pool_generation_after_reset(4, || Ok::<_, ()>(())), Ok(5));
}
#[test]
fn clearing_binding_entries_releases_all_tracked_slots_only_for_that_binding() {
let retained = Arc::new(String::from("texture"));
let weak = Arc::downgrade(&retained);
let mut entries = std::collections::BTreeMap::from([
((0, 0), retained.clone()),
((0, 3), retained.clone()),
((1, 0), Arc::new(String::from("buffer"))),
]);
drop(retained);
clear_binding_entries(&mut entries, 0);
assert_eq!(entries.keys().copied().collect::<Vec<_>>(), [(1, 0)]);
assert_eq!(weak.strong_count(), 0);
}
#[test]
fn binding_map_replacement_retains_arc_owners() {
let owner = Arc::new(String::from("retained"));
let weak = Arc::downgrade(&owner);
let current = std::collections::BTreeMap::from([((0, 0), owner.clone())]);
drop(owner);
assert_eq!(weak.strong_count(), 1);
let replacement =
replace_binding_entries(&current, [((0, 1), Arc::new(String::from("second")))]);
drop(current);
assert_eq!(
weak.upgrade().as_deref().map(String::as_str),
Some("retained")
);
drop(replacement);
assert_eq!(weak.strong_count(), 0);
}
#[test]
fn update_after_bind_uses_feature_for_binding_descriptor_type() {
let layout = [DescriptorSetLayoutBindingDesc {
flags: Some(vk::DescriptorBindingFlags::UPDATE_AFTER_BIND),
..binding(2, vk::DescriptorType::STORAGE_BUFFER, 1)
}];
let mut enabled = PhysicalDeviceFeatures::default();
enabled
.core12
.descriptor_binding_uniform_buffer_update_after_bind = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Err(DescriptorValidationError::FeatureNotEnabled {
binding: 2,
feature: "descriptorBindingStorageBufferUpdateAfterBind",
})
);
enabled
.core12
.descriptor_binding_storage_buffer_update_after_bind = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Ok(())
);
}
#[test]
fn update_unused_while_pending_requires_type_specific_update_after_bind_feature() {
let layout = [DescriptorSetLayoutBindingDesc {
flags: Some(vk::DescriptorBindingFlags::UPDATE_UNUSED_WHILE_PENDING),
..binding(3, vk::DescriptorType::STORAGE_IMAGE, 1)
}];
let mut enabled = PhysicalDeviceFeatures::default();
enabled
.core12
.descriptor_binding_update_unused_while_pending = vk::TRUE;
enabled
.core12
.descriptor_binding_sampled_image_update_after_bind = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Err(DescriptorValidationError::FeatureNotEnabled {
binding: 3,
feature: "descriptorBindingStorageImageUpdateAfterBind",
})
);
enabled
.core12
.descriptor_binding_storage_image_update_after_bind = vk::TRUE;
assert_eq!(
validate_descriptor_binding_features(&layout, &enabled),
Ok(())
);
}
#[test]
fn buffer_is_arc_compatible() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Buffer>();
}
}

View file

@ -133,29 +133,19 @@ impl Default for ImageDesc {
enum ImageInner {
Swapchain(vk::Image, Device),
Allocated(DeviceObject<vk::Image>, Allocation),
#[cfg(feature = "testing")]
Testing(vk::Image),
}
impl ImageInner {
fn is_swapchain_backed(&self) -> bool {
matches!(self, Self::Swapchain(_, _))
}
fn image(&self) -> vk::Image {
match self {
Self::Swapchain(image, _) => *image,
Self::Allocated(image, _) => **image,
#[cfg(feature = "testing")]
Self::Testing(image) => *image,
}
}
fn device(&self) -> &Device {
match self {
Self::Swapchain(_, device) => device,
Self::Allocated(image, _) => image.device(),
#[cfg(feature = "testing")]
Self::Testing(_) => panic!("test image has no Vulkan device"),
}
}
@ -163,16 +153,12 @@ impl ImageInner {
match self {
Self::Swapchain(_, _) => None,
Self::Allocated(_, alloc) => Some(alloc),
#[cfg(feature = "testing")]
Self::Testing(_) => None,
}
}
fn allocation_mut(&mut self) -> Option<&mut Allocation> {
match self {
Self::Swapchain(_, _) => None,
Self::Allocated(_, alloc) => Some(alloc),
#[cfg(feature = "testing")]
Self::Testing(_) => None,
}
}
}
@ -293,20 +279,6 @@ impl Image {
}
}
/// Creates a non-owning image fixture for device-free integration tests.
///
/// This constructor is only available with the crate's `testing` feature. The raw handle is
/// never destroyed and must not be used for Vulkan calls.
#[cfg(feature = "testing")]
#[doc(hidden)]
pub fn for_testing(raw: vk::Image, desc: ImageDesc) -> Self {
Self {
image: ImageInner::Testing(raw),
desc,
views: Default::default(),
}
}
fn new_raw(
device: Device,
desc: &ImageDesc,
@ -419,11 +391,6 @@ impl Image {
self.image.allocation()
}
/// Whether this image wraps a swapchain-owned Vulkan image.
pub fn is_swapchain_backed(&self) -> bool {
self.image.is_swapchain_backed()
}
pub fn allocation_mut(&mut self) -> Option<&mut Allocation> {
self.image.allocation_mut()
}
@ -514,17 +481,20 @@ impl Image {
.view_type(desc.kind)
.format(desc.format)
.components(desc.components)
.subresource_range(image_view_subresource_range(&desc));
.subresource_range(
vk::ImageSubresourceRange::default()
.aspect_mask(desc.aspect)
.base_mip_level(desc.mip_range.start)
.level_count(desc.mip_range.len())
.base_array_layer(desc.layer_range.start)
.layer_count(desc.layer_range.len()),
);
let device = self.image.device();
let view = unsafe { device.raw.create_image_view(&create_info, None)? };
Ok(ManuallyDrop::new(ImageView {
view: ImageViewHandle::Owned(DeviceObject::new_debug_named(
device.clone(),
view,
desc.name.clone(),
)),
view: DeviceObject::new_debug_named(device.clone(), view, desc.name.clone()),
desc,
image: self.clone(),
}))
@ -1022,92 +992,19 @@ impl PartialEq for ImageViewDesc {
#[derive(Debug)]
pub struct ImageView {
view: ImageViewHandle,
view: DeviceObject<vk::ImageView>,
#[allow(dead_code)]
desc: ImageViewDesc,
image: Arc<Image>,
}
#[derive(Debug)]
enum ImageViewHandle {
Owned(DeviceObject<vk::ImageView>),
#[cfg(feature = "testing")]
Testing(vk::ImageView),
}
impl ImageView {
pub fn raw(&self) -> vk::ImageView {
match &self.view {
ImageViewHandle::Owned(view) => **view,
#[cfg(feature = "testing")]
ImageViewHandle::Testing(view) => *view,
}
*self.view
}
pub fn image(&self) -> &Arc<Image> {
&self.image
}
/// Returns the concrete subresource range used to create this view.
pub fn subresource_range(&self) -> vk::ImageSubresourceRange {
image_view_subresource_range(&self.desc)
}
/// Creates a non-owning image-view fixture for device-free integration tests.
///
/// This constructor is only available with the crate's `testing` feature. The raw handle is
/// never destroyed and must not be used for Vulkan calls.
#[cfg(feature = "testing")]
#[doc(hidden)]
pub fn for_testing(
raw: vk::ImageView,
image: Arc<Image>,
range: vk::ImageSubresourceRange,
) -> Self {
let mut desc = ImageViewDesc::color_2d();
desc.aspect = range.aspect_mask;
desc.mip_range = MipRange::new(
range.base_mip_level,
range.base_mip_level + range.level_count,
);
desc.layer_range = MipRange::new(
range.base_array_layer,
range.base_array_layer + range.layer_count,
);
Self {
view: ImageViewHandle::Testing(raw),
desc,
image,
}
}
}
fn image_view_subresource_range(desc: &ImageViewDesc) -> vk::ImageSubresourceRange {
vk::ImageSubresourceRange::default()
.aspect_mask(desc.aspect)
.base_mip_level(desc.mip_range.start)
.level_count(desc.mip_range.len())
.base_array_layer(desc.layer_range.start)
.layer_count(desc.layer_range.len())
}
#[cfg(test)]
mod descriptor_range_tests {
use super::*;
#[test]
fn normalized_range_accessor_resolves_remaining_sentinels() {
let mut desc = ImageViewDesc::color_2d()
.with_mip_range(1..)
.with_layer_range(2..);
desc.mip_range.set_max_end(6);
desc.layer_range.set_max_end(9);
let range = image_view_subresource_range(&desc);
assert_eq!(range.aspect_mask, vk::ImageAspectFlags::COLOR);
assert_eq!(range.base_mip_level, 1);
assert_eq!(range.level_count, 5);
assert_eq!(range.base_array_layer, 2);
assert_eq!(range.layer_count, 7);
}
}
pub struct QueueOwnership {

View file

@ -1,7 +1,6 @@
pub use renderer_device::{Device, Error, Result, device, make_extension};
pub mod buffers;
pub mod descriptors;
pub mod images;
pub mod pipeline;
pub mod util;

File diff suppressed because it is too large Load diff

View file

@ -1,375 +0,0 @@
# Descriptor Set Ownership & Tracking Plan
Status: approved direction, not yet implemented
Scope: `crates/renderer_device` (enabled-feature source), `crates/renderer_resources` (primary), `crates/renderer_rendergraph` (consumer), `crates/renderer` (egui migration, separate change)
## Problem
`renderer_resources::DescriptorPool::allocate` returns raw `vk::DescriptorSet` handles with no
record of what they bind (`pipeline.rs:233-287`). The render graph's draw packets therefore
require callers to manually declare every descriptor-bound resource with accurate
range/stage/access/layout (`GraphDescriptorSet` in `renderer_rendergraph/src/resources.rs:170-197`)
— manual, error-prone, and unverifiable. Bound resources can also be dropped while a set is
still in use; nothing detects this.
## Approved direction
1. **Managed descriptor sets retain owners of the resources and objects they bind** —
`Arc<Buffer>`, `Arc<ImageView>`, `Arc<Sampler>`, and texel-buffer views as appropriate.
`ImageView` retains its `Arc<Image>` (`images.rs:994-1008`), but this is not a swapchain
lifetime token (see below). This ownership protects CPU object lifetimes, not submitted GPU
work by itself.
2. **Importing a managed descriptor set into the graph transitively imports its resources.**
Identity uses `Arc::ptr_eq` first and raw-handle equality fallback. The graph deduplicates
resources and merges per-command draw-access scopes; descriptor use never changes an
imported resource's initial state.
3. **Future, out of scope:** graph-created descriptor sets binding graph-created transient
resources via a graph-owned writer built on a reusable validation/write-plan interface in
`renderer_resources`. It has no graph types and avoids forking descriptor validation.
4. The raw escape hatch stays, clearly named untracked, owning nothing and never eligible for
transitive import.
5. `renderer_resources` stays graph-agnostic: metadata is generic Vulkan facts; graph-side
mapping lives in `renderer_rendergraph`. Zero graph-shaped types in `renderer_resources`.
6. Descriptor owner replacement, managed-set/pool destruction, and reset are caller-synchronized
against GPU work for this skeleton. Dropping wrappers is not evidence that GPU use retired.
A later design may replace this contract with explicit retirement tokens/GPU-usage tracking.
Pool reset also invalidates every Vulkan set allocated from that pool, independently of GPU
retirement. Managed wrappers surviving a successful reset must fail validation before
wrapper-mediated import, update, or bind attempts; the generation check detects invalidation,
but cannot revoke raw handles already exposed by `raw()`. Reset must also be externally
synchronized against every managed-set use; generation validation does not make concurrent
reset/use safe.
7. Swapchain-backed images are rejected by managed image writes with an explicit error. A
managed `Arc<ImageView>` does not keep its swapchain alive (`renderer_resources/AGENTS.md:39-47`);
rejecting these images avoids representing a false ownership guarantee.
8. Documented invariant changes must be reflected in `crates/renderer_resources/AGENTS.md`
(it currently says descriptor sets are not wrapped in owning resource objects). Any
`renderer` re-export decisions are flagged separately.
## Design decisions
### Buffer Arc model
- Store `Arc<Buffer>` in managed sets; callers wrap buffers after construction.
Do NOT make `Buffer` internally Arc-backed.
- Rationale: least invasive; preserves value-based `Buffer::new`/`map`/`map_mut`
(`buffers.rs:75-89,145-160`) and raw-handle equality (`buffers.rs:81-86`).
- Same pattern as `ImageView` -> `Arc<Image>`.
### Managed set layout and supported descriptor types
- A managed set owns: its pool-allocation lifetime token, raw handle, retained layout facts
(binding number/type/count/stages/flags), and a binding map keyed by
`(binding, array_element)`.
- Supported managed descriptor types are `UNIFORM_BUFFER`, `UNIFORM_BUFFER_DYNAMIC`,
`STORAGE_BUFFER`, `STORAGE_BUFFER_DYNAMIC`, `SAMPLED_IMAGE`, `STORAGE_IMAGE`,
`COMBINED_IMAGE_SAMPLER`, `SAMPLER`, `UNIFORM_TEXEL_BUFFER`, and `STORAGE_TEXEL_BUFFER`.
Input attachments, acceleration structures, inline uniform blocks, and every other type
are explicitly rejected by managed allocation/write validation until separately designed.
- Binding entries retain the corresponding owners:
- uniform/storage buffer (including dynamic): `{ Arc<Buffer>, offset, range }`;
- sampled/storage image and combined image sampler: `{ Arc<ImageView>, optional Arc<Sampler>,
normalized subresource range, layout }` (combined requires a sampler);
- sampler-only: `{ Arc<Sampler> }` with no image/view;
- uniform/storage texel buffer: `{ Arc<BufferView>, texel range }` (or an equivalent
resource-crate-owned buffer-view wrapper that owns its Vulkan view and buffer).
- Swapchain-backed images are rejected for every managed image descriptor kind, including
combined image sampler. The error is returned before updating Vulkan descriptors; callers
needing swapchain images use the raw untracked route and must separately retain the frame /
swapchain token through completion.
- Slots may be empty only for `PARTIALLY_BOUND` bindings (egui needs sparse arrays +
update-after-bind). Writes replace retained entries only after the Vulkan update succeeds.
- Validate descriptor-indexing features for the actual binding flags and descriptor types
before creating a managed layout or set. Layout flags alone are not a capability check.
In particular validate each requested `PARTIALLY_BOUND`, `UPDATE_AFTER_BIND`,
`VARIABLE_DESCRIPTOR_COUNT`, and update-unused-while-pending behavior against enabled device
features and the specific descriptor type's feature bit; reject unsupported requests. This check
must use the enabled-feature snapshot exposed by `renderer_device::Device`, never the adapter's
queried-supported feature data (`DeviceInner::features()` currently returns
`adapter.features`; `renderer_device/src/device.rs:594-596`). `DeviceInner` currently retains
`adapter` and `enabled_extensions` but not enabled features (`device.rs:156-168`), although
`create_logical_device` receives the feature chain used for device creation (`device.rs:398-413`).
Add retained enabled-feature state and an accessor in `renderer_device`; layout creation and
managed write validation in `renderer_resources` must consult that accessor for binding flags and
type-specific requirements (including partially-bound, update-after-bind, runtime arrays, and
update-unused-while-pending). Keep this within the existing dependency direction:
`renderer_resources` consumes the data through its existing `renderer_device` re-exports; add
no dependency edge.
### Shared validation/write path
- Managed `DescriptorWriter::write_*` produces staged binding entries and raw Vulkan write
infos, then invokes a reusable `renderer_resources` validation/write-plan API. The API accepts
retained layout facts plus raw write infos/resource facts and returns a validated write plan
or structured errors. It checks descriptor type, binding/array bounds, descriptor ranges,
completeness/partial-binding rules, and type-specific owner facts before Vulkan is updated.
- Commit applies the validated Vulkan writes and only then swaps the retained owner entries.
The same graph-agnostic validation/write-plan interface is the future graph-owned writer's
route; it does not take graph types or rely on `write_raw_untracked`.
- `write_raw_untracked` remains a separate Vulkan-only escape hatch. It performs no managed
validation, retains no owners, and cannot be passed off as the shared managed write path.
### Untracked escape hatch and reset synchronization
- Renamed `allocate_untracked`: returns raw handles, retains no bindings, owns nothing, and
cannot be transitively imported.
- Managed-pool reset and destruction are explicitly externally synchronized operations. Before
reset/destruction, the caller must ensure every GPU submission that could access any set or
descriptor owner from the pool has completed, prevent new submissions and descriptor updates,
and exclude every concurrent managed-set use until reset/destruction is complete. This exclusion
covers allocation from the pool, writer updates/commit, graph recording and descriptor-set
import, and command-buffer recording/binding (as well as other operations that inspect or use a
managed set). This mirrors Vulkan's external-synchronization requirement for pool reset versus
descriptor-set use; it does not impose a stricter guarantee than Vulkan. This is an API safety
contract (prefer an `unsafe` reset entry point with this requirement in its safety docs);
wrapper-drop checks alone are insufficient.
- Every pool owns a monotonically increasing generation counter shared with its managed sets.
Allocation records the current generation in each wrapper. On a successful reset, while holding
the pool mutex and after Vulkan reset succeeds, increment the generation; failed reset leaves it
unchanged. Each managed-set operation that can expose or use the raw handle (`raw`, bindings/
writer access, write/commit, graph import, and command binding) must compare the recorded
generation with the pool's current generation and return a stale-set error on mismatch. Make
raw-handle access fallible (`raw() -> Result<vk::DescriptorSet>`) rather than exposing an
unchecked handle from a surviving wrapper. Recheck at graph import and command binding so a set
imported before reset is detected as stale afterward. These generation checks are defense-in-
depth for stale wrappers after reset, not a substitute for external synchronization: the pool
mutex is not held across command use, so a check immediately before binding cannot close the
check/use race if reset is concurrent. The generation mechanism detects invalidation of all
managed sets, including wrappers still strongly held by callers; reset does not need to find or
mutate those wrappers. `allocate_untracked` remains outside this managed-wrapper check and its
raw handles remain caller responsibility.
- A handle returned by `raw()` escapes the wrapper's control. Its generation check can reject a
wrapper already stale when `raw()` is called, but cannot eliminate the interval between the
check and use or protect an escaped handle if reset later occurs. Once a raw handle escapes,
the caller owns its validity and must uphold the same external-synchronization contract.
- The existing pool mutex only serializes CPU operations, not in-flight GPU work
(`pipeline.rs:253-285`). Dropping all managed wrappers does not prove GPU retirement. The same
caller synchronization applies to replacing/dropping retained binding owners: the caller must
not release or replace an owner while submitted GPU work may still use that descriptor
version. A future retirement-token/GPU-usage-tracking hook can automate this discipline.
- Raw/untracked reset remains caller-synchronized as well.
### Dynamic offsets
- Dynamic offsets are bind-time data, not owned resources. The graph packet carries offsets
per descriptor set, and the draw path constructs the single correctly ordered offset slice
required by `cmd_bind_descriptor_sets`: set order, then dynamic binding number and array
element order as specified by the bound pipeline layouts.
- Validate exact offset count, per-type Vulkan alignment, overflow, and effective range bounds
(`descriptor base offset + dynamic offset + range <= buffer size`) before accepting/recording
the draw. Keep base descriptor ranges for ownership and use effective ranges for draw scopes.
## Transitive import design
- `RenderGraph::import_descriptor_set(Arc<DescriptorSet>, initial_states, dynamic_offsets)
-> Result<GraphDescriptorSet, GraphError>`. `initial_states` explicitly supplies the initial
`ResourceState` for each unique resource referenced by the set (or a documented caller
default expanded to each resource); it is never inferred from descriptor layout/access.
The draw packet retains the `Arc<DescriptorSet>`, `raw()`, graph-derived draw accesses, and
per-set dynamic offsets.
- Descriptor-derived access/layout describes what a draw will USE. It contributes only
per-command draw-access scopes, which the command compiler already derives from
`GraphDescriptorSet.accesses` (`renderer_rendergraph/src/commands.rs:190-216`). The resource's
initial `ResourceState` describes state before graph execution and remains caller-supplied,
because initial barrier generation reads `ResourceDesc::initial_state`
(`renderer_rendergraph/src/graph.rs:352-358,422-447`).
- **Identity index / dedupe:** maintain a graph-local identity index for imported buffers and
textures. Look up by `Arc::ptr_eq` first and Vulkan raw-handle equality fallback. Both
`import_buffer` and `import_texture` use this index rather than unconditionally appending
(`graph.rs:226-245`); `import_descriptor_set` routes every retained buffer/image through those
same methods. A direct import followed by set import (or vice versa) yields one `ResourceId`.
- **Initial-state merge:** on a duplicate explicit direct import, validate the newly supplied
initial state against the resource's recorded initial state; if they differ in stage, access,
or layout, return a conflicting-initial-state graph error. Do not OR-merge initial states.
Set import requires caller input for each resource; if it encounters a resource already
imported, the explicit state must match exactly or import errors. This prevents a fabricated
initial state and ensures the initial barrier source is unambiguous.
- **Draw-access scopes:** descriptor access is derived per binding and per draw, never folded
into `ResourceState`:
- uniform buffer -> `UNIFORM_READ`, exact effective offset/range;
- storage buffer -> conservative `SHADER_STORAGE_READ | SHADER_STORAGE_WRITE`, effective
offset/range;
- sampled image -> shader sampled read + descriptor image layout and normalized view range;
- storage image -> storage read|write in `GENERAL` and normalized view range;
- combined image sampler -> sampled-image read scope (sampler itself has no memory resource
hazard);
- sampler-only -> no graph memory access;
- uniform/storage texel buffer -> corresponding shader read or conservative storage read|write
over the view's buffer range;
- unsupported descriptor kinds or missing required owners -> explicit import/write error,
never guessed access.
Shader stages map from retained layout shader stages to graph pipeline stages.
- **Draw-scope merge:** deduplicate scopes by imported resource identity within the command and
merge compatible scopes by OR'ing stage/access masks while preserving their covered ranges.
Buffer ranges may remain separate if they are not safely coalescible. Texture layout
conflicts apply to overlapping draw scopes and produce an import error; disjoint subresource
ranges may retain distinct layouts. No draw-scope merge alters the resource's initial state.
- `track_descriptor_set` remains an explicitly unsafe-by-contract/deprecated raw route: it
cannot own or verify resources, and must not be used as though managed imports were safe.
## API sketches
```rust
// renderer_resources
impl DescriptorPool {
pub fn allocate_untracked(&self, descs: &[DescriptorSetAllocDesc])
-> VkResult<Vec<vk::DescriptorSet>>;
pub fn allocate_managed(&self, desc: DescriptorSetAllocDesc) -> Result<DescriptorSet>;
}
pub struct DescriptorSet { /* pool token, raw handle, pool generation snapshot, layout facts, binding map */ }
pub enum DescriptorResourceBinding {
Buffer { buffer: Arc<Buffer>, offset: u64, range: u64, dynamic: bool },
Image { view: Arc<ImageView>, sampler: Option<Arc<Sampler>>,
range: vk::ImageSubresourceRange, layout: vk::ImageLayout },
Sampler { sampler: Arc<Sampler> },
TexelBuffer { view: Arc<BufferView>, range: BufferRange },
}
pub struct TrackedDescriptorWrite {
pub binding: u32, pub array_element: u32,
pub descriptor_type: vk::DescriptorType,
pub stages: vk::ShaderStageFlags,
pub resource: DescriptorResourceBinding,
}
pub struct DescriptorWritePlan { /* validated Vulkan writes + staged retained entries */ }
pub fn validate_descriptor_writes(
layout: &DescriptorLayoutFacts,
writes: &[RawDescriptorWriteFacts],
) -> Result<DescriptorWritePlan>;
impl DescriptorSet {
// Checks generation as defense-in-depth; caller must externally synchronize reset against
// every use. Once returned, raw-handle validity is the caller's responsibility.
pub fn raw(&self) -> Result<vk::DescriptorSet>;
pub fn bindings(&self) -> Result<impl Iterator<Item = &TrackedDescriptorWrite>>;
pub fn writer(&mut self) -> Result<DescriptorWriter<'_>>;
}
impl DescriptorWriter<'_> {
pub fn write_buffer(&mut self, binding: u32, first_element: u32,
values: &[BufferDescriptor<'_>]) -> Result<&mut Self>;
pub fn write_image(&mut self, binding: u32, first_element: u32,
values: &[ImageDescriptor<'_>]) -> Result<&mut Self>;
pub fn write_sampler(&mut self, binding: u32, first_element: u32,
values: &[Arc<Sampler>]) -> Result<&mut Self>;
pub fn write_texel_buffer(&mut self, binding: u32, first_element: u32,
values: &[Arc<BufferView>]) -> Result<&mut Self>;
pub fn commit(self, set: &mut DescriptorSet) -> Result<()>;
}
```
```rust
// renderer_rendergraph
impl RenderGraph {
pub fn import_descriptor_set(
&mut self,
set: Arc<DescriptorSet>,
initial_states: &[(DescriptorResourceIdentity, ResourceState)],
dynamic_offsets: &[u32],
) -> Result<GraphDescriptorSet, GraphError>;
}
pub struct GraphDescriptorSet {
/* private raw handle + retained Arc<DescriptorSet>; no unchecked public raw-handle field */
pub accesses: Vec<DescriptorResourceAccess>,
pub dynamic_offsets: Vec<u32>,
/* retained Arc<DescriptorSet> */
}
```
Exact signatures may be adjusted to existing project conventions during implementation.
## Staged tasks
### Task 1 — Enabled-feature source (`renderer_device`)
Retain the exact `PhysicalDeviceFeatures` core and versioned feature chain passed to
`create_logical_device` in `DeviceInner` as the enabled-feature snapshot, and expose it through a
clearly named accessor distinct from the existing adapter-supported `features()` accessor
(`device.rs:156-168,398-413,594-596`; feature-chain type and push logic in
`renderer_device/src/lib.rs:171-194`). Preserve the chain's enabled values after passing it to
`push_to_device_create_info`; do not substitute `adapter.features`, which represents queried
support. Keep the change in `renderer_device`, with `renderer_resources` consuming the accessor
through its existing re-exports and no new dependency edge.
Tests: device-level accessor reflects the values supplied for logical-device creation and remains
distinct from adapter support data.
Accept: `cargo test -p renderer_device`; `cargo check -p renderer_device`.
### Task 2 — Foundation (`renderer_resources`, no graph changes; depends on 1)
Retain layout binding descriptions; add normalized image-view range accessor; Arc-backed
binding entry types for the supported descriptor set; explicit unsupported-type and
swapchain-image errors; descriptor-indexing feature checks using Task 1's enabled-feature
accessor (both layout binding flags and type-specific descriptor features); and the reusable
graph-agnostic validation/write-plan interface.
Tests: binding lookup, range/type/count validation, partial binding, array element replacement,
dynamic binding metadata, sampler-only and texel-buffer owner retention, unsupported type/feature
rejection (including enabled-versus-supported feature cases), swapchain-backed image rejection,
and validation-plan errors.
Accept: `cargo test -p renderer_resources`; `cargo check -p renderer_resources`.
### Task 3 — Managed allocation / writer / reset (depends on 2)
Managed allocation, owned write updates using the shared plan, `allocate_untracked` naming, and
caller-synchronized managed-pool reset/destruction and owner replacement contract. Implement the
pool generation counter: capture it at managed allocation, increment only after successful reset,
and reject every operation on surviving stale wrappers, including raw access, writes, import, and
bind. Reset should be unsafe or clearly documented as requiring GPU completion and exclusion of
concurrent use; wrapper-drop alone does not unlock reset.
Tests: owners remain alive after caller drops Arcs; invalid writes leave old entries intact;
reset invalidates a still-live managed wrapper and subsequent raw access/write returns a stale-set
error; failed reset does not advance the generation; reset contract is tested at the API boundary;
tests verify generation checks reject already-stale wrappers but do not treat those checks as
concurrent-use protection; reset's external-synchronization requirement covers allocation, writer
updates, graph recording/import, and command-buffer recording/binding; no test treats wrapper drop
as proof of GPU retirement.
Accept: `cargo test -p renderer_resources`; `cargo check --workspace`.
### Task 4 — Graph transitive import (`renderer_rendergraph`, depends on 3)
Implement identity index and state-validation behavior in both `import_buffer` and
`import_texture` (replacing their current unconditional append behavior) and route descriptor
imports through those methods. Keep caller-supplied initial state separate from descriptor
draw-access scopes; define explicit per-resource initial state input and conflict errors.
Implement metadata-to-draw-scope mapping, layout conflict behavior, managed set retention,
dynamic-offset storage and ordered draw binding. Revalidate the managed-set generation at graph
import and immediately before command binding, returning an error for a wrapper already stale at
the check. This is defense-in-depth only: callers must externally synchronize reset against graph
recording/import and command-buffer recording/binding because revalidation cannot close a
concurrent reset check/use race.
`Draw::apply` must pass the combined offsets in
Vulkan-required order to `cmd_bind_descriptor_sets` rather than an empty slice.
Tests: direct+transitive dedupe in both import orders; duplicate direct imports with matching and
conflicting initial states; descriptor import initial-state conflicts; descriptor draw scopes do
not alter initial barrier sources; buffer/image access merge, incompatible overlapping texture
draw-layout error, sampled/storage mappings and ranges; an already-stale managed set is rejected at
import or bind after reset (not a claim that generation revalidation makes concurrent reset/use
safe); multiple dynamic bindings across
multiple sets, offset count/order, alignment, overflow/effective-range bounds; raw tracking
behavior.
Accept: `cargo test -p renderer_rendergraph`; `cargo check --workspace`.
### Task 5 — Documentation + egui migration (separate `renderer` change, depends on 3)
Revise resource invariants in `crates/renderer_resources/AGENTS.md` (reverse the current
raw-only statement). Decide public re-exports (egui can use managed set types internally without
public re-exports; `renderer::{buffers,images,pipeline}` are crate-private aliases,
`renderer/src/lib.rs:23`). Migrate egui's set creation/storage (`renderer/src/lib.rs:108-115,
155-210,337`), update and bind flow (`egui.rs:561-611,621-630,742-746`), preserving its sparse
update-after-bind array and uniform/storage buffer binding. Egui uses the legacy graph; its
manual texture-read declarations (`egui.rs:621-630`) remain necessary because Task 4 changes
only `renderer_rendergraph`, not the legacy graph. Its swapchain/external lifetime and submission
ordering must continue to satisfy the caller-synchronized contract.
Accept: `cargo check -p renderer`; `cargo check --workspace`.
## Risks / open questions
- `Image::create_view` returns `ManuallyDrop<ImageView>`; callers must deliberately transfer
it into an `Arc` (`images.rs:1001-1008`).
- Swapchain-backed images are deliberately rejected in managed image writes because `Image`
wrappers do not own swapchain image handles, and `ImageView` retains only `Arc<Image>`
(`renderer_resources/AGENTS.md:39-47`). The untracked path remains caller responsibility.
- GPU retirement is not mechanically tracked in this skeleton: managed owner replacement/drop,
set/pool destruction, and pool reset require caller synchronization with completed submissions.
Reset must additionally be externally synchronized against all managed-set use (allocation,
writer updates, graph recording/import, and command-buffer recording/binding), consistent with
Vulkan's own external-synchronization model. Dropping wrappers or taking the pool CPU mutex is
insufficient. Retirement tokens or a GPU-usage tracking hook are the explicit future path.
Separately, successful pool reset advances the pool generation, making every surviving managed
wrapper detectably stale; generation checks are defense-in-depth and do not replace the
GPU-completion/exclusion contract or close a concurrent check/use race. Escaped raw handles remain
the caller's validity responsibility.
- Supported managed descriptor types and rejected types are enumerated above; feature checks
must use the retained enabled-feature snapshot from `renderer_device` and cover
descriptor-indexing binding flags and type-specific requirements, not adapter support queries or
layout flags alone. Never silently omit bound resources from graph hazards.
- A raw Vulkan set or external `update_descriptor_sets` call bypasses tracking; the API must
distinguish "tracked through this wrapper" from merely possessing a raw set.
- Dynamic offsets are bound per draw, validated against binding order/alignment/effective
buffer bounds, and included in draw scopes; they are not implicit descriptor-set state.