Theme

alex/beta-minecraft-rust

Minecraft Beta 1.7.3 Rewrite

BRANCH main
main
SNAPSHOT Draw terrain GPU-driven from packed per-section buffers with GPU occlusion culling, add a render distance slider (2-128) with a quad/disk shape, cut chunk bookkeeping at high render distances, and fix a light underflow at y=0
DOWNLOAD
Download file

root / crates / volcanic-mem / src / allocator.rs

use crate::device::DeviceFns;
use crate::error::{Error, Result, check};
use crate::tlsf::Tlsf;
use core::ffi::c_void;
use core::ptr::{NonNull, null, null_mut};
use std::sync::{Mutex, MutexGuard, PoisonError};
use volcanic::loader::{CmdsDevice, CmdsInstance};
use volcanic::*;

const DEDICATED: u32 = u32::MAX;
const SMALL_HEAP: u64 = 1 << 30;
const BLOCK_ALIGNMENT: u64 = 1 << 12;
const NEW_BLOCK_SHIFTS: u32 = 3;
const UNUSABLE: VkMemoryPropertyFlags = VkMemoryPropertyFlags::from_bits_retain(
    VkMemoryPropertyFlags::LAZILY_ALLOCATED.bits() | (1 << 5) | (1 << 6) | (1 << 7),
);

#[inline]
const fn align_up(value: u64, align: u64) -> u64 {
    (value + align - 1) & !(align - 1)
}

// ---- Public types -------------------------------------------

/// Where an allocation should live, from the point of view of who accesses it.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum MemoryLocation {
    /// Device-local memory, not host visible when avoidable.
    #[default]
    GpuOnly,
    /// Host-visible memory written sequentially by the CPU. Resources the GPU reads directly
    /// go to device-local host-visible memory when available, while transfer-only (staging)
    /// buffers go to system memory.
    CpuToGpu,
    /// Host-visible, preferably cached memory for reading GPU results back on the CPU.
    GpuToCpu,
}

/// Distinguishes resources for `bufferImageGranularity`: buffers and linear images are
/// `Linear`, optimal-tiling images are `Optimal`.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ResourceKind {
    #[default]
    Linear,
    Optimal,
}

#[derive(Clone, Copy)]
pub struct AllocationDesc {
    pub requirements: VkMemoryRequirements,
    pub location: MemoryLocation,
    pub kind: ResourceKind,
    /// Gives the allocation its own `VkDeviceMemory`.
    pub dedicated: bool,
}

#[derive(Clone, Copy, Debug)]
pub struct AllocatorConfig {
    /// Block size for heaps larger than 1 GiB. Smaller heaps use 1/8 of their size.
    pub preferred_block_size: u64,
    /// Set when `VK_EXT_memory_budget` is enabled, so [`Allocator::budgets`] reports driver numbers.
    pub memory_budget: bool,
    /// Set when `VK_EXT_descriptor_heap` is enabled. Without it, the heap limits are not queried
    /// and [`Allocator::create_descriptor_heap`] fails.
    pub descriptor_heap: bool,
}

impl Default for AllocatorConfig {
    fn default() -> Self {
        Self {
            preferred_block_size: 256 << 20,
            memory_budget: false,
            descriptor_heap: true,
        }
    }
}

/// A sub-range of a `VkDeviceMemory`. Must be returned with [`Allocator::free`].
#[derive(Debug)]
pub struct Allocation {
    memory: VkDeviceMemory,
    offset: u64,
    size: u64,
    mapped: *mut u8,
    memory_type: u32,
    block: u32,
    node: u32,
}

unsafe impl Send for Allocation {}
unsafe impl Sync for Allocation {}

impl Allocation {
    #[inline]
    pub fn memory(&self) -> VkDeviceMemory {
        self.memory
    }

    #[inline]
    pub fn offset(&self) -> u64 {
        self.offset
    }

    #[inline]
    pub fn size(&self) -> u64 {
        self.size
    }

    #[inline]
    pub fn memory_type(&self) -> u32 {
        self.memory_type
    }

    #[inline]
    pub fn is_dedicated(&self) -> bool {
        self.block == DEDICATED
    }

