vidya/crates/renderer_resources/src/buffers.rs
janis 5e4bc0ccf0
rendergraph: correct transient-resource aliasing with pooled placement
Lifetimes derive from the real dependency ordering (retained DAG +
compile-time happens-before closure), not command-index intervals.
Aliasing is restricted to the single-queue contract; alias reuse is
synchronized with memory/image barriers anchored at first-use commands.

- renderer_device: aliasable shared allocations (CAN_ALIAS), memory-
  requirements queries incl. dedicated hints, offset-aware
  bind_buffer/image_memory2, grouped bind under one allocator lock
- renderer_resources: unallocated resource creation, requirements
  cached in Allocation::Unmanaged (OnceLock), MemoryGroup pooling with
  intersecting memoryTypeBits, bind-time validation
- renderer_rendergraph: retained dependency DAG, happens_before
  closure, batch lifetimes, Placement type, plan_placements with
  pooled aliasing (first-fit over offset-sorted ranges, pool classes
  split by bufferImageGranularity), alias-barrier construction,
  placement/bind error variants
- perf: quadratic compile/resolve paths eliminated (per-resource
  indexing, frontier caching, backward-scan prune, cached recording
  index, recorder scratch reuse); single requirements query per
  resource through plan/validate/bind
- design plan + GPU-gated validation tests
2026-10-27 01:57:27 +01:00

641 lines
21 KiB
Rust

