Compare commits
No commits in common. "f7c67f987274bdf49fe3ed73ee7b74c2de3b1760" and "d901fe250bc603633cd2e42c087a189b7cf868f9" have entirely different histories.
f7c67f9872
...
d901fe250b
|
|
@ -1,6 +1,5 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
mem::ManuallyDrop,
|
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -486,13 +485,6 @@ pub fn egui_pass(
|
||||||
textures_indices.push(idx as u32);
|
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 num_draw_calls = draw_calls.len();
|
||||||
let vertices_size = vertices.len() * size_of::<Vertex>();
|
let vertices_size = vertices.len() * size_of::<Vertex>();
|
||||||
let indices_size = indices.len() * size_of::<u32>();
|
let indices_size = indices.len() * size_of::<u32>();
|
||||||
|
|
@ -566,110 +558,57 @@ pub fn egui_pass(
|
||||||
(staging, vertices, indices, draw_calls, texture_ids)
|
(staging, vertices, indices, draw_calls, texture_ids)
|
||||||
};
|
};
|
||||||
|
|
||||||
// This map contains only unique textures referenced by these meshes. Its insertion order
|
let descriptor_infos = textures
|
||||||
// is the exact index stored in `texture_ids`, matching `texture[index]` in egui.slang.
|
|
||||||
let descriptor_resources = textures
|
|
||||||
.values()
|
.values()
|
||||||
.map(|entry| {
|
.map(|entry| {
|
||||||
let texture = texture_handler.get_texture(entry.id).unwrap();
|
let texture = texture_handler.get_texture(entry.id).unwrap();
|
||||||
let view = Arc::new(ManuallyDrop::into_inner(
|
vk::DescriptorImageInfo {
|
||||||
texture
|
sampler: samplers.get_sampler(entry.as_sampler_desc()).unwrap(),
|
||||||
|
image_view: texture
|
||||||
.create_view(
|
.create_view(
|
||||||
ImageViewDesc::color_2d()
|
ImageViewDesc::color_2d()
|
||||||
.with_mip_range(0..1)
|
.with_mip_range(0..1)
|
||||||
.with_layer_range(0..1),
|
.with_layer_range(0..1),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap()
|
||||||
));
|
.raw(),
|
||||||
let sampler = samplers.get_sampler_owned(entry.as_sampler_desc()).unwrap();
|
image_layout: vk::ImageLayout::SHADER_READ_ONLY_OPTIMAL,
|
||||||
(view, sampler)
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let texture_ids = Arc::new(texture_ids);
|
let uniform_info = vk::DescriptorBufferInfo {
|
||||||
// Each pass gets its own managed set, so later passes/frames cannot mutate the contents
|
buffer: texture_ids.raw(),
|
||||||
// captured by this callback.
|
offset: 0,
|
||||||
let descriptor_pool = crate::pipeline::DescriptorPool::new(
|
range: texture_ids.len(),
|
||||||
dev.clone(),
|
};
|
||||||
crate::pipeline::DescriptorPoolDesc {
|
|
||||||
flags: vk::DescriptorPoolCreateFlags::UPDATE_AFTER_BIND,
|
let descriptor_writes = descriptor_infos
|
||||||
name: Some("egui-pass-descriptor-pool".into()),
|
.iter()
|
||||||
sizes: &[
|
.enumerate()
|
||||||
vk::DescriptorPoolSize {
|
.map(|(i, info)| {
|
||||||
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
|
vk::WriteDescriptorSet::default()
|
||||||
descriptor_count: TEXTURE_DESCRIPTOR_CAPACITY as u32,
|
.image_info(core::slice::from_ref(info))
|
||||||
},
|
.descriptor_count(1)
|
||||||
vk::DescriptorPoolSize {
|
.descriptor_type(vk::DescriptorType::COMBINED_IMAGE_SAMPLER)
|
||||||
ty: vk::DescriptorType::STORAGE_BUFFER,
|
.dst_set(egui_state.descriptor_set)
|
||||||
descriptor_count: 1,
|
.dst_binding(EguiState::TEXTURE_BINDING)
|
||||||
},
|
.dst_array_element(i as u32)
|
||||||
],
|
|
||||||
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"))?;
|
.chain(core::iter::once({
|
||||||
{
|
vk::WriteDescriptorSet::default()
|
||||||
let mut writer = descriptor_set
|
.buffer_info(core::slice::from_ref(&uniform_info))
|
||||||
.writer()
|
.descriptor_count(1)
|
||||||
.map_err(|_| crate::Error::Todo("egui descriptor set is stale"))?;
|
.dst_binding(EguiState::UNIFORM_BINDING)
|
||||||
writer
|
.descriptor_type(vk::DescriptorType::STORAGE_BUFFER)
|
||||||
.clear_binding(EguiState::TEXTURE_BINDING)
|
.dst_array_element(0)
|
||||||
.map_err(|_| crate::Error::Todo("egui descriptor binding clear failed"))?;
|
.dst_set(egui_state.descriptor_set)
|
||||||
// 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
|
.collect::<Vec<_>>();
|
||||||
// shader-array entries while preserving the mesh texture indices above.
|
|
||||||
if let Some((first_view, first_sampler)) = descriptor_resources.first() {
|
unsafe {
|
||||||
for array_element in descriptor_resources.len()..TEXTURE_DESCRIPTOR_CAPACITY {
|
dev.dev().update_descriptor_sets(&descriptor_writes, &[]);
|
||||||
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,
|
|
||||||
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_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
|
let to_remove_tex_ids = output
|
||||||
.textures_delta
|
.textures_delta
|
||||||
|
|
@ -692,15 +631,10 @@ pub fn egui_pass(
|
||||||
|
|
||||||
let record: Box<RecordFn> = Box::new({
|
let record: Box<RecordFn> = Box::new({
|
||||||
let pipeline = egui_state.pipeline.clone();
|
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();
|
let screen_rect = egui.screen_rect();
|
||||||
|
|
||||||
move |ctx: &RenderContext| -> crate::Result<()> {
|
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 cmd = &ctx.cmd;
|
||||||
let staging = ctx.get_buffer(draw_staging).unwrap();
|
let staging = ctx.get_buffer(draw_staging).unwrap();
|
||||||
let vertices = ctx.get_buffer(vertices).unwrap();
|
let vertices = ctx.get_buffer(vertices).unwrap();
|
||||||
|
|
@ -808,7 +742,7 @@ pub fn egui_pass(
|
||||||
cmd.bind_descriptor_sets(
|
cmd.bind_descriptor_sets(
|
||||||
pipeline.layout(),
|
pipeline.layout(),
|
||||||
vk::PipelineBindPoint::GRAPHICS,
|
vk::PipelineBindPoint::GRAPHICS,
|
||||||
&[descriptor_set_raw],
|
&[descriptor_set],
|
||||||
);
|
);
|
||||||
cmd.draw_indexed_indirect(
|
cmd.draw_indexed_indirect(
|
||||||
draw_calls.raw(),
|
draw_calls.raw(),
|
||||||
|
|
|
||||||
|
|
@ -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};
|
use std::{collections::HashMap, sync::Arc};
|
||||||
|
|
||||||
|
|
@ -18,14 +22,15 @@ pub mod util;
|
||||||
|
|
||||||
pub(crate) use renderer_resources::{buffers, images, pipeline};
|
pub(crate) use renderer_resources::{buffers, images, pipeline};
|
||||||
|
|
||||||
|
pub use renderer_device::{device, instance, queue, sync};
|
||||||
pub use renderer_device::{
|
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,
|
DebugUtils, DebugUtilsCreateInfo, Device, DeviceOwned, DeviceRef, Error,
|
||||||
ExtendsDeviceFeatures2Debug, ExtendsDeviceProperties2Debug, Extension, Instance,
|
ExtendsDeviceFeatures2Debug, ExtendsDeviceProperties2Debug, Extension, Instance,
|
||||||
PhysicalDeviceFeatures, PhysicalDeviceInfo, PhysicalDeviceProperties, Result,
|
PhysicalDeviceFeatures, PhysicalDeviceInfo, PhysicalDeviceProperties, Result,
|
||||||
SurfaceCapabilities, compatible_extension_properties, extension_intersection,
|
SurfaceCapabilities,
|
||||||
get_physical_device_features, get_physical_device_properties, make_extension,
|
|
||||||
};
|
};
|
||||||
pub use renderer_device::{device, instance, queue, sync};
|
|
||||||
pub use renderer_macros::DeviceRef;
|
pub use renderer_macros::DeviceRef;
|
||||||
|
|
||||||
mod texture {
|
mod texture {
|
||||||
|
|
@ -70,9 +75,11 @@ mod texture {
|
||||||
|
|
||||||
use util::Rgba;
|
use util::Rgba;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
pub struct SamplerCache {
|
pub struct SamplerCache {
|
||||||
device: Device,
|
device: Device,
|
||||||
samplers: HashMap<pipeline::SamplerDesc, Arc<pipeline::Sampler>>,
|
samplers: HashMap<pipeline::SamplerDesc, pipeline::Sampler>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SamplerCache {
|
impl SamplerCache {
|
||||||
|
|
@ -84,22 +91,16 @@ impl SamplerCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_sampler(&mut self, desc: pipeline::SamplerDesc) -> VkResult<vk::Sampler> {
|
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;
|
use std::collections::hash_map::Entry;
|
||||||
match self.samplers.entry(desc) {
|
let entry = match self.samplers.entry(desc) {
|
||||||
Entry::Occupied(entry) => Ok(entry.get().clone()),
|
Entry::Occupied(entry) => entry.get().raw(),
|
||||||
Entry::Vacant(entry) => {
|
Entry::Vacant(entry) => {
|
||||||
let sampler = Arc::new(pipeline::Sampler::new(self.device.clone(), entry.key())?);
|
let sampler = pipeline::Sampler::new(self.device.clone(), entry.key())?;
|
||||||
entry.insert(sampler.clone());
|
entry.insert(sampler).raw()
|
||||||
Ok(sampler)
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
Ok(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -107,6 +108,9 @@ impl SamplerCache {
|
||||||
pub struct EguiState {
|
pub struct EguiState {
|
||||||
textures: HashMap<egui::TextureId, EguiTextureInfo>,
|
textures: HashMap<egui::TextureId, EguiTextureInfo>,
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
descriptor_pool: pipeline::DescriptorPool,
|
||||||
|
descriptor_set: vk::DescriptorSet,
|
||||||
|
#[allow(unused)]
|
||||||
descriptor_layout: pipeline::DescriptorSetLayout,
|
descriptor_layout: pipeline::DescriptorSetLayout,
|
||||||
pipeline: Arc<pipeline::Pipeline>,
|
pipeline: Arc<pipeline::Pipeline>,
|
||||||
}
|
}
|
||||||
|
|
@ -151,35 +155,60 @@ impl EguiState {
|
||||||
const TEXTURE_BINDING: u32 = 0;
|
const TEXTURE_BINDING: u32 = 0;
|
||||||
const UNIFORM_BINDING: u32 = 1;
|
const UNIFORM_BINDING: u32 = 1;
|
||||||
pub fn new(device: Device) -> Result<Self> {
|
pub fn new(device: Device) -> Result<Self> {
|
||||||
let descriptor_layout = util::timed("Create Descriptor Set", || {
|
let (descriptor_pool, descriptor_layout, sets) =
|
||||||
let descriptor_layout = pipeline::DescriptorSetLayout::new(
|
util::timed("Create Descriptor Set", || {
|
||||||
device.clone(),
|
let descriptor_pool = pipeline::DescriptorPool::new(
|
||||||
pipeline::DescriptorSetLayoutDesc {
|
device.clone(),
|
||||||
flags: vk::DescriptorSetLayoutCreateFlags::UPDATE_AFTER_BIND_POOL,
|
pipeline::DescriptorPoolDesc {
|
||||||
name: Some("egui-descriptor-layout".into()),
|
flags: vk::DescriptorPoolCreateFlags::UPDATE_AFTER_BIND,
|
||||||
bindings: &[
|
name: Some("egui-descriptorpool".into()),
|
||||||
pipeline::DescriptorSetLayoutBindingDesc {
|
sizes: &[
|
||||||
binding: Self::TEXTURE_BINDING,
|
vk::DescriptorPoolSize {
|
||||||
count: 10,
|
ty: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
|
||||||
kind: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
|
descriptor_count: 10,
|
||||||
stage: vk::ShaderStageFlags::FRAGMENT,
|
},
|
||||||
flags: Some(
|
vk::DescriptorPoolSize {
|
||||||
vk::DescriptorBindingFlags::PARTIALLY_BOUND
|
ty: vk::DescriptorType::STORAGE_BUFFER,
|
||||||
| vk::DescriptorBindingFlags::UPDATE_AFTER_BIND,
|
descriptor_count: 1,
|
||||||
),
|
},
|
||||||
},
|
],
|
||||||
pipeline::DescriptorSetLayoutBindingDesc {
|
max_sets: 1,
|
||||||
binding: Self::UNIFORM_BINDING,
|
},
|
||||||
count: 1,
|
)?;
|
||||||
kind: vk::DescriptorType::STORAGE_BUFFER,
|
let descriptor_layout = pipeline::DescriptorSetLayout::new(
|
||||||
stage: vk::ShaderStageFlags::FRAGMENT,
|
device.clone(),
|
||||||
flags: None,
|
pipeline::DescriptorSetLayoutDesc {
|
||||||
},
|
flags: vk::DescriptorSetLayoutCreateFlags::UPDATE_AFTER_BIND_POOL,
|
||||||
],
|
name: Some("egui-descriptor-layout".into()),
|
||||||
},
|
bindings: &[
|
||||||
)?;
|
pipeline::DescriptorSetLayoutBindingDesc {
|
||||||
Result::Ok(descriptor_layout)
|
binding: Self::TEXTURE_BINDING,
|
||||||
})?;
|
count: 10,
|
||||||
|
kind: vk::DescriptorType::COMBINED_IMAGE_SAMPLER,
|
||||||
|
stage: vk::ShaderStageFlags::FRAGMENT,
|
||||||
|
flags: Some(
|
||||||
|
vk::DescriptorBindingFlags::PARTIALLY_BOUND
|
||||||
|
| vk::DescriptorBindingFlags::UPDATE_AFTER_BIND,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
pipeline::DescriptorSetLayoutBindingDesc {
|
||||||
|
binding: Self::UNIFORM_BINDING,
|
||||||
|
count: 1,
|
||||||
|
kind: vk::DescriptorType::STORAGE_BUFFER,
|
||||||
|
stage: vk::ShaderStageFlags::FRAGMENT,
|
||||||
|
flags: None,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
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(
|
let pipeline_layout = Arc::new(pipeline::PipelineLayout::new(
|
||||||
device.clone(),
|
device.clone(),
|
||||||
|
|
@ -303,7 +332,9 @@ impl EguiState {
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
textures: HashMap::new(),
|
textures: HashMap::new(),
|
||||||
|
descriptor_pool,
|
||||||
descriptor_layout,
|
descriptor_layout,
|
||||||
|
descriptor_set: sets[0],
|
||||||
pipeline: Arc::new(pipeline),
|
pipeline: Arc::new(pipeline),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -443,7 +474,9 @@ impl Renderer2 {
|
||||||
|
|
||||||
pub use vk::Extent2D;
|
pub use vk::Extent2D;
|
||||||
|
|
||||||
use crate::instance::InstanceDesc;
|
use crate::{
|
||||||
|
instance::InstanceDesc,
|
||||||
|
};
|
||||||
|
|
||||||
pub mod utils {
|
pub mod utils {
|
||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,7 @@ where
|
||||||
R: IntoImageSubresourceLayers,
|
R: IntoImageSubresourceLayers,
|
||||||
{
|
{
|
||||||
fn side_effects(&self, mut map: SideEffectMap) {
|
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 texels_per_block = self.dst.format().texels_per_block() as u64;
|
||||||
|
|
||||||
let src_id = self.src.id();
|
let src_id = self.src.id();
|
||||||
|
|
@ -144,7 +144,7 @@ where
|
||||||
let texel_count = if src_region.row_size != 0 {
|
let texel_count = if src_region.row_size != 0 {
|
||||||
let texel_count = src_region.row_size as u64 * src_region.row_count as u64;
|
let texel_count = src_region.row_size as u64 * src_region.row_count as u64;
|
||||||
debug_assert!(
|
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"
|
"Buffer copy region must be a multiple of the block size"
|
||||||
);
|
);
|
||||||
texel_count
|
texel_count
|
||||||
|
|
@ -217,7 +217,7 @@ where
|
||||||
R: IntoImageSubresourceLayers,
|
R: IntoImageSubresourceLayers,
|
||||||
{
|
{
|
||||||
fn side_effects(&self, mut map: SideEffectMap) {
|
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 texels_per_block = self.src.format().texels_per_block() as u64;
|
||||||
|
|
||||||
let src_id = self.src.id();
|
let src_id = self.src.id();
|
||||||
|
|
@ -232,7 +232,7 @@ where
|
||||||
let texel_count = if dst_region.row_size != 0 {
|
let texel_count = if dst_region.row_size != 0 {
|
||||||
let texel_count = dst_region.row_size as u64 * dst_region.row_count as u64;
|
let texel_count = dst_region.row_size as u64 * dst_region.row_count as u64;
|
||||||
debug_assert!(
|
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"
|
"Buffer copy region must be a multiple of the block size"
|
||||||
);
|
);
|
||||||
texel_count
|
texel_count
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@
|
||||||
## Key types and ownership
|
## Key types and ownership
|
||||||
- `Device(Arc<DeviceInner>)` is the cloneable owning handle. It dereferences to `DeviceInner`; equality is `Arc::ptr_eq`.
|
- `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`.
|
- `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`.
|
- `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.
|
- `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`.
|
- `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.
|
- `_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` 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.
|
- 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.
|
- `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.
|
- `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`.
|
- 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`.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ use std::{
|
||||||
sync::{Arc, Weak},
|
sync::{Arc, Weak},
|
||||||
};
|
};
|
||||||
|
|
||||||
use ash::{ext, khr, prelude::VkResult, vk};
|
use ash::{
|
||||||
|
ext, khr,
|
||||||
|
prelude::VkResult,
|
||||||
|
vk,
|
||||||
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use raw_window_handle::RawDisplayHandle;
|
use raw_window_handle::RawDisplayHandle;
|
||||||
|
|
||||||
|
|
@ -65,6 +69,8 @@ impl From<String> for DebugName {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result,
|
Instance, PhysicalDeviceFeatures, PhysicalDeviceInfo, Result,
|
||||||
device::asdf::traits::ExternallyManagedObject,
|
device::asdf::traits::ExternallyManagedObject,
|
||||||
|
|
@ -156,8 +162,6 @@ pub struct DeviceInner {
|
||||||
pub sync_threadpool: sync::SyncThreadpool,
|
pub sync_threadpool: sync::SyncThreadpool,
|
||||||
pub device_extensions: DeviceExtensions,
|
pub device_extensions: DeviceExtensions,
|
||||||
pub pools: DevicePools,
|
pub pools: DevicePools,
|
||||||
/// Features enabled when this logical device was created.
|
|
||||||
pub enabled_features: PhysicalDeviceFeatures,
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub enabled_extensions: Vec<&'static CStr>,
|
pub enabled_extensions: Vec<&'static CStr>,
|
||||||
|
|
||||||
|
|
@ -406,8 +410,6 @@ impl PhysicalDeviceInfo {
|
||||||
let create_info = vk::DeviceCreateInfo::default()
|
let create_info = vk::DeviceCreateInfo::default()
|
||||||
.queue_create_infos(&queue_create_infos)
|
.queue_create_infos(&queue_create_infos)
|
||||||
.enabled_extension_names(&extensions);
|
.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 create_info = features.push_to_device_create_info(create_info);
|
||||||
|
|
||||||
let device = unsafe {
|
let device = unsafe {
|
||||||
|
|
@ -462,7 +464,6 @@ impl PhysicalDeviceInfo {
|
||||||
queues: device_queues,
|
queues: device_queues,
|
||||||
device_extensions,
|
device_extensions,
|
||||||
enabled_extensions,
|
enabled_extensions,
|
||||||
enabled_features,
|
|
||||||
sync_threadpool: sync::SyncThreadpool::new(),
|
sync_threadpool: sync::SyncThreadpool::new(),
|
||||||
pools: DevicePools::new(inner.clone(), pipeline_cache),
|
pools: DevicePools::new(inner.clone(), pipeline_cache),
|
||||||
_drop: DeviceDrop {
|
_drop: DeviceDrop {
|
||||||
|
|
@ -498,10 +499,6 @@ impl PhysicalDeviceInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn enabled_features_snapshot(features: &PhysicalDeviceFeatures) -> PhysicalDeviceFeatures {
|
|
||||||
features.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct DevicePools {
|
pub struct DevicePools {
|
||||||
pub pipeline_cache: PipelineCache,
|
pub pipeline_cache: PipelineCache,
|
||||||
|
|
@ -597,12 +594,6 @@ impl DeviceInner {
|
||||||
pub fn features(&self) -> &crate::PhysicalDeviceFeatures {
|
pub fn features(&self) -> &crate::PhysicalDeviceFeatures {
|
||||||
&self.adapter.features
|
&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 {
|
pub fn properties(&self) -> &crate::PhysicalDeviceProperties {
|
||||||
&self.adapter.properties
|
&self.adapter.properties
|
||||||
}
|
}
|
||||||
|
|
@ -915,8 +906,8 @@ pub mod asdf {
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use ash::vk::Handle;
|
use ash::vk::Handle;
|
||||||
|
|
||||||
use crate::Device;
|
|
||||||
use crate::device::{DebugName, DeviceInner};
|
use crate::device::{DebugName, DeviceInner};
|
||||||
|
use crate::Device;
|
||||||
|
|
||||||
pub mod traits {
|
pub mod traits {
|
||||||
/// A trait describing an object owned by some manager-type, which is
|
/// A trait describing an object owned by some manager-type, which is
|
||||||
|
|
@ -1031,7 +1022,10 @@ pub mod asdf {
|
||||||
let inner: &T = &self.inner;
|
let inner: &T = &self.inner;
|
||||||
|
|
||||||
fmt.field_with("device", |f| {
|
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_with("handle", |f| write!(f, "{inner:?}"))
|
||||||
.field("name", &self.name);
|
.field("name", &self.name);
|
||||||
|
|
@ -1138,21 +1132,4 @@ pub mod asdf {
|
||||||
let _pool_owned: DeviceObject<vk::Semaphore> =
|
let _pool_owned: DeviceObject<vk::Semaphore> =
|
||||||
DeviceObject::new(summon::<Device>(), summon::<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
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -261,19 +261,25 @@ impl Instance {
|
||||||
raw: vk::SurfaceKHR,
|
raw: vk::SurfaceKHR,
|
||||||
present_mode: Option<vk::PresentModeKHR>,
|
present_mode: Option<vk::PresentModeKHR>,
|
||||||
) -> crate::Result<SurfaceCapabilities> {
|
) -> 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 =
|
let present_modes = unsafe {
|
||||||
unsafe { functor.get_physical_device_surface_present_modes(pdev, raw)? };
|
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) =
|
let (capabilities, present_modes) = if let Some(ref functor) =
|
||||||
self.extensions.get_surface_capabilities2
|
self.extensions.get_surface_capabilities2
|
||||||
&& self.extensions.surface_maintenance1.is_some()
|
&& self.extensions.surface_maintenance1.is_some()
|
||||||
{
|
{
|
||||||
let mut capabilities = vk::SurfaceCapabilities2KHR::default();
|
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 present_mode = present_mode.unwrap_or(present_modes[0]);
|
||||||
let mut surface_present_mode =
|
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 properties = get_physical_device_properties(&self.inner, pdev)?;
|
||||||
let features = get_physical_device_features(&self.inner, pdev, &properties);
|
let features = get_physical_device_features(&self.inner, pdev, &properties);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@ pub struct SurfaceCapabilities {
|
||||||
pub present_modes: Vec<vk::PresentModeKHR>,
|
pub present_modes: Vec<vk::PresentModeKHR>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug)]
|
||||||
pub struct PhysicalDeviceFeatures {
|
pub struct PhysicalDeviceFeatures {
|
||||||
pub core: vk::PhysicalDeviceFeatures,
|
pub core: vk::PhysicalDeviceFeatures,
|
||||||
pub core11: vk::PhysicalDeviceVulkan11Features<'static>,
|
pub core11: vk::PhysicalDeviceVulkan11Features<'static>,
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,10 @@ use std::{
|
||||||
|
|
||||||
use crate::device::{
|
use crate::device::{
|
||||||
Pool, PoolObject, Pooled, Recycle,
|
Pool, PoolObject, Pooled, Recycle,
|
||||||
asdf::{DeviceObject, traits::ExternallyManagedObject as ExternallyManagedObjectTrait},
|
asdf::{
|
||||||
|
DeviceObject,
|
||||||
|
traits::ExternallyManagedObject as ExternallyManagedObjectTrait,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use crate::{Result, device::DeviceInner};
|
use crate::{Result, device::DeviceInner};
|
||||||
|
|
||||||
|
|
@ -156,8 +159,12 @@ impl SyncThreadpool {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum Fence {
|
pub enum Fence {
|
||||||
Dedicated { fence: DeviceObject<vk::Fence> },
|
Dedicated {
|
||||||
Pooled { fence: PoolObject<vk::Fence> },
|
fence: DeviceObject<vk::Fence>,
|
||||||
|
},
|
||||||
|
Pooled {
|
||||||
|
fence: PoolObject<vk::Fence>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Pooled for vk::Fence {
|
impl Pooled for vk::Fence {
|
||||||
|
|
@ -272,14 +279,12 @@ pub enum SemaphoreInner {
|
||||||
impl Recycle for SemaphoreInner {
|
impl Recycle for SemaphoreInner {
|
||||||
fn recycle(self, owner: &Device) {
|
fn recycle(self, owner: &Device) {
|
||||||
match self {
|
match self {
|
||||||
SemaphoreInner::Binary(semaphore) => owner
|
SemaphoreInner::Binary(semaphore) => {
|
||||||
.pools
|
owner.pools.binary_semaphores.push(BinarySemaphore(semaphore))
|
||||||
.binary_semaphores
|
}
|
||||||
.push(BinarySemaphore(semaphore)),
|
SemaphoreInner::Timeline(semaphore) => {
|
||||||
SemaphoreInner::Timeline(semaphore) => owner
|
owner.pools.timeline_semaphores.push(TimelineSemaphore(semaphore))
|
||||||
.pools
|
}
|
||||||
.timeline_semaphores
|
|
||||||
.push(TimelineSemaphore(semaphore)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,3 @@ gpu-allocator.workspace = true
|
||||||
renderer_device = { path = "../renderer_device" }
|
renderer_device = { path = "../renderer_device" }
|
||||||
renderer_resources = { path = "../renderer_resources" }
|
renderer_resources = { path = "../renderer_resources" }
|
||||||
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
ash-formats = { git = "https://github.com/janis-bhm/ash-formats", branch = "main" }
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
renderer_resources = { path = "../renderer_resources", features = ["testing"] }
|
|
||||||
|
|
|
||||||
|
|
@ -219,10 +219,6 @@ impl Command for Draw {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecordedCommand 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> {
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError> {
|
||||||
for vertex in &self.vertices {
|
for vertex in &self.vertices {
|
||||||
let buffers = [recorder.buffer(vertex.slice.buffer.id())?];
|
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 descriptor_sets: Vec<_> = self.descriptor_sets.iter().map(|set| set.raw).collect();
|
||||||
let dynamic_offsets = self
|
|
||||||
.descriptor_sets
|
|
||||||
.iter()
|
|
||||||
.flat_map(|set| set.dynamic_offsets.iter().copied())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
if !descriptor_sets.is_empty() {
|
if !descriptor_sets.is_empty() {
|
||||||
unsafe {
|
unsafe {
|
||||||
recorder.device.cmd_bind_descriptor_sets(
|
recorder.device.cmd_bind_descriptor_sets(
|
||||||
|
|
@ -276,7 +267,7 @@ impl RecordedCommand for Draw {
|
||||||
self.pipeline_layout,
|
self.pipeline_layout,
|
||||||
0,
|
0,
|
||||||
&descriptor_sets,
|
&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 {
|
pub fn resource_id(buffer: Buffer) -> ResourceId {
|
||||||
buffer.id()
|
buffer.id()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -9,7 +9,7 @@ pub mod resources;
|
||||||
pub use graph::{CompiledGraph, GraphError, RenderGraph, ResourceInterference};
|
pub use graph::{CompiledGraph, GraphError, RenderGraph, ResourceInterference};
|
||||||
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
pub use recorder::{CommandIndex, CommandList, CommandRecorder, SideEffectMap};
|
||||||
pub use resources::{
|
pub use resources::{
|
||||||
Buffer, BufferRange, BufferSlice, DescriptorResourceAccess, DescriptorResourceIdentity,
|
Buffer, BufferRange, BufferSlice, DescriptorResourceAccess, GraphBufferDesc,
|
||||||
GraphBufferDesc, GraphDescriptorSet, GraphTextureDesc, IndexBinding, IndexFormat,
|
GraphDescriptorSet, GraphTextureDesc, IndexBinding, IndexFormat, IndirectBinding,
|
||||||
IndirectBinding, PushConstantData, ResourceId, ResourceState, Texture, VertexBinding,
|
PushConstantData, ResourceId, Texture, VertexBinding,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -100,11 +100,6 @@ pub struct CommandList {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait RecordedCommand {
|
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>;
|
fn apply(&mut self, recorder: &mut CommandRecorder<'_>) -> Result<(), RecordError>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -130,13 +125,6 @@ impl CommandList {
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.commands.is_empty()
|
self.commands.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn preflight(&self) -> Result<(), RecordError> {
|
|
||||||
for command in &self.commands {
|
|
||||||
command.preflight()?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for CommandList {
|
impl Default for CommandList {
|
||||||
|
|
@ -151,8 +139,6 @@ pub enum RecordError {
|
||||||
MissingImage(ResourceId),
|
MissingImage(ResourceId),
|
||||||
UnresolvedCreatedBuffer(ResourceId),
|
UnresolvedCreatedBuffer(ResourceId),
|
||||||
UnresolvedCreatedImage(ResourceId),
|
UnresolvedCreatedImage(ResourceId),
|
||||||
DescriptorSet(String),
|
|
||||||
StaleDescriptorSet,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Vulkan recording context. Handle maps are populated by the compiled graph.
|
/// Vulkan recording context. Handle maps are populated by the compiled graph.
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ use ash::vk;
|
||||||
use renderer_resources::{
|
use renderer_resources::{
|
||||||
buffers::Buffer as BufferResource,
|
buffers::Buffer as BufferResource,
|
||||||
images::{Image as ImageResource, ImageSubresourceExt},
|
images::{Image as ImageResource, ImageSubresourceExt},
|
||||||
pipeline::DescriptorSet,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Minimal graph-level buffer description. Buffer usage, queue sharing, and
|
/// 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.
|
/// 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
|
/// Callers must list every resource used by the set with its accurate range,
|
||||||
/// require caller-managed resource lifetime and access declarations.
|
/// 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)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct GraphDescriptorSet {
|
pub struct GraphDescriptorSet {
|
||||||
pub(crate) raw: vk::DescriptorSet,
|
pub raw: vk::DescriptorSet,
|
||||||
pub accesses: Vec<DescriptorResourceAccess>,
|
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)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -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`.
|
`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
|
## 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.
|
- `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.
|
- `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`.
|
- `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.
|
- `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
|
## 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`.
|
- 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`.
|
- `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` 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.
|
- `DescriptorPool::allocate` returns raw descriptor-set handles from the provided `DescriptorSetLayout` values and optionally assigns names from `DescriptorSetAllocDesc`.
|
||||||
- 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.
|
- `DescriptorSetLayout::new` applies per-binding descriptor flags when the device supports Vulkan 1.2 or `VK_EXT_descriptor_indexing`.
|
||||||
- `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.
|
|
||||||
- `PipelineLayoutDesc` references descriptor-set layouts and push-constant ranges; `PipelineLayout` owns the created Vulkan layout.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
- `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.
|
- 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.
|
- `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` 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.
|
||||||
- `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.
|
|
||||||
- Descriptor-set layout binding flags are chained into layout creation when the device API version or descriptor-indexing extension supports them.
|
- 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.
|
- 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.
|
- Renderer shader assets remain under `crates/renderer/shaders`; this crate does not own those assets.
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,6 @@ name = "renderer_resources"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[features]
|
|
||||||
testing = []
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
renderer_device = { path = "../renderer_device" }
|
renderer_device = { path = "../renderer_device" }
|
||||||
ash = { workspace = true }
|
ash = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::{borrow::Cow, sync::Arc};
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use ash::vk;
|
use ash::vk;
|
||||||
use gpu_allocator::vulkan::AllocationScheme;
|
use gpu_allocator::vulkan::AllocationScheme;
|
||||||
|
|
@ -73,34 +73,19 @@ impl Default for BufferDesc {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Buffer {
|
pub struct Buffer {
|
||||||
buffer: BufferHandle,
|
buffer: DeviceObject<vk::Buffer>,
|
||||||
pub desc: BufferDesc,
|
pub desc: BufferDesc,
|
||||||
alloc: Allocation,
|
alloc: Allocation,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum BufferHandle {
|
|
||||||
Owned(DeviceObject<vk::Buffer>),
|
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
Testing(vk::Buffer),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Eq for Buffer {}
|
impl Eq for Buffer {}
|
||||||
impl PartialEq for Buffer {
|
impl PartialEq for Buffer {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.raw() == other.raw()
|
*self.buffer == *other.buffer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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> {
|
pub fn new(device: Device, desc: BufferDesc) -> crate::Result<Self> {
|
||||||
let (buffer, requirements) = Self::new_raw(device.clone(), &desc)?;
|
let (buffer, requirements) = Self::new_raw(device.clone(), &desc)?;
|
||||||
|
|
||||||
|
|
@ -128,11 +113,7 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
buffer: BufferHandle::Owned(DeviceObject::new_debug_named(
|
buffer: DeviceObject::new_debug_named(device.clone(), buffer, desc.name.clone()),
|
||||||
device.clone(),
|
|
||||||
buffer,
|
|
||||||
desc.name.clone(),
|
|
||||||
)),
|
|
||||||
desc,
|
desc,
|
||||||
alloc: Allocation::Owned(DeviceObject::new(device, alloc)),
|
alloc: Allocation::Owned(DeviceObject::new(device, alloc)),
|
||||||
})
|
})
|
||||||
|
|
@ -182,78 +163,9 @@ impl Buffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn raw(&self) -> vk::Buffer {
|
pub fn raw(&self) -> vk::Buffer {
|
||||||
match &self.buffer {
|
*self.buffer
|
||||||
BufferHandle::Owned(buffer) => **buffer,
|
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
BufferHandle::Testing(buffer) => *buffer,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn len(&self) -> u64 {
|
pub fn len(&self) -> u64 {
|
||||||
self.desc.size
|
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) };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(¤t, [((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>();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -133,29 +133,19 @@ impl Default for ImageDesc {
|
||||||
enum ImageInner {
|
enum ImageInner {
|
||||||
Swapchain(vk::Image, Device),
|
Swapchain(vk::Image, Device),
|
||||||
Allocated(DeviceObject<vk::Image>, Allocation),
|
Allocated(DeviceObject<vk::Image>, Allocation),
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
Testing(vk::Image),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageInner {
|
impl ImageInner {
|
||||||
fn is_swapchain_backed(&self) -> bool {
|
|
||||||
matches!(self, Self::Swapchain(_, _))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn image(&self) -> vk::Image {
|
fn image(&self) -> vk::Image {
|
||||||
match self {
|
match self {
|
||||||
Self::Swapchain(image, _) => *image,
|
Self::Swapchain(image, _) => *image,
|
||||||
Self::Allocated(image, _) => **image,
|
Self::Allocated(image, _) => **image,
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
Self::Testing(image) => *image,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn device(&self) -> &Device {
|
fn device(&self) -> &Device {
|
||||||
match self {
|
match self {
|
||||||
Self::Swapchain(_, device) => device,
|
Self::Swapchain(_, device) => device,
|
||||||
Self::Allocated(image, _) => image.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 {
|
match self {
|
||||||
Self::Swapchain(_, _) => None,
|
Self::Swapchain(_, _) => None,
|
||||||
Self::Allocated(_, alloc) => Some(alloc),
|
Self::Allocated(_, alloc) => Some(alloc),
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
Self::Testing(_) => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn allocation_mut(&mut self) -> Option<&mut Allocation> {
|
fn allocation_mut(&mut self) -> Option<&mut Allocation> {
|
||||||
match self {
|
match self {
|
||||||
Self::Swapchain(_, _) => None,
|
Self::Swapchain(_, _) => None,
|
||||||
Self::Allocated(_, alloc) => Some(alloc),
|
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(
|
fn new_raw(
|
||||||
device: Device,
|
device: Device,
|
||||||
desc: &ImageDesc,
|
desc: &ImageDesc,
|
||||||
|
|
@ -419,11 +391,6 @@ impl Image {
|
||||||
self.image.allocation()
|
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> {
|
pub fn allocation_mut(&mut self) -> Option<&mut Allocation> {
|
||||||
self.image.allocation_mut()
|
self.image.allocation_mut()
|
||||||
}
|
}
|
||||||
|
|
@ -514,17 +481,20 @@ impl Image {
|
||||||
.view_type(desc.kind)
|
.view_type(desc.kind)
|
||||||
.format(desc.format)
|
.format(desc.format)
|
||||||
.components(desc.components)
|
.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 device = self.image.device();
|
||||||
let view = unsafe { device.raw.create_image_view(&create_info, None)? };
|
let view = unsafe { device.raw.create_image_view(&create_info, None)? };
|
||||||
|
|
||||||
Ok(ManuallyDrop::new(ImageView {
|
Ok(ManuallyDrop::new(ImageView {
|
||||||
view: ImageViewHandle::Owned(DeviceObject::new_debug_named(
|
view: DeviceObject::new_debug_named(device.clone(), view, desc.name.clone()),
|
||||||
device.clone(),
|
|
||||||
view,
|
|
||||||
desc.name.clone(),
|
|
||||||
)),
|
|
||||||
desc,
|
desc,
|
||||||
image: self.clone(),
|
image: self.clone(),
|
||||||
}))
|
}))
|
||||||
|
|
@ -1022,92 +992,19 @@ impl PartialEq for ImageViewDesc {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ImageView {
|
pub struct ImageView {
|
||||||
view: ImageViewHandle,
|
view: DeviceObject<vk::ImageView>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
desc: ImageViewDesc,
|
desc: ImageViewDesc,
|
||||||
image: Arc<Image>,
|
image: Arc<Image>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum ImageViewHandle {
|
|
||||||
Owned(DeviceObject<vk::ImageView>),
|
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
Testing(vk::ImageView),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ImageView {
|
impl ImageView {
|
||||||
pub fn raw(&self) -> vk::ImageView {
|
pub fn raw(&self) -> vk::ImageView {
|
||||||
match &self.view {
|
*self.view
|
||||||
ImageViewHandle::Owned(view) => **view,
|
|
||||||
#[cfg(feature = "testing")]
|
|
||||||
ImageViewHandle::Testing(view) => *view,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn image(&self) -> &Arc<Image> {
|
pub fn image(&self) -> &Arc<Image> {
|
||||||
&self.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 {
|
pub struct QueueOwnership {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
pub use renderer_device::{Device, Error, Result, device, make_extension};
|
pub use renderer_device::{Device, Error, Result, device, make_extension};
|
||||||
|
|
||||||
pub mod buffers;
|
pub mod buffers;
|
||||||
pub mod descriptors;
|
|
||||||
pub mod images;
|
pub mod images;
|
||||||
pub mod pipeline;
|
pub mod pipeline;
|
||||||
pub mod util;
|
pub mod util;
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -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.
|
|
||||||
Loading…
Reference in a new issue