    /// Host pointer to the start of the allocation, if its memory is host visible.
    #[inline]
    pub fn mapped_ptr(&self) -> Option<NonNull<u8>> {
        NonNull::new(self.mapped)
    }

    #[inline]
    pub fn mapped_slice(&self) -> Option<&[u8]> {
        self.mapped_ptr()
            .map(|ptr| unsafe { core::slice::from_raw_parts(ptr.as_ptr(), self.size as usize) })
    }

    #[inline]
    pub fn mapped_slice_mut(&mut self) -> Option<&mut [u8]> {
        self.mapped_ptr()
            .map(|ptr| unsafe { core::slice::from_raw_parts_mut(ptr.as_ptr(), self.size as usize) })
    }
}

/// Usage counters of one memory heap.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Statistics {
    /// `VkDeviceMemory` objects, including dedicated allocations.
    pub block_count: u32,
    pub block_bytes: u64,
    pub allocation_count: u32,
    pub allocation_bytes: u64,
}

impl Statistics {
    fn add(&mut self, other: &Self) {
        self.block_count += other.block_count;
        self.block_bytes += other.block_bytes;
        self.allocation_count += other.allocation_count;
        self.allocation_bytes += other.allocation_bytes;
    }
}

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct HeapBudget {
    pub statistics: Statistics,
    /// Bytes the whole process uses in this heap; estimated from `block_bytes` without
    /// `VK_EXT_memory_budget`.
    pub usage: u64,
    /// Bytes the process can use before running into trouble; estimated as 80% of the heap
    /// size without `VK_EXT_memory_budget`.
    pub budget: u64,
}

// ---- Internals ----------------------------------------------

#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Dedicated {
    No,
    Prefer,
    Require,
}

#[derive(Clone, Copy)]
pub(crate) enum Target {
    None,
    Buffer(VkBuffer),
    Image(VkImage),
}

pub(crate) struct Request {
    pub size: u64,
    pub alignment: u64,
    pub type_bits: u32,
    pub location: MemoryLocation,
    pub kind: ResourceKind,
    pub device_access: bool,
    pub dedicated: Dedicated,
    pub target: Target,
}

struct Block {
    memory: VkDeviceMemory,
    mapped: *mut u8,
    tlsf: Tlsf,
}

struct Pool {
    blocks: Vec<Option<Block>>,
    /// Live block slots, kept roughly sorted by ascending free space.
    order: Vec<u32>,
    vacant: Vec<u32>,
    statistics: Statistics,
}

unsafe impl Send for Pool {}

impl Pool {
    /// One bubble step towards ascending free space, keeping allocations packed into the fullest blocks.
    #[inline]
    fn sort_step(&mut self) {
        for i in 1..self.order.len() {
            let free = |slot: u32| {
                self.blocks[slot as usize]
                    .as_ref()
                    .map_or(0, |b| b.tlsf.free_bytes())
            };
            if free(self.order[i - 1]) > free(self.order[i]) {
                self.order.swap(i - 1, i);
                return;
            }
        }
    }
}

#[derive(Clone, Copy)]
struct MemoryType {
    flags: VkMemoryPropertyFlags,
    heap: u32,
}

/// Thread-safe TLSF-based Vulkan memory allocator.
pub struct Allocator {
    device: VkDevice,
    fns: DeviceFns,
    physical_device: VkPhysicalDevice,
    get_memory_properties2: Option<PFN_vkGetPhysicalDeviceMemoryProperties2>,
    types: Box<[MemoryType]>,
    heaps: Box<[VkMemoryHeap]>,
    pools: Box<[Mutex<Pool>]>,
    block_sizes: Box<[u64]>,
    /// Memory types ordered by preference, indexed by `location * 2 + device_access`.
    preferences: [Box<[u8]>; 6],
    granularity: u64,
    atom: u64,
    pub(crate) heap_limits: crate::resource::HeapLimits,
}

unsafe impl Send for Allocator {}
unsafe impl Sync for Allocator {}

