vidya/crates/renderer_resources/src/memory.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

412 lines
14 KiB
Rust

//! Shared aliasable memory allocation helpers for buffers and images.
use std::sync::Arc;
use ash::vk;
use crate::{
Device,
buffers::Buffer,
device::{DeviceObject, GpuAllocation, MemoryBinding},
images::Image,
};
/// Vulkan memory requirements returned by the device layer.
pub use crate::device::MemoryRequirements;
/// Errors from resource requirement queries, validation, and shared binding.
#[derive(Debug)]
pub enum Error {
RequirementQuery(crate::Error),
Bind(crate::Error),
Allocation(crate::Error),
MisalignedOffset {
offset: u64,
alignment: u64,
},
IncompatibleMemoryType {
memory_type_index: u32,
memory_type_bits: u32,
},
OutOfBounds {
offset: u64,
end: u64,
allocation_size: u64,
},
AlreadyBound,
AlreadyAllocated,
RequiresDedicatedAllocation,
GroupMemoryTypeMismatch {
memory_type_index: u32,
},
GroupCapacityExceeded {
requested_end: u64,
capacity: u64,
},
EmptyGroup,
NotAllocated,
InvalidGroupRequirements,
OffsetOverflow,
}
impl core::fmt::Display for Error {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::RequirementQuery(error) => {
write!(f, "failed to query resource memory requirements: {error}")
}
Self::Bind(error) => write!(f, "failed to bind resource memory: {error}"),
Self::Allocation(error) => write!(f, "failed to allocate group memory: {error}"),
Self::MisalignedOffset { offset, alignment } => write!(
f,
"allocation offset {offset} is not aligned to {alignment}"
),
Self::IncompatibleMemoryType {
memory_type_index,
memory_type_bits,
} => write!(
f,
"allocation memory type {memory_type_index} is not allowed by mask {memory_type_bits:#010x}"
),
Self::OutOfBounds {
offset,
end,
allocation_size,
} => write!(
f,
"resource range [{offset}, {end}) does not fit allocation size {allocation_size}"
),
Self::AlreadyBound => f.write_str("resource is already bound to an allocation"),
Self::AlreadyAllocated => f.write_str("memory group has already been allocated"),
Self::RequiresDedicatedAllocation => f.write_str(
"resources requiring dedicated allocations cannot be added to a memory group",
),
Self::GroupMemoryTypeMismatch { memory_type_index } => write!(
f,
"resource memory requirements do not include memory type {memory_type_index}"
),
Self::GroupCapacityExceeded {
requested_end,
capacity,
} => write!(
f,
"memory group capacity exceeded: requested end {requested_end}, capacity {capacity}"
),
Self::EmptyGroup => f.write_str("memory group has no members"),
Self::NotAllocated => f.write_str("memory group has not been allocated"),
Self::InvalidGroupRequirements => {
f.write_str("memory group size or alignment is invalid")
}
Self::OffsetOverflow => f.write_str("memory group offset arithmetic overflowed"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::RequirementQuery(error) | Self::Bind(error) | Self::Allocation(error) => {
Some(error)
}
_ => None,
}
}
}
pub type Result<T> = core::result::Result<T, Error>;
pub(crate) fn validate_binding(
requirements: &MemoryRequirements,
allocation: &DeviceObject<GpuAllocation>,
offset: u64,
) -> Result<()> {
let alignment = requirements.alignment;
if alignment == 0 || !offset.is_multiple_of(alignment) {
return Err(Error::MisalignedOffset { offset, alignment });
}
let memory_type_index = allocation.memory_type_index();
if requirements.memory_type_bits & (1 << memory_type_index) == 0 {
return Err(Error::IncompatibleMemoryType {
memory_type_index,
memory_type_bits: requirements.memory_type_bits,
});
}
let end = offset
.checked_add(requirements.size)
.ok_or(Error::OutOfBounds {
offset,
end: u64::MAX,
allocation_size: allocation.size(),
})?;
if end > allocation.size() {
return Err(Error::OutOfBounds {
offset,
end,
allocation_size: allocation.size(),
});
}
Ok(())
}
enum Member<'a> {
Buffer {
resource: &'a mut Buffer,
offset: u64,
},
Image {
resource: &'a mut Image,
offset: u64,
},
}
/// Packs resources into a single aliasable allocation with a fixed memory type.
///
/// Members are exclusively borrowed until `bind_all` consumes the group. Their
/// allocation ownership is then transferred to each resource; the group retains
/// no allocation handle after binding.
pub struct MemoryGroup<'a> {
memory_type_index: u32,
property_flags: vk::MemoryPropertyFlags,
size: u64,
alignment: u64,
next_offset: u64,
members: Vec<Member<'a>>,
allocation: Option<Arc<DeviceObject<GpuAllocation>>>,
}
impl<'a> MemoryGroup<'a> {
/// Creates an empty group for a fixed memory type and allocation capacity.
pub fn new(
memory_type_index: u32,
property_flags: vk::MemoryPropertyFlags,
size: u64,
alignment: u64,
) -> Result<Self> {
if memory_type_index >= u32::BITS || size == 0 || alignment == 0 {
return Err(Error::InvalidGroupRequirements);
}
Ok(Self {
memory_type_index,
property_flags,
size,
alignment,
next_offset: 0,
members: Vec::new(),
allocation: None,
})
}
/// Adds a buffer, placing it at the next suitably aligned offset.
pub fn add_buffer(&mut self, resource: &'a mut Buffer) -> Result<u64> {
if self.allocation.is_some() {
return Err(Error::AlreadyAllocated);
}
let requirements = resource.memory_requirements()?;
self.add_member_type_check(&requirements)?;
let offset = self.next_member_offset(&requirements)?;
let next_offset = offset
.checked_add(requirements.size)
.ok_or(Error::OffsetOverflow)?;
self.members.push(Member::Buffer { resource, offset });
self.next_offset = next_offset;
Ok(offset)
}
/// Adds either resource kind through a uniform entry point.
pub fn add(&mut self, member: MemoryMember<'a>) -> Result<u64> {
match member {
MemoryMember::Buffer(resource) => self.add_buffer(resource),
MemoryMember::Image(resource) => self.add_image(resource),
}
}
/// Adds an image, placing it at the next suitably aligned offset.
pub fn add_image(&mut self, resource: &'a mut Image) -> Result<u64> {
if self.allocation.is_some() {
return Err(Error::AlreadyAllocated);
}
let requirements = resource.memory_requirements()?;
self.add_member_type_check(&requirements)?;
let offset = self.next_member_offset(&requirements)?;
let next_offset = offset
.checked_add(requirements.size)
.ok_or(Error::OffsetOverflow)?;
self.members.push(Member::Image { resource, offset });
self.next_offset = next_offset;
Ok(offset)
}
/// Allocates the group's shared backing memory.
pub fn allocate(&mut self, device: &Device) -> Result<()> {
if self.members.is_empty() {
return Err(Error::EmptyGroup);
}
if self.allocation.is_some() {
return Err(Error::AlreadyAllocated);
}
let requirements = MemoryRequirements {
size: self.size,
alignment: self.alignment,
memory_type_bits: 1 << self.memory_type_index,
requires_dedicated_allocation: false,
prefers_dedicated_allocation: false,
};
let allocation = device
.allocate_memory(&requirements, self.property_flags)
.map_err(Error::Allocation)?;
for member in &self.members {
let (requirements, offset) = match member {
Member::Buffer { resource, offset } => (resource.memory_requirements()?, *offset),
Member::Image { resource, offset } => (resource.memory_requirements()?, *offset),
};
validate_binding(&requirements, allocation.as_ref(), offset)?;
}
self.allocation = Some(allocation);
Ok(())
}
/// Binds every member and transfers the allocation reference into each resource.
pub fn bind_all(mut self) -> Result<()> {
let allocation = self.allocation.take().ok_or(Error::NotAllocated)?;
// Validate resources first to preserve the same resource-level error mapping as
// individual binding, then prepare the ordered device batch from cached requirements.
let mut bindings = Vec::with_capacity(self.members.len());
for member in &self.members {
match member {
Member::Buffer { resource, offset } => {
resource.validate_unallocated()?;
let requirements = resource.memory_requirements()?;
validate_binding(&requirements, allocation.as_ref(), *offset)?;
resource.validate_device(allocation.device())?;
bindings.push(MemoryBinding::Buffer {
buffer: resource.raw(),
offset: *offset,
requirements,
});
}
Member::Image { resource, offset } => {
resource.validate_unallocated()?;
let requirements = resource.memory_requirements()?;
validate_binding(&requirements, allocation.as_ref(), *offset)?;
resource.validate_device(allocation.device())?;
bindings.push(MemoryBinding::Image {
image: resource.raw(),
offset: *offset,
requirements,
});
}
}
}
if let Err(bind_error) = allocation
.device()
.bind_group_at_offset(&allocation, &bindings)
{
// A Vulkan bind failure leaves its earlier prefix bound; retain its allocation just
// as repeated `bind_allocation` calls would have done.
for (index, member) in self.members.into_iter().enumerate() {
if index >= bind_error.bound_count {
break;
}
match member {
Member::Buffer { resource, .. } => {
resource.set_shared_allocation(allocation.clone());
}
Member::Image { resource, .. } => {
resource.set_shared_allocation(allocation.clone());
}
}
}
return Err(Error::Bind(bind_error.error));
}
for member in self.members {
match member {
Member::Buffer { resource, .. } => {
resource.set_shared_allocation(allocation.clone());
}
Member::Image { resource, .. } => {
resource.set_shared_allocation(allocation.clone());
}
}
}
// `allocation` drops here. Only successfully bound resources retain it.
Ok(())
}
fn add_member_type_check(&self, requirements: &MemoryRequirements) -> Result<()> {
if requirements.requires_dedicated_allocation {
return Err(Error::RequiresDedicatedAllocation);
}
if requirements.memory_type_bits & (1 << self.memory_type_index) == 0 {
return Err(Error::GroupMemoryTypeMismatch {
memory_type_index: self.memory_type_index,
});
}
Ok(())
}
fn next_member_offset(&self, requirements: &MemoryRequirements) -> Result<u64> {
if requirements.alignment == 0 {
return Err(Error::InvalidGroupRequirements);
}
let alignment = checked_lcm(requirements.alignment, self.alignment)?;
let offset = (self
.next_offset
.checked_add(alignment - 1)
.ok_or(Error::OffsetOverflow)?
/ alignment)
.checked_mul(alignment)
.ok_or(Error::OffsetOverflow)?;
let end = offset
.checked_add(requirements.size)
.ok_or(Error::OffsetOverflow)?;
if end > self.size {
return Err(Error::GroupCapacityExceeded {
requested_end: end,
capacity: self.size,
});
}
Ok(offset)
}
}
fn checked_lcm(lhs: u64, rhs: u64) -> Result<u64> {
let gcd = {
let (mut lhs, mut rhs) = (lhs, rhs);
while rhs != 0 {
(lhs, rhs) = (rhs, lhs % rhs);
}
lhs
};
lhs.checked_div(gcd)
.and_then(|lhs| lhs.checked_mul(rhs))
.ok_or(Error::OffsetOverflow)
}
/// A resource accepted by [`MemoryGroup::add`].
pub enum MemoryMember<'a> {
Buffer(&'a mut Buffer),
Image(&'a mut Image),
}