use std::{borrow::Cow, sync::Arc};
use ash::vk;
use renderer_device::queue::QueueFamilyIndices;
use crate::{
Device,
device::{
Allocation, AllocationDesc, AllocationStrategy, DeviceObject, MemoryLocation, QueueFlags,
},
};
#[derive(Clone)]
pub struct BufferDesc {
pub flags: vk::BufferCreateFlags,
pub name: Option<Cow<'static, str>>,
pub size: u64,
pub usage: vk::BufferUsageFlags,
pub queue_families: QueueFlags,
pub mem_location: MemoryLocation,
pub alloc_scheme: AllocationStrategy,
}
impl std::hash::Hash for BufferDesc {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.flags.hash(state);
self.size.hash(state);
self.usage.hash(state);
self.queue_families.hash(state);
self.mem_location.hash(state);
}
}
impl std::fmt::Debug for BufferDesc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BufferDesc")
.field("flags", &self.flags)
.field("name", &self.name)
.field("size", &self.size)
.field("usage", &self.usage)
.field("queue_families", &self.queue_families)
.field("mem_location", &self.mem_location)
.field("alloc_scheme", &self.alloc_scheme)
.finish()
}
}
impl Eq for BufferDesc {}
impl PartialEq for BufferDesc {
fn eq(&self, other: &Self) -> bool {
self.flags == other.flags
// for hashmaps, `Eq` may be more strict than `Hash`
&& self.size == other.size
&& self.usage == other.usage
&& self.queue_families == other.queue_families
&& self.mem_location == other.mem_location
}
}
impl Default for BufferDesc {
fn default() -> Self {
Self {
flags: Default::default(),
name: Default::default(),
size: Default::default(),
usage: Default::default(),
queue_families: QueueFlags::empty(),
mem_location: MemoryLocation::Unknown,
alloc_scheme: AllocationStrategy::default(),
}
}
}
#[derive(Debug)]
pub struct Buffer {
buffer: BufferHandle,
pub desc: BufferDesc,
alloc: Allocation,
}
#[derive(Debug)]
enum BufferHandle {
Owned(DeviceObject<vk::Buffer>),
#[cfg(feature = "testing")]
Testing(vk::Buffer),
}
impl Eq for Buffer {}
impl PartialEq for Buffer {
fn eq(&self, other: &Self) -> bool {
self.raw() == other.raw()
}
}
impl Buffer {
fn owned(&self) -> Option<&DeviceObject<vk::Buffer>> {
match &self.buffer {
BufferHandle::Owned(buffer) => Some(buffer),
#[cfg(feature = "testing")]
BufferHandle::Testing(_) => None,
}
}
pub fn new(device: Device, desc: BufferDesc) -> crate::Result<Self> {
let buffer = Self::new_raw(device.clone(), &desc)?;
let alloc = device.allocate_for_buffer(
*buffer,
AllocationDesc {
name: desc.name.as_deref().unwrap_or_default(),
location: desc.mem_location,
strategy: desc.alloc_scheme,
},
)?;
let buffer = Self {
buffer: BufferHandle::Owned(buffer),
desc,
alloc: Allocation::Owned(DeviceObject::new(device.clone(), alloc)),
};
device.bind_buffer_memory(
buffer.alloc.allocation().expect("owned buffer allocation"),
buffer.raw(),
)?;
Ok(buffer)
}
/// Creates a Vulkan buffer without allocating or binding memory.
pub fn new_unallocated(device: Device, desc: BufferDesc) -> crate::Result<Self> {
let buffer = Self::new_raw(device, &desc)?;
Ok(Self {
buffer: BufferHandle::Owned(buffer),
desc,
alloc: Allocation::Unmanaged {
requirements: std::sync::OnceLock::new(),
},
})
}
/// Returns the Vulkan memory requirements, including dedicated-allocation hints.
pub fn memory_requirements(&self) -> crate::memory::Result<crate::memory::MemoryRequirements> {
let Some(buffer) = self.owned() else {
return Err(crate::memory::Error::RequirementQuery(
crate::Error::Unspecified,
));
};
let requirements_cache = match &self.alloc {
Allocation::Unmanaged { requirements } => Some(requirements),
Allocation::Owned(_) | Allocation::Shared(_) => None,
};
if let Some(requirements) = requirements_cache.and_then(std::sync::OnceLock::get) {
return Ok(*requirements);
}
let requirements = buffer
.device()
.buffer_memory_requirements(**buffer)
.map_err(crate::memory::Error::RequirementQuery)?;
if let Some(cache) = requirements_cache {
let _ = cache.set(requirements);
}
Ok(requirements)
}
/// Binds this buffer into an aliasable allocation at an allocation-local offset.
///
/// Requirements are queried once and cached in the resource. The Vulkan bind remains the
/// authoritative validation.
pub fn bind_allocation(
&mut self,
allocation: Arc<DeviceObject<crate::device::GpuAllocation>>,
offset: u64,
) -> crate::memory::Result<()> {
if !matches!(self.alloc, Allocation::Unmanaged { .. }) {
return Err(crate::memory::Error::AlreadyBound);
}
let requirements = self.memory_requirements()?;
crate::memory::validate_binding(&requirements, allocation.as_ref(), offset)?;
let device = self.owned().expect("owned buffer").device();
self.alloc = device
.bind_buffer_at_offset(self.raw(), &requirements, allocation, offset)
.map_err(crate::memory::Error::Bind)?;
Ok(())
}
pub(crate) fn validate_unallocated(&self) -> crate::memory::Result<()> {
if matches!(self.alloc, Allocation::Unmanaged { .. }) {
Ok(())
} else {
Err(crate::memory::Error::AlreadyBound)
}
}
pub(crate) fn validate_device(&self, device: &Device) -> crate::memory::Result<()> {
if self.owned().expect("unallocated buffer is owned").device() == device {
Ok(())
} else {
Err(crate::memory::Error::Bind(
crate::Error::AllocationDeviceMismatch,
))
}
}
pub(crate) fn set_shared_allocation(
&mut self,
allocation: Arc<DeviceObject<crate::device::GpuAllocation>>,
) {
debug_assert!(matches!(self.alloc, Allocation::Unmanaged { .. }));
self.alloc = Allocation::Shared(allocation);
}
fn new_raw(device: Device, desc: &BufferDesc) -> crate::Result<DeviceObject<vk::Buffer>> {
let queue_families = device.queues.family_indices(desc.queue_families);
let sharing_mode = if queue_families.len() > 1 {
vk::SharingMode::CONCURRENT
} else {
vk::SharingMode::EXCLUSIVE
};
let create_info = vk::BufferCreateInfo::default()
.size(desc.size)
.usage(desc.usage)
.queue_family_indices(queue_families.as_u32_slice())
.sharing_mode(sharing_mode);
let buffer = unsafe { device.dev().create_buffer(&create_info, None)? };
Ok(DeviceObject::new_debug_named(
device,
buffer,
desc.name.clone(),
))
}
pub fn map(&mut self) -> Option<&[u8]> {
if let Some(alloc) = self.alloc.allocation() {
alloc
.mapped_slice()
.map(|slice| &slice[..self.desc.size as usize])
} else {
None
}
}
pub fn map_mut(&mut self) -> Option<&mut [u8]> {
if let Some(alloc) = self.alloc.allocation_mut() {
alloc
.mapped_slice_mut()
.map(|slice| &mut slice[..self.desc.size as usize])
} else {
None
}
}
pub fn raw(&self) -> vk::Buffer {
match &self.buffer {
BufferHandle::Owned(buffer) => **buffer,
#[cfg(feature = "testing")]
BufferHandle::Testing(buffer) => *buffer,
}
}
pub fn len(&self) -> u64 {
self.desc.size
}
/// Creates a non-owning buffer fixture for device-free integration tests.
///
/// This constructor is only available with the crate's `testing` feature. The raw handle is
/// never destroyed and must not be used for Vulkan calls.
#[cfg(feature = "testing")]
#[doc(hidden)]
pub fn for_testing(raw: vk::Buffer, size: u64) -> Self {
Self {
buffer: BufferHandle::Testing(raw),
desc: BufferDesc {
size,
..Default::default()
},
alloc: Allocation::Unmanaged {
requirements: std::sync::OnceLock::new(),
},
}
}
}
#[cfg(test)]
mod allocation_validation_tests {
use ash::vk;
use renderer_device::testing::TestVulkan;
use super::{Buffer, BufferDesc};
use crate::{
device::MemoryLocation,
images::{Image, ImageDesc},
};
use renderer_device::device::AllocationStrategy;
#[test]
fn vma_mapping_and_dedicated_resources_under_validation() {
let test = TestVulkan::expect();
let device = test.device().clone();
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Buffer>();
assert_send_sync::<Image>();
let mut mapped_buffer = Buffer::new(
device.clone(),
BufferDesc {
size: 64,
usage: vk::BufferUsageFlags::TRANSFER_SRC,
mem_location: MemoryLocation::CpuToGpu,
..Default::default()
},
)
.expect("create CpuToGpu buffer");
let allocation = mapped_buffer
.alloc
.allocation()
.expect("CpuToGpu buffer allocation");
let memory_type = test
.physical_device()
.properties
.memory_properties
.memory_types[allocation.memory_type_index() as usize];
assert!(memory_type.property_flags.contains(
vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT
));
let expected = (0..64).map(|byte| byte as u8).collect::<Vec<_>>();
mapped_buffer
.map_mut()
.expect("CpuToGpu buffer is mapped")
.copy_from_slice(&expected);
assert_eq!(
mapped_buffer.map().expect("CpuToGpu buffer remains mapped"),
expected
);
let dedicated_buffer = Buffer::new(
device.clone(),
BufferDesc {
size: 256,
usage: vk::BufferUsageFlags::TRANSFER_DST,
alloc_scheme: AllocationStrategy::Dedicated,
..Default::default()
},
)
.expect("create dedicated buffer");
assert!(
dedicated_buffer
.alloc
.allocation()
.expect("dedicated buffer allocation")
.is_dedicated(),
"VMA should allocate the dedicated buffer from its own memory block"
);
let dedicated_image = Image::new(
device,
ImageDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 16,
height: 16,
depth: 1,
},
usage: vk::ImageUsageFlags::SAMPLED,
alloc_scheme: AllocationStrategy::Dedicated,
..Default::default()
},
)
.expect("create dedicated image");
assert!(
dedicated_image
.allocation()
.expect("dedicated image allocation")
.allocation()
.expect("dedicated image VMA allocation")
.is_dedicated(),
"VMA should allocate the dedicated image from its own memory block"
);
drop(dedicated_image);
drop(dedicated_buffer);
drop(mapped_buffer);
test.device().wait_idle().expect("wait for device idle");
test.assert_no_validation_errors();
}
}
#[cfg(all(test, feature = "testing"))]
mod unallocated_memory_tests {
use ash::vk;
use renderer_device::testing::TestVulkan;
use super::{Buffer, BufferDesc};
use crate::{
device::MemoryLocation,
images::{Image, ImageDesc},
memory::{Error, MemoryGroup, MemoryMember},
};
fn buffer_desc() -> BufferDesc {
BufferDesc {
size: 256,
usage: vk::BufferUsageFlags::TRANSFER_SRC | vk::BufferUsageFlags::TRANSFER_DST,
mem_location: MemoryLocation::GpuOnly,
..Default::default()
}
}
#[test]
fn unallocated_resources_query_requirements_and_bind() {
let test = TestVulkan::expect();
let device = test.device().clone();
let mut buffer = Buffer::new_unallocated(device.clone(), buffer_desc())
.expect("create unallocated buffer");
let buffer_requirements = buffer.memory_requirements().expect("buffer requirements");
assert!(buffer_requirements.size >= buffer.len());
assert!(matches!(
&buffer.alloc,
crate::device::Allocation::Unmanaged { .. }
));
let mut image = Image::new_unallocated(
device.clone(),
ImageDesc {
format: vk::Format::R8G8B8A8_UNORM,
extent: vk::Extent3D {
width: 16,
height: 16,
depth: 1,
},
usage: vk::ImageUsageFlags::TRANSFER_DST,
..Default::default()
},
)
.expect("create unallocated image");
let image_requirements = image.memory_requirements().expect("image requirements");
assert!(image_requirements.size > 0);
assert!(
image
.allocation()
.is_some_and(|allocation| allocation.allocation().is_none())
);
let buffer_memory = device
.allocate_memory(&buffer_requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate buffer memory");
buffer
.bind_allocation(buffer_memory, 0)
.expect("bind buffer at offset zero");
assert!(matches!(
&buffer.alloc,
crate::device::Allocation::Shared(_)
));
let image_memory = device
.allocate_memory(&image_requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate image memory");
image
.bind_allocation(image_memory, 0)
.expect("bind image at offset zero");
assert!(matches!(
image.allocation(),
Some(crate::device::Allocation::Shared(_))
));
drop(image);
drop(buffer);
test.device().wait_idle().expect("wait for device idle");
test.assert_no_validation_errors();
}
#[test]
fn bind_rejects_invalid_offsets_and_rebinding() {
let test = TestVulkan::expect();
let device = test.device().clone();
let mut buffer = Buffer::new_unallocated(device.clone(), buffer_desc())
.expect("create buffer for offset checks");
let requirements = buffer.memory_requirements().expect("buffer requirements");
let allocation = device
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate offset-check memory");
if requirements.alignment > 1 {
assert!(matches!(
buffer.bind_allocation(allocation.clone(), 1),
Err(Error::MisalignedOffset { .. })
));
}
assert!(matches!(
buffer.bind_allocation(allocation.clone(), requirements.size),
Err(Error::OutOfBounds { .. })
));
buffer
.bind_allocation(allocation.clone(), 0)
.expect("first buffer bind");
assert!(matches!(
buffer.bind_allocation(allocation, 0),
Err(Error::AlreadyBound)
));
let allocated = device
.allocate_memory(&requirements, vk::MemoryPropertyFlags::empty())
.expect("allocate for synthetic type-bits validation");
let impossible = crate::memory::MemoryRequirements {
memory_type_bits: 0,
..requirements
};
assert!(matches!(
crate::memory::validate_binding(&impossible, allocated.as_ref(), 0),
Err(Error::IncompatibleMemoryType { .. })
));
drop(buffer);
test.device().wait_idle().expect("wait for device idle");
test.assert_no_validation_errors();
}
#[test]
fn memory_group_packs_and_binds_two_buffers() {
let test = TestVulkan::expect();
let device = test.device().clone();
let mut first =
Buffer::new_unallocated(device.clone(), buffer_desc()).expect("create first buffer");
let mut second =
Buffer::new_unallocated(device.clone(), buffer_desc()).expect("create second buffer");
let first_requirements = first.memory_requirements().expect("first requirements");
let second_requirements = second.memory_requirements().expect("second requirements");
let common_bits =
first_requirements.memory_type_bits & second_requirements.memory_type_bits;
assert_ne!(common_bits, 0, "the test adapter needs a common type");
let memory_type_index = common_bits.trailing_zeros();
let alignment = first_requirements
.alignment
.max(second_requirements.alignment);
let first_end = first_requirements.size;
let expected_second_offset = first_end
.checked_add(alignment - 1)
.expect("offset arithmetic")
/ alignment
* alignment;
let size = expected_second_offset + second_requirements.size;
let mut group = MemoryGroup::new(
memory_type_index,
vk::MemoryPropertyFlags::empty(),
size,
alignment,
)
.expect("create memory group");
let first_offset = group
.add(MemoryMember::Buffer(&mut first))
.expect("pack first buffer");
let second_offset = group
.add(MemoryMember::Buffer(&mut second))
.expect("pack second buffer");
assert_eq!(first_offset, 0);
assert_eq!(second_offset, expected_second_offset);
assert_eq!(second_offset % alignment, 0);
group.allocate(&device).expect("allocate shared memory");
group.bind_all().expect("bind group members");
assert!(matches!(&first.alloc, crate::device::Allocation::Shared(_)));
assert!(matches!(
&second.alloc,
crate::device::Allocation::Shared(_)
));
assert_eq!(
first
.alloc
.allocation()
.map(crate::device::GpuAllocation::memory),
second
.alloc
.allocation()
.map(crate::device::GpuAllocation::memory),
"both buffers retain the shared Vulkan memory block"
);
drop(first);
drop(second);
test.device().wait_idle().expect("wait for device idle");
test.assert_no_validation_errors();
}
}
/// 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) };
}
}
}