impl Allocator {
    /// # Safety
    /// The command tables must belong to `physical_device` and `device`. The device must be a
    /// Vulkan 1.3 device with the `bufferDeviceAddress` and `maintenance5` features enabled, plus
    /// `VK_EXT_descriptor_heap` and `descriptorHeap` if `config.descriptor_heap` is set, and it
    /// must outlive the allocator.
    pub unsafe fn new(
        instance: &CmdsInstance,
        physical_device: VkPhysicalDevice,
        device_cmds: &CmdsDevice,
        device: VkDevice,
        config: &AllocatorConfig,
    ) -> Result<Self> {
        let fns = DeviceFns::load(device_cmds)?;
        let get_properties2 = instance
            .vkGetPhysicalDeviceProperties2
            .ok_or(Error::MissingCommand("vkGetPhysicalDeviceProperties2"))?;
        let get_memory_properties2 =
            instance
                .vkGetPhysicalDeviceMemoryProperties2
                .ok_or(Error::MissingCommand(
                    "vkGetPhysicalDeviceMemoryProperties2",
                ))?;

        let mut heap_properties: VkPhysicalDeviceDescriptorHeapPropertiesEXT =
            unsafe { core::mem::zeroed() };
        heap_properties.sType = VkStructureType::PhysicalDeviceDescriptorHeapPropertiesExt;
        let mut properties: VkPhysicalDeviceProperties2 = unsafe { core::mem::zeroed() };
        properties.sType = VkStructureType::PhysicalDeviceProperties2;
        if config.descriptor_heap {
            properties.pNext = (&raw mut heap_properties).cast();
        }
        unsafe { get_properties2(physical_device, &mut properties) };
        let limits = &properties.properties.limits;

        let mut memory: VkPhysicalDeviceMemoryProperties2 = unsafe { core::mem::zeroed() };
        memory.sType = VkStructureType::PhysicalDeviceMemoryProperties2;
        unsafe { get_memory_properties2(physical_device, &mut memory) };
        let memory = &memory.memoryProperties;

        let heaps: Box<[_]> = memory.memoryHeaps[..memory.memoryHeapCount as usize].into();
        let types: Box<[_]> = memory.memoryTypes[..memory.memoryTypeCount as usize]
            .iter()
            .map(|t| MemoryType {
                flags: t.propertyFlags,
                heap: t.heapIndex,
            })
            .collect();
        let block_sizes = types
            .iter()
            .map(|t| {
                let heap_size = heaps[t.heap as usize].size;
                let size = if heap_size <= SMALL_HEAP {
                    heap_size / 8
                } else {
                    config.preferred_block_size
                };
                align_up(size.max(BLOCK_ALIGNMENT), BLOCK_ALIGNMENT)
            })
            .collect();
        let pools = types
            .iter()
            .map(|_| {
                Mutex::new(Pool {
                    blocks: Vec::new(),
                    order: Vec::new(),
                    vacant: Vec::new(),
                    statistics: Statistics::default(),
                })
            })
            .collect();
        let preferences = core::array::from_fn(|i| {
            let location = [
                MemoryLocation::GpuOnly,
                MemoryLocation::CpuToGpu,
                MemoryLocation::GpuToCpu,
            ][i / 2];
            preference_order(&types, location, i % 2 == 1)
        });

        Ok(Self {
            device,
            fns,
            physical_device,
            get_memory_properties2: config.memory_budget.then_some(get_memory_properties2),
            types,
            heaps,
            pools,
            block_sizes,
            preferences,
            granularity: limits.bufferImageGranularity.max(1),
            atom: limits.nonCoherentAtomSize.max(1),
            heap_limits: crate::resource::HeapLimits::new(&heap_properties),
        })
    }

    #[inline]
    pub fn device(&self) -> VkDevice {
        self.device
    }

    #[inline]
    pub(crate) fn fns(&self) -> &DeviceFns {
        &self.fns
    }

    /// Allocates memory for a resource the caller creates and binds itself.
    pub fn allocate(&self, desc: &AllocationDesc) -> Result<Allocation> {
        self.allocate_request(&Request {
            size: desc.requirements.size,
            alignment: desc.requirements.alignment,
            type_bits: desc.requirements.memoryTypeBits,
            location: desc.location,
            kind: desc.kind,
            device_access: true,
            dedicated: if desc.dedicated {
                Dedicated::Require
            } else {
                Dedicated::No
            },
            target: Target::None,
        })
    }

