use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::{Rc, Weak};
use ash::{Device, vk};
use crate::suballoc::block_alloc::{BlockAllocator, Placement};
const MAX_BLOCK_BYTES: u64 = 64 * 1024 * 1024;
const FIRST_BLOCK_BYTES: u64 = 4 * 1024 * 1024;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub(super) enum ResourceKind {
Linear,
Optimal,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
struct PoolKey {
memory_type: u32,
kind: ResourceKind,
device_address: bool,
}
struct Block {
memory: vk::DeviceMemory,
mapped: *mut u8,
}
struct Pool {
placement: BlockAllocator,
blocks: Vec<Option<Block>>,
}
impl Pool {
fn new() -> Self {
Self {
placement: BlockAllocator::new(MAX_BLOCK_BYTES),
blocks: Vec::new(),
}
}
fn next_block_bytes(&self, size: u64, align: u64) -> u64 {
let grown = FIRST_BLOCK_BYTES
.saturating_mul(1 << self.placement.block_count().min(4))
.min(MAX_BLOCK_BYTES);
size.saturating_add(align.max(1).saturating_sub(1))
.max(grown)
}
}
enum PooledHandle {
Buffer(vk::Buffer),
Image(vk::Image),
}
struct Retired {
handle: PooledHandle,
views: Vec<vk::ImageView>,
retire_at: u64,
}
struct Inner {
pools: HashMap<PoolKey, Pool>,
retired: Vec<Retired>,
frame: u64,
retire_depth: u64,
}
impl Inner {
fn release(&mut self, lease: &mut Lease) {
let retire = self.frame + self.retire_depth;
if let Some(pool) = self.pools.get_mut(&lease.key) {
pool.placement.free(lease.placement, lease.size, retire);
}
self.retired.push(Retired {
handle: std::mem::replace(&mut lease.handle, PooledHandle::Buffer(vk::Buffer::null())),
views: std::mem::take(&mut *lease.views.borrow_mut()),
retire_at: retire,
});
}
}
struct Lease {
owner: Weak<RefCell<Inner>>,
key: PoolKey,
placement: Placement,
size: u64,
handle: PooledHandle,
views: RefCell<Vec<vk::ImageView>>,
}
impl Drop for Lease {
fn drop(&mut self) {
if let Some(inner) = self.owner.upgrade() {
inner.borrow_mut().release(self);
}
}
}
#[derive(Clone)]
pub(super) struct PooledBuffer {
buffer: vk::Buffer,
mapped: *mut u8,
_lease: Option<Rc<Lease>>,
}
impl PooledBuffer {
pub(super) fn buffer(&self) -> vk::Buffer {
self.buffer
}
pub(super) fn mapped_ptr(&self) -> *mut u8 {
self.mapped
}
pub(super) fn byte_len(&self) -> usize {
self._lease.as_ref().map_or(0, |l| l.size as usize)
}
fn write_dst(&self, offset: usize, len: usize) -> *mut u8 {
assert!(
!self.mapped.is_null(),
"write into a buffer with no host mapping"
);
let cap = self.byte_len();
assert!(
offset.checked_add(len).is_some_and(|end| end <= cap),
"buffer write [{offset}, {}) exceeds mapped length {cap}",
offset.saturating_add(len),
);
unsafe { self.mapped.add(offset) }
}
pub(super) fn write_bytes(&self, offset: usize, bytes: &[u8]) {
if bytes.is_empty() {
return;
}
let dst = self.write_dst(offset, bytes.len());
unsafe {
std::ptr::copy_nonoverlapping(bytes.as_ptr(), dst, bytes.len());
}
}
pub(super) fn write_val<T: Copy>(&self, offset: usize, value: &T) {
let len = size_of::<T>();
if len == 0 {
return;
}
let dst = self.write_dst(offset, len);
unsafe {
std::ptr::copy_nonoverlapping((value as *const T).cast::<u8>(), dst, len);
}
}
pub(super) fn write_slice<T: Copy>(&self, offset: usize, values: &[T]) {
let len = size_of_val(values);
if len == 0 {
return;
}
let dst = self.write_dst(offset, len);
unsafe {
std::ptr::copy_nonoverlapping(values.as_ptr().cast::<u8>(), dst, len);
}
}
pub(super) fn zero_bytes(&self, offset: usize, len: usize) {
if len == 0 {
return;
}
let dst = self.write_dst(offset, len);
unsafe {
std::ptr::write_bytes(dst, 0, len);
}
}
pub(super) fn null() -> Self {
Self {
buffer: vk::Buffer::null(),
mapped: std::ptr::null_mut(),
_lease: None,
}
}
pub(super) fn is_null(&self) -> bool {
self.buffer == vk::Buffer::null()
}
}
#[derive(Clone)]
pub(super) struct PooledImage {
image: vk::Image,
_lease: Option<Rc<Lease>>,
}
impl PooledImage {
pub(super) fn image(&self) -> vk::Image {
self.image
}
pub(super) fn attach_view(&self, view: vk::ImageView) {
debug_assert!(self._lease.is_some(), "attach_view on a null PooledImage");
if let Some(lease) = &self._lease {
lease.views.borrow_mut().push(view);
}
}
pub(super) fn null() -> Self {
Self {
image: vk::Image::null(),
_lease: None,
}
}
#[cfg(test)]
pub(super) fn is_null(&self) -> bool {
self.image == vk::Image::null()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(super) struct AllocatorStats {
pub(super) reserved_bytes: u64,
pub(super) in_use_bytes: u64,
pub(super) block_count: usize,
}
impl std::fmt::Display for AllocatorStats {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} block(s), {} KiB reserved for {} KiB of resources",
self.block_count,
self.reserved_bytes / 1024,
self.in_use_bytes / 1024,
)
}
}
struct Reservation {
memory: vk::DeviceMemory,
mapped: *mut u8,
key: PoolKey,
placement: Placement,
size: u64,
}
#[derive(Clone)]
pub(super) struct DeviceAllocator {
device: Device,
inner: Rc<RefCell<Inner>>,
memory_props: vk::PhysicalDeviceMemoryProperties,
max_allocations: u32,
}
impl DeviceAllocator {
pub(super) fn new(
instance: &ash::Instance,
physical_device: vk::PhysicalDevice,
device: &Device,
frames_in_flight: usize,
) -> Self {
let memory_props =
unsafe { instance.get_physical_device_memory_properties(physical_device) };
let max_allocations = unsafe { instance.get_physical_device_properties(physical_device) }
.limits
.max_memory_allocation_count;
Self {
device: device.clone(),
inner: Rc::new(RefCell::new(Inner {
pools: HashMap::new(),
retired: Vec::new(),
frame: 0,
retire_depth: frames_in_flight as u64 + 1,
})),
memory_props,
max_allocations,
}
}
pub(super) fn create_buffer(
&self,
size: vk::DeviceSize,
usage: vk::BufferUsageFlags,
props: vk::MemoryPropertyFlags,
) -> crate::gfx::error::RenderResult<PooledBuffer> {
let info = vk::BufferCreateInfo::default()
.size(size.max(1))
.usage(usage)
.sharing_mode(vk::SharingMode::EXCLUSIVE);
let buffer = unsafe { self.device.create_buffer(&info, None) }
.map_err(|e| super::error::map_vk_result(e, "create_buffer"))?;
let reqs = unsafe { self.device.get_buffer_memory_requirements(buffer) };
let device_address = usage.contains(vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS);
let reservation = match self.reserve(reqs, props, ResourceKind::Linear, device_address) {
Ok(r) => r,
Err(e) => {
unsafe { self.device.destroy_buffer(buffer, None) };
return Err(e);
}
};
if let Err(e) = unsafe {
self.device
.bind_buffer_memory(buffer, reservation.memory, reservation.placement.offset)
} {
unsafe { self.device.destroy_buffer(buffer, None) };
self.release(reservation);
return Err(super::error::map_vk_result(e, "bind_buffer_memory"));
}
let mapped = resource_ptr(&reservation);
Ok(PooledBuffer {
buffer,
mapped,
_lease: Some(Rc::new(
self.lease(reservation, PooledHandle::Buffer(buffer)),
)),
})
}
pub(super) fn create_image(
&self,
info: &vk::ImageCreateInfo,
props: vk::MemoryPropertyFlags,
) -> crate::gfx::error::RenderResult<PooledImage> {
let image = unsafe { self.device.create_image(info, None) }
.map_err(|e| super::error::map_vk_result(e, "create_image"))?;
let reqs = unsafe { self.device.get_image_memory_requirements(image) };
let kind = if info.tiling == vk::ImageTiling::LINEAR {
ResourceKind::Linear
} else {
ResourceKind::Optimal
};
let reservation = match self.reserve(reqs, props, kind, false) {
Ok(r) => r,
Err(e) => {
unsafe { self.device.destroy_image(image, None) };
return Err(e);
}
};
if let Err(e) = unsafe {
self.device
.bind_image_memory(image, reservation.memory, reservation.placement.offset)
} {
unsafe { self.device.destroy_image(image, None) };
self.release(reservation);
return Err(super::error::map_vk_result(e, "bind_image_memory"));
}
Ok(PooledImage {
image,
_lease: Some(Rc::new(self.lease(reservation, PooledHandle::Image(image)))),
})
}
pub(super) fn begin_frame(&self) {
self.advance(1);
self.release_empty_blocks();
}
pub(super) fn reclaim_idle(&self) {
let depth = self.inner.borrow().retire_depth;
self.advance(depth);
}
fn advance(&self, ticks: u64) {
let mut inner = self.inner.borrow_mut();
inner.frame += ticks;
let frame = inner.frame;
let mut index = 0;
while index < inner.retired.len() {
if inner.retired[index].retire_at <= frame {
let retired = inner.retired.swap_remove(index);
self.destroy_retired(retired);
} else {
index += 1;
}
}
for pool in inner.pools.values_mut() {
pool.placement.reclaim(frame);
}
}
fn release_empty_blocks(&self) {
let mut inner = self.inner.borrow_mut();
for pool in inner.pools.values_mut() {
for block_index in pool.placement.take_empty_blocks() {
if let Some(block) = pool.blocks.get_mut(block_index).and_then(Option::take) {
tracing::debug!("allocator: released empty block {block_index}");
unsafe { self.device.free_memory(block.memory, None) };
}
}
}
}
pub(super) fn stats(&self) -> AllocatorStats {
let inner = self.inner.borrow();
let mut stats = AllocatorStats::default();
for pool in inner.pools.values() {
stats.reserved_bytes += pool.placement.reserved_bytes();
stats.in_use_bytes += pool.placement.in_use_bytes();
stats.block_count += pool.placement.block_count();
}
stats
}
pub(super) fn max_allocations(&self) -> u32 {
self.max_allocations
}
pub(super) fn destroy(&self) {
let mut inner = self.inner.borrow_mut();
for retired in std::mem::take(&mut inner.retired) {
self.destroy_retired(retired);
}
for pool in inner.pools.values_mut() {
for block in pool.blocks.iter_mut().filter_map(Option::take) {
unsafe { self.device.free_memory(block.memory, None) };
}
}
inner.pools.clear();
}
fn destroy_retired(&self, retired: Retired) {
unsafe {
for view in retired.views {
self.device.destroy_image_view(view, None);
}
match retired.handle {
PooledHandle::Buffer(buffer) => self.device.destroy_buffer(buffer, None),
PooledHandle::Image(image) => self.device.destroy_image(image, None),
}
}
}
fn reserve(
&self,
reqs: vk::MemoryRequirements,
props: vk::MemoryPropertyFlags,
kind: ResourceKind,
device_address: bool,
) -> crate::gfx::error::RenderResult<Reservation> {
let memory_type = self.find_memory_type(reqs.memory_type_bits, props)?;
let key = PoolKey {
memory_type,
kind,
device_address,
};
let align = reqs.alignment.max(1);
let mut inner = self.inner.borrow_mut();
let pool = inner.pools.entry(key).or_insert_with(Pool::new);
if let Some(placement) = pool.placement.alloc(reqs.size, align) {
let block = pool.blocks[placement.block]
.as_ref()
.ok_or("allocator: placement named a released block")?;
return Ok(Reservation {
memory: block.memory,
mapped: block.mapped,
key,
placement,
size: reqs.size,
});
}
let block_bytes = pool.next_block_bytes(reqs.size, align);
let (memory, mapped) = Self::create_block(
&self.device,
&self.memory_props,
memory_type,
block_bytes,
device_address,
)?;
let index = pool.placement.add_block(block_bytes);
if index == pool.blocks.len() {
pool.blocks.push(Some(Block { memory, mapped }));
} else {
pool.blocks[index] = Some(Block { memory, mapped });
}
let placement = pool
.placement
.alloc_in(index, reqs.size, align)
.ok_or("allocator: a block sized for a request failed to host it")?;
Ok(Reservation {
memory,
mapped,
key,
placement,
size: reqs.size,
})
}
fn release(&self, reservation: Reservation) {
let mut inner = self.inner.borrow_mut();
let retire = inner.frame + inner.retire_depth;
if let Some(pool) = inner.pools.get_mut(&reservation.key) {
pool.placement
.free(reservation.placement, reservation.size, retire);
}
}
fn lease(&self, reservation: Reservation, handle: PooledHandle) -> Lease {
Lease {
owner: Rc::downgrade(&self.inner),
key: reservation.key,
placement: reservation.placement,
size: reservation.size,
handle,
views: RefCell::new(Vec::new()),
}
}
fn find_memory_type(
&self,
type_filter: u32,
props: vk::MemoryPropertyFlags,
) -> Result<u32, String> {
for i in 0..self.memory_props.memory_type_count {
if (type_filter & (1 << i)) != 0
&& self.memory_props.memory_types[i as usize]
.property_flags
.contains(props)
{
return Ok(i);
}
}
Err("no suitable memory type found".to_string())
}
fn create_block(
device: &Device,
memory_props: &vk::PhysicalDeviceMemoryProperties,
memory_type: u32,
size: u64,
device_address: bool,
) -> crate::gfx::error::RenderResult<(vk::DeviceMemory, *mut u8)> {
let mut flags_info =
vk::MemoryAllocateFlagsInfo::default().flags(vk::MemoryAllocateFlags::DEVICE_ADDRESS);
let mut info = vk::MemoryAllocateInfo::default()
.allocation_size(size)
.memory_type_index(memory_type);
if device_address {
info = info.push_next(&mut flags_info);
}
let memory = unsafe { device.allocate_memory(&info, None) }.map_err(|e| {
super::error::map_vk_result(e, &format!("allocator: block of {size} bytes"))
})?;
tracing::debug!(
"allocator: new {} KiB block (memory type {memory_type}, device_address {device_address})",
size / 1024,
);
let host_visible = memory_props.memory_types[memory_type as usize]
.property_flags
.contains(vk::MemoryPropertyFlags::HOST_VISIBLE);
let mapped = if host_visible {
match unsafe {
device.map_memory(memory, 0, vk::WHOLE_SIZE, vk::MemoryMapFlags::empty())
} {
Ok(ptr) => ptr as *mut u8,
Err(e) => {
unsafe { device.free_memory(memory, None) };
return Err(super::error::map_vk_result(e, "allocator: map block"));
}
}
} else {
std::ptr::null_mut()
};
Ok((memory, mapped))
}
}
fn resource_ptr(reservation: &Reservation) -> *mut u8 {
if reservation.mapped.is_null() {
std::ptr::null_mut()
} else {
unsafe {
reservation
.mapped
.add(reservation.placement.offset as usize)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocks_double_from_first_to_max() {
let mut pool = Pool::new();
let mut expected = FIRST_BLOCK_BYTES;
for _ in 0..6 {
let bytes = pool.next_block_bytes(1024, 256);
assert_eq!(bytes, expected);
pool.placement.add_block(bytes);
expected = (expected * 2).min(MAX_BLOCK_BYTES);
}
assert_eq!(pool.next_block_bytes(1024, 256), MAX_BLOCK_BYTES);
}
#[test]
fn an_oversized_request_sizes_its_own_block() {
let pool = Pool::new();
let size = MAX_BLOCK_BYTES * 2;
assert_eq!(pool.next_block_bytes(size, 1), size);
assert_eq!(pool.next_block_bytes(size, 4096), size + 4095);
}
#[test]
fn null_resources_are_inert() {
let buffer = PooledBuffer::null();
assert!(buffer.is_null());
assert!(buffer.mapped_ptr().is_null());
let image = PooledImage::null();
assert!(image.is_null());
drop(buffer);
drop(image);
}
struct TestGpu {
device: Device,
instance: ash::Instance,
physical_device: vk::PhysicalDevice,
_entry: ash::Entry,
}
impl Drop for TestGpu {
fn drop(&mut self) {
unsafe {
self.device.destroy_device(None);
self.instance.destroy_instance(None);
}
}
}
fn test_gpu() -> Option<TestGpu> {
let gpu = test_gpu_impl(false);
if gpu.is_none() {
eprintln!("skipped: no Vulkan driver");
}
gpu
}
fn test_gpu_with_device_address() -> Option<TestGpu> {
let gpu = test_gpu_impl(true);
if gpu.is_none() {
eprintln!("skipped: no bufferDeviceAddress-capable Vulkan driver");
}
gpu
}
fn test_gpu_impl(device_address: bool) -> Option<TestGpu> {
let entry = crate::vulkan::loader::load_entry().ok()?;
let app = vk::ApplicationInfo::default().api_version(if device_address {
vk::API_VERSION_1_2
} else {
vk::API_VERSION_1_0
});
let instance = unsafe {
entry.create_instance(
&vk::InstanceCreateInfo::default().application_info(&app),
None,
)
}
.ok()?;
let destroy_instance = |instance: ash::Instance| {
unsafe { instance.destroy_instance(None) };
None
};
let physical_device = match unsafe { instance.enumerate_physical_devices() } {
Ok(devices) if !devices.is_empty() => devices[0],
_ => return destroy_instance(instance),
};
let mut enable = vk::PhysicalDeviceBufferDeviceAddressFeatures::default();
if device_address {
let props = unsafe { instance.get_physical_device_properties(physical_device) };
if props.api_version < vk::API_VERSION_1_2 {
return destroy_instance(instance);
}
let mut bda = vk::PhysicalDeviceBufferDeviceAddressFeatures::default();
let mut feats = vk::PhysicalDeviceFeatures2::default().push_next(&mut bda);
unsafe { instance.get_physical_device_features2(physical_device, &mut feats) };
if bda.buffer_device_address == 0 {
return destroy_instance(instance);
}
enable = enable.buffer_device_address(true);
}
let queue_infos = [vk::DeviceQueueCreateInfo::default()
.queue_family_index(0)
.queue_priorities(&[1.0])];
let mut device_info = vk::DeviceCreateInfo::default().queue_create_infos(&queue_infos);
if device_address {
device_info = device_info.push_next(&mut enable);
}
let device = match unsafe { instance.create_device(physical_device, &device_info, None) } {
Ok(device) => device,
Err(_) => return destroy_instance(instance),
};
Some(TestGpu {
device,
instance,
physical_device,
_entry: entry,
})
}
fn test_allocator(gpu: &TestGpu) -> DeviceAllocator {
DeviceAllocator::new(&gpu.instance, gpu.physical_device, &gpu.device, 2)
}
const HOST: vk::MemoryPropertyFlags = vk::MemoryPropertyFlags::from_raw(
vk::MemoryPropertyFlags::HOST_VISIBLE.as_raw()
| vk::MemoryPropertyFlags::HOST_COHERENT.as_raw(),
);
#[test]
fn small_buffers_share_one_block_and_map_at_their_offsets() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let a = alloc
.create_buffer(1024, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
let b = alloc
.create_buffer(1024, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
assert_ne!(a.buffer(), b.buffer());
assert_eq!(alloc.stats().block_count, 1);
assert!(!a.mapped_ptr().is_null());
assert!(!b.mapped_ptr().is_null());
unsafe {
std::ptr::write_bytes(a.mapped_ptr(), 0xAA, 1024);
std::ptr::write_bytes(b.mapped_ptr(), 0xBB, 1024);
assert_eq!(*a.mapped_ptr(), 0xAA);
assert_eq!(*a.mapped_ptr().add(1023), 0xAA);
assert_eq!(*b.mapped_ptr(), 0xBB);
}
drop(a);
drop(b);
alloc.destroy();
}
#[test]
fn safe_writes_land_at_their_offsets() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let buffer = alloc
.create_buffer(1024, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
assert!(buffer.byte_len() >= 1024);
buffer.write_bytes(0, &[0x11, 0x22]);
buffer.write_val(4, &0x3344_5566_u32);
buffer.write_slice(8, &[0x77_u8, 0x88]);
buffer.write_bytes(2, &[]);
buffer.write_bytes(10, &[0xEE, 0xEE]);
buffer.zero_bytes(10, 2);
buffer.zero_bytes(12, 0);
unsafe {
let p = buffer.mapped_ptr();
assert_eq!(*p, 0x11);
assert_eq!(*p.add(1), 0x22);
assert_eq!(
std::ptr::read_unaligned(p.add(4).cast::<u32>()),
0x3344_5566
);
assert_eq!(*p.add(8), 0x77);
assert_eq!(*p.add(9), 0x88);
assert_eq!(*p.add(10), 0);
assert_eq!(*p.add(11), 0);
}
drop(buffer);
alloc.destroy();
}
#[test]
fn an_out_of_bounds_write_panics() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let buffer = alloc
.create_buffer(64, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
let len = buffer.byte_len();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
buffer.write_bytes(len, &[0xFF]);
}));
assert!(result.is_err());
drop(buffer);
alloc.destroy();
}
#[test]
fn writes_to_a_null_buffer_panic() {
let buffer = PooledBuffer::null();
assert_eq!(buffer.byte_len(), 0);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
buffer.write_val(0, &1_u32);
}));
assert!(result.is_err());
}
#[test]
fn a_dropped_buffer_frees_its_range_after_the_retire_window() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let buffer = alloc
.create_buffer(1024, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
let held = alloc.stats();
assert!(held.in_use_bytes >= 1024);
assert_eq!(held.block_count, 1);
drop(buffer);
assert_eq!(alloc.stats().in_use_bytes, 0);
assert_eq!(alloc.stats().block_count, 1);
alloc.begin_frame();
assert_eq!(alloc.stats().block_count, 1);
alloc.begin_frame();
alloc.begin_frame();
assert_eq!(alloc.stats().block_count, 0);
assert_eq!(alloc.stats().reserved_bytes, 0);
alloc.destroy();
}
#[test]
fn a_clone_keeps_the_resource_alive() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let a = alloc
.create_buffer(1024, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
let b = a.clone();
drop(a);
assert!(alloc.stats().in_use_bytes >= 1024);
drop(b);
assert_eq!(alloc.stats().in_use_bytes, 0);
alloc.destroy();
}
#[test]
fn an_image_and_its_views_retire_together() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let info = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.extent(vk::Extent3D {
width: 4,
height: 4,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.format(vk::Format::R8G8B8A8_UNORM)
.tiling(vk::ImageTiling::OPTIMAL)
.initial_layout(vk::ImageLayout::UNDEFINED)
.usage(vk::ImageUsageFlags::SAMPLED)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.samples(vk::SampleCountFlags::TYPE_1);
let image = alloc
.create_image(&info, vk::MemoryPropertyFlags::DEVICE_LOCAL)
.unwrap();
let view_info = vk::ImageViewCreateInfo::default()
.image(image.image())
.view_type(vk::ImageViewType::TYPE_2D)
.format(vk::Format::R8G8B8A8_UNORM)
.subresource_range(vk::ImageSubresourceRange {
aspect_mask: vk::ImageAspectFlags::COLOR,
base_mip_level: 0,
level_count: 1,
base_array_layer: 0,
layer_count: 1,
});
let view = unsafe { gpu.device.create_image_view(&view_info, None) }.unwrap();
image.attach_view(view);
drop(image);
for _ in 0..3 {
alloc.begin_frame();
}
assert_eq!(alloc.stats().block_count, 0);
alloc.destroy();
}
#[test]
fn linear_and_optimal_images_never_share_a_block() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let base = vk::ImageCreateInfo::default()
.image_type(vk::ImageType::TYPE_2D)
.extent(vk::Extent3D {
width: 4,
height: 4,
depth: 1,
})
.mip_levels(1)
.array_layers(1)
.format(vk::Format::R8G8B8A8_UNORM)
.initial_layout(vk::ImageLayout::UNDEFINED)
.sharing_mode(vk::SharingMode::EXCLUSIVE)
.samples(vk::SampleCountFlags::TYPE_1);
let optimal = alloc
.create_image(
&base
.tiling(vk::ImageTiling::OPTIMAL)
.usage(vk::ImageUsageFlags::SAMPLED),
vk::MemoryPropertyFlags::DEVICE_LOCAL,
)
.unwrap();
let linear = alloc
.create_image(
&base
.tiling(vk::ImageTiling::LINEAR)
.usage(vk::ImageUsageFlags::TRANSFER_DST),
HOST,
)
.unwrap();
assert_eq!(alloc.stats().block_count, 2);
drop(optimal);
drop(linear);
alloc.destroy();
}
#[test]
fn an_oversized_buffer_gets_a_dedicated_block() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let big = alloc
.create_buffer(
MAX_BLOCK_BYTES + 1024,
vk::BufferUsageFlags::TRANSFER_SRC,
HOST,
)
.unwrap();
assert_eq!(alloc.stats().block_count, 1);
let small = alloc
.create_buffer(1024, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
assert_eq!(alloc.stats().block_count, 2);
drop(big);
drop(small);
alloc.destroy();
}
#[test]
fn a_reclaimed_range_is_reused_by_a_later_allocation() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let anchor = alloc
.create_buffer(1024, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
let big_size = 3 * 1024 * 1024;
let big = alloc
.create_buffer(big_size, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
assert_eq!(alloc.stats().block_count, 1);
drop(big);
for _ in 0..3 {
alloc.begin_frame();
}
let again = alloc
.create_buffer(big_size, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
assert_eq!(alloc.stats().block_count, 1);
drop(anchor);
drop(again);
alloc.destroy();
}
#[test]
fn reclaim_idle_retires_pending_frees_without_frame_ticks() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let anchor = alloc
.create_buffer(1024, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
let big_size = 3 * 1024 * 1024;
let big = alloc
.create_buffer(big_size, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
drop(big);
alloc.reclaim_idle();
let again = alloc
.create_buffer(big_size, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
assert_eq!(alloc.stats().block_count, 1);
drop(anchor);
drop(again);
alloc.destroy();
}
#[test]
fn a_released_reservation_returns_its_range() {
let Some(gpu) = test_gpu() else {
return;
};
let alloc = test_allocator(&gpu);
let reqs = vk::MemoryRequirements {
size: 1024,
alignment: 256,
memory_type_bits: !0,
};
let reservation = alloc
.reserve(reqs, HOST, ResourceKind::Linear, false)
.unwrap();
assert!(alloc.stats().in_use_bytes >= 1024);
alloc.release(reservation);
assert_eq!(alloc.stats().in_use_bytes, 0);
for _ in 0..3 {
alloc.begin_frame();
}
assert_eq!(alloc.stats().block_count, 0);
alloc.destroy();
}
#[test]
fn an_emptied_pool_restarts_the_growth_ladder() {
let mut pool = Pool::new();
let b1 = pool.next_block_bytes(1024, 1);
assert_eq!(b1, FIRST_BLOCK_BYTES);
let i1 = pool.placement.add_block(b1);
let p1 = pool.placement.alloc_in(i1, 1024, 1).unwrap();
let b2 = pool.next_block_bytes(1024, 1);
assert_eq!(b2, FIRST_BLOCK_BYTES * 2);
let i2 = pool.placement.add_block(b2);
let p2 = pool.placement.alloc_in(i2, 1024, 1).unwrap();
pool.placement.free(p1, 1024, 0);
pool.placement.free(p2, 1024, 0);
pool.placement.reclaim(1);
assert_eq!(pool.placement.take_empty_blocks().len(), 2);
assert_eq!(pool.next_block_bytes(1024, 1), FIRST_BLOCK_BYTES);
}
#[test]
fn device_address_buffers_pool_apart_and_report_addresses() {
let Some(gpu) = test_gpu_with_device_address() else {
return;
};
let alloc = test_allocator(&gpu);
let plain = alloc
.create_buffer(1024, vk::BufferUsageFlags::TRANSFER_SRC, HOST)
.unwrap();
let a = alloc
.create_buffer(1024, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, HOST)
.unwrap();
let b = alloc
.create_buffer(1024, vk::BufferUsageFlags::SHADER_DEVICE_ADDRESS, HOST)
.unwrap();
assert_eq!(alloc.stats().block_count, 2);
let address = |buffer: vk::Buffer| unsafe {
gpu.device
.get_buffer_device_address(&vk::BufferDeviceAddressInfo::default().buffer(buffer))
};
let addr_a = address(a.buffer());
let addr_b = address(b.buffer());
assert_ne!(addr_a, 0);
assert_ne!(addr_b, 0);
assert_ne!(addr_a, addr_b);
drop(plain);
drop(a);
drop(b);
alloc.destroy();
}
}