    pub fn free(&self, allocation: Allocation) {
        let size = allocation.size;
        if allocation.block == DEDICATED {
            unsafe { (self.fns.free_memory)(self.device, allocation.memory, null()) };
            let stats = &mut self.lock(allocation.memory_type).statistics;
            stats.block_count -= 1;
            stats.block_bytes -= size;
            stats.allocation_count -= 1;
            stats.allocation_bytes -= size;
            return;
        }
        let released = {
            let mut guard = self.lock(allocation.memory_type);
            let pool = &mut *guard;
            let slot = allocation.block as usize;
            let tlsf = &mut pool.blocks[slot]
                .as_mut()
                .expect("allocation from a freed block")
                .tlsf;
            tlsf.free(allocation.node);
            pool.statistics.allocation_count -= 1;
            pool.statistics.allocation_bytes -= size;

            let empty = tlsf.is_empty();
            let keep = !empty
                || pool
                    .order
                    .iter()
                    .filter(|&&s| {
                        pool.blocks[s as usize]
                            .as_ref()
                            .is_some_and(|b| b.tlsf.is_empty())
                    })
                    .count()
                    == 1;
            if keep {
                pool.sort_step();
                None
            } else {
                let block = pool.blocks[slot].take().expect("live block");
                pool.order.retain(|&s| s as usize != slot);
                pool.vacant.push(slot as u32);
                pool.statistics.block_count -= 1;
                pool.statistics.block_bytes -= block.tlsf.size();
                Some(block.memory)
            }
        };
        if let Some(memory) = released {
            unsafe { (self.fns.free_memory)(self.device, memory, null()) };
        }
    }

    /// Makes host writes in `offset..offset + size` of the allocation visible to the device.
    /// Pass `VK_WHOLE_SIZE` to cover the rest of the allocation. No-op on coherent memory.
    pub fn flush(&self, allocation: &Allocation, offset: u64, size: u64) -> Result<()> {
        match self.mapped_range(allocation, offset, size) {
            Some(range) => check("vkFlushMappedMemoryRanges", unsafe {
                (self.fns.flush_mapped_memory_ranges)(self.device, 1, &range)
            }),
            None => Ok(()),
        }
    }

    /// Makes device writes in `offset..offset + size` of the allocation visible to the host.
    /// Pass `VK_WHOLE_SIZE` to cover the rest of the allocation. No-op on coherent memory.
    pub fn invalidate(&self, allocation: &Allocation, offset: u64, size: u64) -> Result<()> {
        match self.mapped_range(allocation, offset, size) {
            Some(range) => check("vkInvalidateMappedMemoryRanges", unsafe {
                (self.fns.invalidate_mapped_memory_ranges)(self.device, 1, &range)
            }),
            None => Ok(()),
        }
    }

    /// Per-heap usage, indexed like `VkPhysicalDeviceMemoryProperties::memoryHeaps`.
    pub fn budgets(&self) -> Vec<HeapBudget> {
        let mut budgets: Vec<HeapBudget> = self
            .heaps
            .iter()
            .map(|heap| HeapBudget {
                budget: heap.size / 10 * 8,
                ..Default::default()
            })
            .collect();
        for (pool, ty) in self.pools.iter().zip(&self.types) {
            let stats = pool
                .lock()
                .unwrap_or_else(PoisonError::into_inner)
                .statistics;
            budgets[ty.heap as usize].statistics.add(&stats);
        }
        for budget in &mut budgets {
            budget.usage = budget.statistics.block_bytes;
        }
        if let Some(driver) = self.driver_budget() {
            for (i, budget) in budgets.iter_mut().enumerate() {
                budget.usage = driver.heapUsage[i];
                budget.budget = driver.heapBudget[i];
            }
        }
        budgets
    }

    /// Statistics summed over all heaps.
    pub fn statistics(&self) -> Statistics {
        let mut total = Statistics::default();
        for pool in &self.pools {
            total.add(
                &pool
                    .lock()
                    .unwrap_or_else(PoisonError::into_inner)
                    .statistics,
            );
        }
        total
    }

    // ---- Allocation paths ---------------------------------------

    pub(crate) fn allocate_request(&self, request: &Request) -> Result<Allocation> {
        if request.size == 0 {
            return Err(Error::ZeroSize);
        }
        if !request.alignment.is_power_of_two() {
            return Err(Error::InvalidAlignment(request.alignment));
        }
        let order =
            &self.preferences[request.location as usize * 2 + request.device_access as usize];
        let mut candidates = order
            .iter()
            .map(|&ty| ty as u32)
            .filter(|ty| request.type_bits & (1 << ty) != 0)
            .peekable();
        if candidates.peek().is_none() {
            return Err(Error::NoMemoryType);
        }
        while let Some(ty) = candidates.next() {
            // Over-budget memory types are skipped while a fallback remains.
            let within_budget = candidates.peek().is_some();
            match self.allocate_from_type(ty, request, within_budget) {
                Err(Error::OutOfMemory) => continue,
                result => return result,
            }
        }
        Err(Error::OutOfMemory)
    }

    fn allocate_from_type(
        &self,
        ty: u32,
        request: &Request,
        within_budget: bool,
    ) -> Result<Allocation> {
        let block_size = self.block_sizes[ty as usize];
        let dedicated = match request.dedicated {
            Dedicated::Require => return self.allocate_dedicated(ty, request, within_budget),
            Dedicated::Prefer => true,
            Dedicated::No => request.size > block_size / 2,
        };
        if dedicated {
            match self.allocate_dedicated(ty, request, within_budget) {
                Err(Error::OutOfMemory) if request.size <= block_size => {}
                result => return result,
            }
        }
        self.allocate_in_block(ty, request, within_budget)
    }

    fn allocate_dedicated(
        &self,
        ty: u32,
        request: &Request,
        within_budget: bool,
    ) -> Result<Allocation> {
        if within_budget && !self.fits_budget(ty, request.size) {
            return Err(Error::OutOfMemory);
        }
        let (memory, mapped) = self.allocate_memory(ty, request.size, request.target)?;
        let stats = &mut self.lock(ty).statistics;
        stats.block_count += 1;
        stats.block_bytes += request.size;
        stats.allocation_count += 1;
        stats.allocation_bytes += request.size;
        Ok(Allocation {
            memory,
            offset: 0,
            size: request.size,
            mapped,
            memory_type: ty,
            block: DEDICATED,
            node: 0,
        })
    }

    fn allocate_in_block(
        &self,
        ty: u32,
        request: &Request,
        within_budget: bool,
    ) -> Result<Allocation> {
        let (size, alignment, kind) = (request.size, request.alignment, request.kind as u32);
        if size > self.block_sizes[ty as usize] {
            return Err(Error::OutOfMemory);
        }
        let mut guard = self.lock(ty);
        let pool = &mut *guard;
        // Close fits in any block beat splitting a large hole in a fuller one.
        for tight in [true, false] {
            for i in 0..pool.order.len() {
                let slot = pool.order[i];
                let block = pool.blocks[slot as usize]
                    .as_mut()
                    .expect("ordered block is live");
                let hit = if tight {
                    block.tlsf.allocate_tight(size, alignment, kind)
                } else {
                    block.tlsf.allocate_loose(size, alignment, kind)
                };
                if let Some((node, offset)) = hit {
                    let allocation = suballocation(block, ty, slot, node, offset, size);
                    pool.statistics.allocation_count += 1;
                    pool.statistics.allocation_bytes += size;
                    pool.sort_step();
                    return Ok(allocation);
                }
            }
        }

        let slot = self.create_block(ty, pool, size, within_budget)?;
        let block = pool.blocks[slot as usize]
            .as_mut()
            .expect("new block is live");
        let (node, offset) = block
            .tlsf
            .allocate(size, alignment, kind)
            .expect("an empty block larger than the request always fits");
        let allocation = suballocation(block, ty, slot, node, offset, size);
        pool.statistics.allocation_count += 1;
        pool.statistics.allocation_bytes += size;
        Ok(allocation)
    }

    /// Starts small (1/8 of the preferred size) while the pool is young, and halves on failure.
    fn create_block(
        &self,
        ty: u32,
        pool: &mut Pool,
        min_size: u64,
        within_budget: bool,
    ) -> Result<u32> {
        let largest = pool
            .blocks
            .iter()
            .flatten()
            .map(|b| b.tlsf.size())
            .max()
            .unwrap_or(0);
        let mut size = self.block_sizes[ty as usize];
        let mut shifts = 0;
        while shifts < NEW_BLOCK_SHIFTS && size / 2 > largest && size / 2 >= min_size * 2 {
            size /= 2;
            shifts += 1;
        }
        if within_budget && self.get_memory_properties2.is_some() {
            let available = self.available(ty);
            while shifts < NEW_BLOCK_SHIFTS && size > available && size / 2 >= min_size {
                size /= 2;
                shifts += 1;
            }
            if size > available {
                return Err(Error::OutOfMemory);
            }
        }
        let (memory, mapped) = loop {
            match self.allocate_memory(ty, size, Target::None) {
                Err(Error::OutOfMemory) if shifts < NEW_BLOCK_SHIFTS && size / 2 >= min_size => {
                    size /= 2;
                    shifts += 1;
                }
                result => break result?,
            }
        };
        let block = Block {
            memory,
            mapped,
            tlsf: Tlsf::new(size, self.granularity),
        };
        let slot = match pool.vacant.pop() {
            Some(slot) => {
                pool.blocks[slot as usize] = Some(block);
                slot
            }
            None => {
                pool.blocks.push(Some(block));
                (pool.blocks.len() - 1) as u32
            }
        };
        pool.order.push(slot);
        pool.statistics.block_count += 1;
        pool.statistics.block_bytes += size;
        Ok(slot)
    }

    fn allocate_memory(
        &self,
        ty: u32,
        size: u64,
        target: Target,
    ) -> Result<(VkDeviceMemory, *mut u8)> {
        let flags_info = VkMemoryAllocateFlagsInfo {
            sType: VkStructureType::MemoryAllocateFlagsInfo,
            pNext: null(),
            flags: VkMemoryAllocateFlags::DEVICE_ADDRESS,
            deviceMask: 0,
        };
        let (buffer, image) = match target {
            Target::None => (0, 0),
            Target::Buffer(buffer) => (buffer, 0),
            Target::Image(image) => (0, image),
        };
        let dedicated_info = VkMemoryDedicatedAllocateInfo {
            sType: VkStructureType::MemoryDedicatedAllocateInfo,
            pNext: (&raw const flags_info).cast(),
            image,
            buffer,
        };
        let info = VkMemoryAllocateInfo {
            sType: VkStructureType::MemoryAllocateInfo,
            pNext: if matches!(target, Target::None) {
                (&raw const flags_info).cast()
            } else {
                (&raw const dedicated_info).cast()
            },
            allocationSize: size,
            memoryTypeIndex: ty,
        };
        let mut memory = 0;
        check("vkAllocateMemory", unsafe {
            (self.fns.allocate_memory)(self.device, &info, null(), &mut memory)
        })?;

        let mut mapped: *mut c_void = null_mut();
        if self.types[ty as usize]
            .flags
            .contains(VkMemoryPropertyFlags::HOST_VISIBLE)
        {
            let result = unsafe {
                (self.fns.map_memory)(
                    self.device,
                    memory,
                    0,
                    VK_WHOLE_SIZE,
                    VkMemoryMapFlags::empty(),
                    &mut mapped,
                )
            };
            if let Err(error) = check("vkMapMemory", result) {
                unsafe { (self.fns.free_memory)(self.device, memory, null()) };
                return Err(error);
            }
        }
        Ok((memory, mapped.cast()))
    }

    #[inline]
    fn fits_budget(&self, ty: u32, size: u64) -> bool {
        self.get_memory_properties2.is_none() || size <= self.available(ty)
    }

    /// Driver-reported headroom of the heap backing memory type `ty`.
    fn available(&self, ty: u32) -> u64 {
        let heap = self.types[ty as usize].heap as usize;
        self.driver_budget().map_or(u64::MAX, |driver| {
            driver.heapBudget[heap].saturating_sub(driver.heapUsage[heap])
        })
    }

    fn driver_budget(&self) -> Option<VkPhysicalDeviceMemoryBudgetPropertiesEXT> {
        let get_memory_properties2 = self.get_memory_properties2?;
        let mut driver: VkPhysicalDeviceMemoryBudgetPropertiesEXT = unsafe { core::mem::zeroed() };
        driver.sType = VkStructureType::PhysicalDeviceMemoryBudgetPropertiesExt;
        let mut memory: VkPhysicalDeviceMemoryProperties2 = unsafe { core::mem::zeroed() };
        memory.sType = VkStructureType::PhysicalDeviceMemoryProperties2;
        memory.pNext = (&raw mut driver).cast();
        unsafe { get_memory_properties2(self.physical_device, &mut memory) };
        Some(driver)
    }

    fn mapped_range(
        &self,
        allocation: &Allocation,
        offset: u64,
        size: u64,
    ) -> Option<VkMappedMemoryRange> {
        let flags = self.types[allocation.memory_type as usize].flags;
        if !flags.contains(VkMemoryPropertyFlags::HOST_VISIBLE)
            || flags.contains(VkMemoryPropertyFlags::HOST_COHERENT)
        {
            return None;
        }
        debug_assert!(offset <= allocation.size);
        let size = if size == VK_WHOLE_SIZE {
            allocation.size - offset
        } else {
            size
        };
        let start = (allocation.offset + offset) & !(self.atom - 1);
        let end = align_up(allocation.offset + offset + size, self.atom);
        // Block sizes are multiples of the atom size; dedicated allocations may not be.
        let size = if allocation.is_dedicated() && end > allocation.size {
            VK_WHOLE_SIZE
        } else {
            end - start
        };
        Some(VkMappedMemoryRange {
            sType: VkStructureType::MappedMemoryRange,
            pNext: null(),
            memory: allocation.memory,
            offset: start,
            size,
        })
    }

    #[inline]
    fn lock(&self, ty: u32) -> MutexGuard<'_, Pool> {
        self.pools[ty as usize]
            .lock()
            .unwrap_or_else(PoisonError::into_inner)
    }
}

impl Drop for Allocator {
    fn drop(&mut self) {
        for pool in &mut self.pools {
            let pool = pool.get_mut().unwrap_or_else(PoisonError::into_inner);
            debug_assert!(
                std::thread::panicking() || pool.statistics.allocation_count == 0,
                "allocator dropped with {} live allocations",
                pool.statistics.allocation_count
            );
            for block in pool.blocks.drain(..).flatten() {
                unsafe { (self.fns.free_memory)(self.device, block.memory, null()) };
            }
        }
    }
}

#[inline]
fn suballocation(
    block: &Block,
    ty: u32,
    slot: u32,
    node: u32,
    offset: u64,
    size: u64,
) -> Allocation {
    Allocation {
        memory: block.memory,
        offset,
        size,
        mapped: if block.mapped.is_null() {
            null_mut()
        } else {
            unsafe { block.mapped.add(offset as usize) }
        },
        memory_type: ty,
        block: slot,
        node,
    }
}

/// Memory types usable for `location`, cheapest first. Cost counts missing preferred flags
/// plus present unwanted flags, mirroring VMA's `VMA_MEMORY_USAGE_AUTO` heuristics.
fn preference_order(
    types: &[MemoryType],
    location: MemoryLocation,
    device_access: bool,
) -> Box<[u8]> {
    use VkMemoryPropertyFlags as F;
    let none = F::empty();
    let (required, preferred, unwanted) = match location {
        MemoryLocation::GpuOnly => (none, F::DEVICE_LOCAL, F::HOST_VISIBLE | F::HOST_CACHED),
        MemoryLocation::CpuToGpu if device_access => (
            F::HOST_VISIBLE,
            F::DEVICE_LOCAL | F::HOST_COHERENT,
            F::HOST_CACHED,
        ),
        MemoryLocation::CpuToGpu => (
            F::HOST_VISIBLE,
            F::HOST_COHERENT,
            F::DEVICE_LOCAL | F::HOST_CACHED,
        ),
        MemoryLocation::GpuToCpu => (F::HOST_VISIBLE, F::HOST_CACHED | F::HOST_COHERENT, none),
    };
    let mut order: Vec<(u32, u8)> = types
        .iter()
        .enumerate()
        .filter(|(_, t)| t.flags.contains(required) && !t.flags.intersects(UNUSABLE))
        .map(|(i, t)| {
            (
                (preferred - t.flags).bits().count_ones()
                    + (t.flags & unwanted).bits().count_ones(),
                i as u8,
            )
        })
        .collect();
    order.sort_unstable();
    order.into_iter().map(|(_, i)| i).collect()
}