vk_alloc/
lib.rs

1#![warn(missing_docs)]
2#![deny(clippy::as_conversions)]
3#![deny(clippy::panic)]
4#![deny(clippy::unwrap_used)]
5
6//! A segregated list memory allocator for Vulkan.
7//!
8//! The allocator can pool allocations of a user defined lifetime together to help
9//! reducing the fragmentation.
10//!
11//! ## Example:
12//! ```ignore
13//! #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
14//! enum Lifetime {
15//!     Buffer,
16//!     Image,
17//! }
18//!
19//! impl vk_alloc::Lifetime for Lifetime {}
20//!
21//! unsafe {
22//!     Allocator::<Lifetime>::new(
23//!         &instance,
24//!         &physical_device,
25//!         &AllocatorDescriptor {
26//!             ..Default::default()
27//!         },
28//!     ).unwrap();
29//!
30//!     let allocation = alloc
31//!         .allocate(
32//!             &logical_device,
33//!             &AllocationDescriptor {
34//!                 location: MemoryLocation::GpuOnly,
35//!                 requirements: vk::MemoryRequirementsBuilder::new()
36//!                     .alignment(512)
37//!                     .size(1024)
38//!                     .memory_type_bits(u32::MAX)
39//!                     .build(),
40//!                 lifetime: Lifetime::Buffer,
41//!                 is_dedicated: false,
42//!                 is_optimal: false,
43//!             },
44//!         )
45//!         .unwrap();
46//! }
47//! ```
48//!
49use std::collections::HashMap;
50use std::convert::TryInto;
51use std::ffi::c_void;
52use std::fmt::Debug;
53use std::hash::Hash;
54use std::num::NonZeroUsize;
55use std::ptr;
56
57use erupt::{vk, ExtendableFrom};
58use parking_lot::{Mutex, RwLock};
59#[cfg(feature = "tracing")]
60use tracing1::{debug, info};
61
62pub use error::AllocatorError;
63
64mod error;
65
66type Result<T> = std::result::Result<T, AllocatorError>;
67
68/// For a minimal bucket size of 256b as log2.
69const MINIMAL_BUCKET_SIZE_LOG2: u32 = 8;
70
71/// The lifetime of an allocation. Used to pool allocations and reduce fragmentation.
72pub trait Lifetime: Debug + Copy + Hash + Eq + PartialEq {}
73
74/// Describes the configuration of an `Allocator`.
75#[derive(Debug, Clone)]
76pub struct AllocatorDescriptor {
77    /// The size of the blocks that are allocated. Defined as log2(size in bytes). Default: 64 MiB.
78    pub block_size: u8,
79}
80
81impl Default for AllocatorDescriptor {
82    fn default() -> Self {
83        Self { block_size: 26 }
84    }
85}
86
87/// The general purpose memory allocator. Implemented as a segregated list allocator.
88#[derive(Debug)]
89pub struct Allocator<LT: Lifetime> {
90    driver_id: vk::DriverId,
91    is_integrated: bool,
92    pools: RwLock<HashMap<LT, Vec<Mutex<MemoryPool>>>>,
93    block_size: vk::DeviceSize,
94    memory_types: Vec<vk::MemoryType>,
95    memory_properties: vk::PhysicalDeviceMemoryProperties,
96    buffer_image_granularity: u64,
97}
98
99impl<LT: Lifetime> Allocator<LT> {
100    /// Creates a new allocator.
101    ///
102    /// # Safety
103    /// Caller needs to make sure that the provided instance and device are in a valid state.
104    #[cfg_attr(feature = "profiling", profiling::function)]
105    pub unsafe fn new(
106        instance: &erupt::InstanceLoader,
107        physical_device: vk::PhysicalDevice,
108        descriptor: &AllocatorDescriptor,
109    ) -> Result<Self> {
110        let (driver_id, is_integrated, buffer_image_granularity) =
111            query_driver(instance, physical_device);
112
113        #[cfg(feature = "tracing")]
114        debug!("Driver ID of the physical device: {:?}", driver_id);
115
116        let memory_properties = instance.get_physical_device_memory_properties(physical_device);
117
118        let memory_types_count: usize = (memory_properties.memory_type_count).try_into()?;
119        let memory_types = memory_properties.memory_types[..memory_types_count].to_owned();
120
121        #[cfg(feature = "tracing")]
122        print_memory_types(memory_properties, &memory_types)?;
123
124        let block_size: vk::DeviceSize = (2u64).pow(descriptor.block_size.into()).into();
125
126        Ok(Self {
127            driver_id,
128            is_integrated,
129            pools: RwLock::default(),
130            block_size,
131            memory_types,
132            memory_properties,
133            buffer_image_granularity,
134        })
135    }
136
137    /// Allocates memory for a buffer.
138    ///
139    /// # Safety
140    /// Caller needs to make sure that the provided device and buffer are in a valid state.
141    #[cfg_attr(feature = "profiling", profiling::function)]
142    pub unsafe fn allocate_memory_for_buffer(
143        &self,
144        device: &erupt::DeviceLoader,
145        buffer: vk::Buffer,
146        location: MemoryLocation,
147        lifetime: LT,
148    ) -> Result<Allocation<LT>> {
149        let info = vk::BufferMemoryRequirementsInfo2Builder::new().buffer(buffer);
150        let mut dedicated_requirements = vk::MemoryDedicatedRequirementsBuilder::new();
151        let requirements =
152            vk::MemoryRequirements2Builder::new().extend_from(&mut dedicated_requirements);
153
154        let requirements =
155            device.get_buffer_memory_requirements2(&info, Some(requirements.build_dangling()));
156
157        let memory_requirements = requirements.memory_requirements;
158
159        let is_dedicated = dedicated_requirements.prefers_dedicated_allocation == 1
160            || dedicated_requirements.requires_dedicated_allocation == 1;
161
162        let alloc_decs = AllocationDescriptor {
163            requirements: memory_requirements,
164            location,
165            lifetime,
166            is_dedicated,
167            is_optimal: false,
168        };
169
170        self.allocate(device, &alloc_decs)
171    }
172
173    /// Allocates memory for an image. `is_optimal` must be set true if the image is a optimal image (a regular texture).
174    ///
175    /// # Safety
176    /// Caller needs to make sure that the provided device and image are in a valid state.
177    #[cfg_attr(feature = "profiling", profiling::function)]
178    pub unsafe fn allocate_memory_for_image(
179        &self,
180        device: &erupt::DeviceLoader,
181        image: vk::Image,
182        location: MemoryLocation,
183        lifetime: LT,
184        is_optimal: bool,
185    ) -> Result<Allocation<LT>> {
186        let info = vk::ImageMemoryRequirementsInfo2Builder::new().image(image);
187        let mut dedicated_requirements = vk::MemoryDedicatedRequirementsBuilder::new();
188        let requirements =
189            vk::MemoryRequirements2Builder::new().extend_from(&mut dedicated_requirements);
190
191        let requirements =
192            device.get_image_memory_requirements2(&info, Some(requirements.build_dangling()));
193
194        let memory_requirements = requirements.memory_requirements;
195
196        let is_dedicated = dedicated_requirements.prefers_dedicated_allocation == 1
197            || dedicated_requirements.requires_dedicated_allocation == 1;
198
199        let alloc_decs = AllocationDescriptor {
200            requirements: memory_requirements,
201            location,
202            lifetime,
203            is_dedicated,
204            is_optimal,
205        };
206
207        self.allocate(device, &alloc_decs)
208    }
209
210    /// Allocates memory on the allocator.
211    ///
212    /// # Safety
213    /// Caller needs to make sure that the provided device is in a valid state.
214    #[cfg_attr(feature = "profiling", profiling::function)]
215    pub unsafe fn allocate(
216        &self,
217        device: &erupt::DeviceLoader,
218        descriptor: &AllocationDescriptor<LT>,
219    ) -> Result<Allocation<LT>> {
220        let size = descriptor.requirements.size;
221        let alignment = descriptor.requirements.alignment;
222
223        #[cfg(feature = "tracing")]
224        debug!(
225            "Allocating {} bytes with an alignment of {}.",
226            size, alignment
227        );
228
229        if size == 0 || !alignment.is_power_of_two() {
230            return Err(AllocatorError::InvalidAlignment);
231        }
232
233        let memory_type_index = self.find_memory_type_index(
234            descriptor.location,
235            descriptor.requirements.memory_type_bits,
236        )?;
237
238        let has_key = self.pools.read().contains_key(&descriptor.lifetime);
239        if !has_key {
240            let mut pools = Vec::with_capacity(self.memory_types.len());
241            for (i, memory_type) in self.memory_types.iter().enumerate() {
242                let pool = MemoryPool::new(
243                    self.block_size,
244                    i.try_into()?,
245                    memory_type
246                        .property_flags
247                        .contains(vk::MemoryPropertyFlags::HOST_VISIBLE),
248                )?;
249                pools.push(Mutex::new(pool));
250            }
251
252            self.pools.write().insert(descriptor.lifetime, pools);
253        }
254
255        let lifetime_pools = self.pools.read();
256
257        let pool = &lifetime_pools
258            .get(&descriptor.lifetime)
259            .ok_or_else(|| {
260                AllocatorError::Internal(format!(
261                    "can't find pool for lifetime {:?}",
262                    descriptor.lifetime
263                ))
264            })?
265            .get(memory_type_index)
266            .ok_or_else(|| {
267                AllocatorError::Internal(format!(
268                    "can't find memory_type {} in pool {:?}",
269                    memory_type_index, descriptor.lifetime
270                ))
271            })?;
272
273        if descriptor.is_dedicated || size >= self.block_size {
274            #[cfg(feature = "tracing")]
275            debug!(
276                "Allocating as dedicated block on memory type {}",
277                memory_type_index
278            );
279            pool.lock()
280                .allocate_dedicated(device, size, descriptor.lifetime)
281        } else {
282            #[cfg(feature = "tracing")]
283            debug!("Sub allocating on memory type {}", memory_type_index);
284            pool.lock().allocate(
285                device,
286                self.buffer_image_granularity,
287                size,
288                alignment,
289                descriptor.lifetime,
290                descriptor.is_optimal,
291            )
292        }
293    }
294
295    #[cfg_attr(feature = "profiling", profiling::function)]
296    fn find_memory_type_index(
297        &self,
298        location: MemoryLocation,
299        memory_type_bits: u32,
300    ) -> Result<usize> {
301        // AMD APU main memory heap is NOT DEVICE_LOCAL.
302        let memory_property_flags = if (self.driver_id == vk::DriverId::AMD_OPEN_SOURCE
303            || self.driver_id == vk::DriverId::AMD_PROPRIETARY
304            || self.driver_id == vk::DriverId::MESA_RADV)
305            && self.is_integrated
306        {
307            match location {
308                MemoryLocation::GpuOnly => {
309                    vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT
310                }
311                MemoryLocation::CpuToGpu => {
312                    vk::MemoryPropertyFlags::DEVICE_LOCAL
313                        | vk::MemoryPropertyFlags::HOST_VISIBLE
314                        | vk::MemoryPropertyFlags::HOST_COHERENT
315                }
316                MemoryLocation::GpuToCpu => {
317                    vk::MemoryPropertyFlags::HOST_VISIBLE
318                        | vk::MemoryPropertyFlags::HOST_COHERENT
319                        | vk::MemoryPropertyFlags::HOST_CACHED
320                }
321            }
322        } else {
323            match location {
324                MemoryLocation::GpuOnly => vk::MemoryPropertyFlags::DEVICE_LOCAL,
325                MemoryLocation::CpuToGpu => {
326                    vk::MemoryPropertyFlags::DEVICE_LOCAL
327                        | vk::MemoryPropertyFlags::HOST_VISIBLE
328                        | vk::MemoryPropertyFlags::HOST_COHERENT
329                }
330                MemoryLocation::GpuToCpu => {
331                    vk::MemoryPropertyFlags::HOST_VISIBLE
332                        | vk::MemoryPropertyFlags::HOST_COHERENT
333                        | vk::MemoryPropertyFlags::HOST_CACHED
334                }
335            }
336        };
337
338        let memory_type_index_optional =
339            self.query_memory_type_index(memory_type_bits, memory_property_flags)?;
340
341        if let Some(index) = memory_type_index_optional {
342            return Ok(index);
343        }
344
345        // Fallback for drivers that don't expose BAR (Base Address Register).
346        let memory_property_flags = match location {
347            MemoryLocation::GpuOnly => vk::MemoryPropertyFlags::DEVICE_LOCAL,
348            MemoryLocation::CpuToGpu => {
349                vk::MemoryPropertyFlags::HOST_VISIBLE | vk::MemoryPropertyFlags::HOST_COHERENT
350            }
351            MemoryLocation::GpuToCpu => {
352                vk::MemoryPropertyFlags::HOST_VISIBLE
353                    | vk::MemoryPropertyFlags::HOST_COHERENT
354                    | vk::MemoryPropertyFlags::HOST_CACHED
355            }
356        };
357
358        let memory_type_index_optional =
359            self.query_memory_type_index(memory_type_bits, memory_property_flags)?;
360
361        match memory_type_index_optional {
362            Some(index) => Ok(index),
363            None => Err(AllocatorError::NoCompatibleMemoryTypeFound),
364        }
365    }
366
367    #[cfg_attr(feature = "profiling", profiling::function)]
368    fn query_memory_type_index(
369        &self,
370        memory_type_bits: u32,
371        memory_property_flags: vk::MemoryPropertyFlags,
372    ) -> Result<Option<usize>> {
373        let memory_properties = &self.memory_properties;
374        let memory_type_count: usize = memory_properties.memory_type_count.try_into()?;
375        let index = memory_properties.memory_types[..memory_type_count]
376            .iter()
377            .enumerate()
378            .find(|(index, memory_type)| {
379                memory_type_is_compatible(*index, memory_type_bits)
380                    && memory_type.property_flags.contains(memory_property_flags)
381            })
382            .map(|(index, _)| index);
383        Ok(index)
384    }
385
386    /// Frees the allocation.
387    ///
388    /// # Safety
389    /// Caller needs to make sure that the allocation is not in use anymore and will not be used
390    /// after being deallocated.
391    #[cfg_attr(feature = "profiling", profiling::function)]
392    pub unsafe fn deallocate(
393        &self,
394        device: &erupt::DeviceLoader,
395        allocation: &Allocation<LT>,
396    ) -> Result<()> {
397        let memory_type_index: usize = allocation.memory_type_index.try_into()?;
398        let pools = &self.pools.read();
399        let memory_pool = &pools
400            .get(&allocation.lifetime)
401            .ok_or_else(|| {
402                AllocatorError::Internal(format!(
403                    "can't find pool for lifetime {:?}",
404                    allocation.lifetime
405                ))
406            })?
407            .get(memory_type_index)
408            .ok_or_else(|| {
409                AllocatorError::Internal(format!(
410                    "can't find memory_type {} in pool {:?}",
411                    memory_type_index, allocation.lifetime
412                ))
413            })?;
414
415        if let Some(chunk_key) = allocation.chunk_key {
416            #[cfg(feature = "tracing")]
417            debug!(
418                "Deallocating chunk on device memory 0x{:02x}, offset {}, size {}",
419                allocation.device_memory.0, allocation.offset, allocation.size
420            );
421            memory_pool.lock().free_chunk(chunk_key)?;
422        } else {
423            // Dedicated block
424            #[cfg(feature = "tracing")]
425            debug!(
426                "Deallocating dedicated device memory 0x{:02x} size {}",
427                allocation.device_memory.0, allocation.size
428            );
429            memory_pool
430                .lock()
431                .free_block(&device, allocation.block_key)?;
432        }
433
434        Ok(())
435    }
436
437    /// Releases all memory blocks back to the system. Should be called before drop.
438    ///
439    /// # Safety
440    /// Caller needs to make sure that no allocations are used anymore and will not being used
441    /// after calling this function.
442    #[cfg_attr(feature = "profiling", profiling::function)]
443    pub unsafe fn cleanup(&self, device: &erupt::DeviceLoader) {
444        for (_, mut lifetime_pools) in self.pools.write().drain() {
445            lifetime_pools.drain(..).for_each(|pool| {
446                pool.lock().blocks.iter_mut().for_each(|block| {
447                    if let Some(block) = block {
448                        block.destroy(&device)
449                    }
450                })
451            });
452        }
453    }
454
455    /// Number of allocations.
456    #[cfg_attr(feature = "profiling", profiling::function)]
457    pub fn allocation_count(&self) -> usize {
458        let mut count = 0;
459        for (_, lifetime_pools) in self.pools.read().iter() {
460            lifetime_pools.iter().for_each(|pool| {
461                let pool = pool.lock();
462                for chunk in &pool.chunks {
463                    if let Some(chunk) = chunk {
464                        if chunk.chunk_type != ChunkType::Free {
465                            count += 1;
466                        }
467                    }
468                }
469            });
470        }
471
472        for (_, lifetime_pools) in self.pools.read().iter() {
473            lifetime_pools.iter().for_each(|pool| {
474                let pool = pool.lock();
475                for block in &pool.blocks {
476                    if let Some(block) = block {
477                        if block.is_dedicated {
478                            count += 1;
479                        }
480                    }
481                }
482            });
483        }
484
485        count
486    }
487
488    /// Number of unused ranges between allocations.
489    #[cfg_attr(feature = "profiling", profiling::function)]
490    pub fn unused_range_count(&self) -> usize {
491        let mut unused_count: usize = 0;
492
493        for (_, lifetime_pools) in self.pools.read().iter() {
494            lifetime_pools.iter().for_each(|pool| {
495                collect_start_chunks(&pool).iter().for_each(|key| {
496                    let mut next_key: NonZeroUsize = *key;
497                    let mut previous_size: vk::DeviceSize = 0;
498                    let mut previous_offset: vk::DeviceSize = 0;
499                    loop {
500                        let pool = pool.lock();
501                        let chunk = pool.chunks[next_key.get()]
502                            .as_ref()
503                            .expect("can't find chunk in chunk list");
504                        if chunk.offset != previous_offset + previous_size {
505                            unused_count += 1;
506                        }
507
508                        if let Some(key) = chunk.next {
509                            next_key = key
510                        } else {
511                            break;
512                        }
513
514                        previous_size = chunk.size;
515                        previous_offset = chunk.offset
516                    }
517                });
518            })
519        }
520
521        unused_count
522    }
523
524    /// Number of bytes used by the allocations.
525    #[cfg_attr(feature = "profiling", profiling::function)]
526    pub fn used_bytes(&self) -> vk::DeviceSize {
527        let mut bytes = 0;
528
529        for (_, lifetime_pools) in self.pools.read().iter() {
530            lifetime_pools.iter().for_each(|pool| {
531                let pool = pool.lock();
532                for chunk in &pool.chunks {
533                    if let Some(chunk) = chunk {
534                        if chunk.chunk_type != ChunkType::Free {
535                            bytes += chunk.size;
536                        }
537                    }
538                }
539            });
540        }
541
542        for (_, lifetime_pools) in self.pools.read().iter() {
543            lifetime_pools.iter().for_each(|pool| {
544                let pool = pool.lock();
545                for block in &pool.blocks {
546                    if let Some(block) = block {
547                        if block.is_dedicated {
548                            bytes += block.size;
549                        }
550                    }
551                }
552            });
553        }
554
555        bytes
556    }
557
558    /// Number of bytes used by the unused ranges between allocations.
559    #[cfg_attr(feature = "profiling", profiling::function)]
560    pub fn unused_bytes(&self) -> vk::DeviceSize {
561        let mut unused_bytes: vk::DeviceSize = 0;
562
563        for (_, lifetime_pools) in self.pools.read().iter() {
564            lifetime_pools.iter().for_each(|pool| {
565                collect_start_chunks(&pool).iter().for_each(|key| {
566                    let mut next_key: NonZeroUsize = *key;
567                    let mut previous_size: vk::DeviceSize = 0;
568                    let mut previous_offset: vk::DeviceSize = 0;
569                    loop {
570                        let pool = pool.lock();
571                        let chunk = pool.chunks[next_key.get()]
572                            .as_ref()
573                            .expect("can't find chunk in chunk list");
574                        if chunk.offset != previous_offset + previous_size {
575                            unused_bytes += chunk.offset - (previous_offset + previous_size);
576                        }
577
578                        if let Some(key) = chunk.next {
579                            next_key = key
580                        } else {
581                            break;
582                        }
583
584                        previous_size = chunk.size;
585                        previous_offset = chunk.offset
586                    }
587                });
588            });
589        }
590
591        unused_bytes
592    }
593
594    /// Number of allocated Vulkan memory blocks.
595    #[cfg_attr(feature = "profiling", profiling::function)]
596    pub fn block_count(&self) -> usize {
597        let mut count: usize = 0;
598
599        for (_, lifetime_pools) in self.pools.read().iter() {
600            count += lifetime_pools
601                .iter()
602                .map(|pool| pool.lock().blocks.len())
603                .sum::<usize>();
604        }
605
606        count
607    }
608}
609
610#[derive(Debug, Copy, Clone, Eq, PartialEq)]
611#[repr(u8)]
612enum ChunkType {
613    Free,
614    Linear,
615    Optimal,
616}
617
618impl ChunkType {
619    /// There is an implementation-dependent limit, bufferImageGranularity, which specifies a
620    /// page-like granularity at which linear and non-linear resources must be placed in adjacent
621    /// memory locations to avoid aliasing.
622    #[cfg_attr(feature = "profiling", profiling::function)]
623    fn granularity_conflict(self, other: ChunkType) -> bool {
624        if self == ChunkType::Free || other == ChunkType::Free {
625            return false;
626        }
627
628        self != other
629    }
630}
631
632/// The intended location of the memory.
633#[derive(Debug, Clone, Copy, PartialEq, Eq)]
634pub enum MemoryLocation {
635    /// Mainly used for uploading data to the GPU.
636    CpuToGpu,
637    /// Used as fast access memory for the GPU.
638    GpuOnly,
639    /// Mainly used for downloading data from the GPU.
640    GpuToCpu,
641}
642
643/// The descriptor for an allocation on the allocator.
644#[derive(Clone, Debug)]
645pub struct AllocationDescriptor<LT: Lifetime> {
646    /// Location where the memory allocation should be stored.
647    pub location: MemoryLocation,
648    /// Vulkan memory requirements for an allocation.
649    pub requirements: vk::MemoryRequirements,
650    /// The lifetime of an allocation. Used to pool together resources of the same lifetime.
651    pub lifetime: LT,
652    /// If the allocation should be dedicated.
653    pub is_dedicated: bool,
654    /// True if the allocation is for a optimal image (regular textures). Buffers and linear
655    /// images need to set this false.
656    pub is_optimal: bool,
657}
658
659/// An allocation of the `Allocator`.
660#[derive(Clone, Debug)]
661pub struct Allocation<LT: Lifetime> {
662    memory_type_index: u32,
663    lifetime: LT,
664    block_key: NonZeroUsize,
665    chunk_key: Option<NonZeroUsize>,
666    mapped_ptr: Option<std::ptr::NonNull<c_void>>,
667
668    device_memory: vk::DeviceMemory,
669    offset: vk::DeviceSize,
670    size: vk::DeviceSize,
671}
672
673unsafe impl<LT: Lifetime> Send for Allocation<LT> {}
674
675unsafe impl<LT: Lifetime> Sync for Allocation<LT> {}
676
677impl<LT: Lifetime> Allocation<LT> {
678    /// The `DeviceMemory` of the allocation. Managed by the allocator.
679    #[inline]
680    pub fn device_memory(&self) -> vk::DeviceMemory {
681        self.device_memory
682    }
683
684    /// The offset inside the `DeviceMemory`.
685    #[inline]
686    pub fn offset(&self) -> vk::DeviceSize {
687        self.offset
688    }
689
690    /// The size of the allocation.
691    #[inline]
692    pub fn size(&self) -> vk::DeviceSize {
693        self.size
694    }
695
696    /// Returns a valid mapped slice if the memory is host visible, otherwise it will return None.
697    /// The slice already references the exact memory region of the sub allocation, so no offset needs to be applied.
698    ///
699    /// # Safety
700    /// Caller needs to make sure that the allocation is still valid and coherent.
701    #[cfg_attr(feature = "profiling", profiling::function)]
702    pub unsafe fn mapped_slice(&self) -> Result<Option<&[u8]>> {
703        let slice = if let Some(ptr) = self.mapped_ptr {
704            let size = self.size.try_into()?;
705            #[allow(clippy::as_conversions)]
706            Some(std::slice::from_raw_parts(ptr.as_ptr() as *const _, size))
707        } else {
708            None
709        };
710        Ok(slice)
711    }
712
713    /// Returns a valid mapped mutable slice if the memory is host visible, otherwise it will return None.
714    /// The slice already references the exact memory region of the sub allocation, so no offset needs to be applied.
715    ///
716    /// # Safety
717    /// Caller needs to make sure that the allocation is still valid and coherent.
718    #[cfg_attr(feature = "profiling", profiling::function)]
719    pub unsafe fn mapped_slice_mut(&mut self) -> Result<Option<&mut [u8]>> {
720        let slice = if let Some(ptr) = self.mapped_ptr.as_mut() {
721            let size = self.size.try_into()?;
722            #[allow(clippy::as_conversions)]
723            Some(std::slice::from_raw_parts_mut(ptr.as_ptr() as *mut _, size))
724        } else {
725            None
726        };
727        Ok(slice)
728    }
729}
730
731#[derive(Clone, Debug)]
732struct BestFitCandidate {
733    aligned_offset: u64,
734    key: NonZeroUsize,
735    free_list_index: usize,
736    free_size: vk::DeviceSize,
737}
738
739/// A managed memory region of a specific memory type.
740///
741/// Used to separate buffer (linear) and texture (optimal) memory regions,
742/// so that internal memory fragmentation is kept low.
743#[derive(Debug)]
744struct MemoryPool {
745    memory_type_index: u32,
746    block_size: vk::DeviceSize,
747    is_mappable: bool,
748    blocks: Vec<Option<MemoryBlock>>,
749    chunks: Vec<Option<MemoryChunk>>,
750    free_chunks: Vec<Vec<NonZeroUsize>>,
751    max_bucket_index: u32,
752
753    // Helper lists to find free slots inside the block and chunks lists.
754    free_block_slots: Vec<NonZeroUsize>,
755    free_chunk_slots: Vec<NonZeroUsize>,
756}
757
758impl MemoryPool {
759    #[cfg_attr(feature = "profiling", profiling::function)]
760    fn new(block_size: vk::DeviceSize, memory_type_index: u32, is_mappable: bool) -> Result<Self> {
761        let mut blocks = Vec::with_capacity(128);
762        let mut chunks = Vec::with_capacity(128);
763
764        // Fill the Zero slot with None, since our keys are of type NonZeroUsize
765        blocks.push(None);
766        chunks.push(None);
767
768        // The smallest bucket size is 256b, which is log2(256) = 8. So the maximal bucket size is
769        // "64 - 8 - log2(block_size - 1)". We can't have a free chunk that is bigger than a block.
770        let bucket_count = 64 - MINIMAL_BUCKET_SIZE_LOG2 - (block_size - 1).leading_zeros();
771
772        // We preallocate only a reasonable amount of entries for each bucket.
773        // The highest bucket for example can only hold two values at most.
774        let mut free_chunks = Vec::with_capacity(bucket_count.try_into()?);
775        for i in 0..bucket_count {
776            let min_bucket_element_size = if i == 0 {
777                512
778            } else {
779                2u64.pow(MINIMAL_BUCKET_SIZE_LOG2 - 1 + i).into()
780            };
781            let max_elements: usize = (block_size / min_bucket_element_size).try_into()?;
782            free_chunks.push(Vec::with_capacity(512.min(max_elements)));
783        }
784
785        Ok(Self {
786            memory_type_index,
787            block_size,
788            is_mappable,
789            blocks,
790            chunks,
791            free_chunks,
792            free_block_slots: Vec::with_capacity(16),
793            free_chunk_slots: Vec::with_capacity(16),
794            max_bucket_index: bucket_count - 1,
795        })
796    }
797
798    #[cfg_attr(feature = "profiling", profiling::function)]
799    fn add_block(&mut self, block: MemoryBlock) -> NonZeroUsize {
800        if let Some(key) = self.free_block_slots.pop() {
801            self.blocks[key.get()] = Some(block);
802            key
803        } else {
804            let key = self.blocks.len();
805            self.blocks.push(Some(block));
806            NonZeroUsize::new(key).expect("new block key was zero")
807        }
808    }
809
810    #[cfg_attr(feature = "profiling", profiling::function)]
811    fn add_chunk(&mut self, chunk: MemoryChunk) -> NonZeroUsize {
812        if let Some(key) = self.free_chunk_slots.pop() {
813            self.chunks[key.get()] = Some(chunk);
814            key
815        } else {
816            let key = self.chunks.len();
817            self.chunks.push(Some(chunk));
818            NonZeroUsize::new(key).expect("new chunk key was zero")
819        }
820    }
821
822    #[cfg_attr(feature = "profiling", profiling::function)]
823    unsafe fn allocate_dedicated<LT: Lifetime>(
824        &mut self,
825        device: &erupt::DeviceLoader,
826        size: vk::DeviceSize,
827        lifetime: LT,
828    ) -> Result<Allocation<LT>> {
829        let block = MemoryBlock::new(device, size, self.memory_type_index, self.is_mappable, true)?;
830
831        let device_memory = block.device_memory;
832        let mapped_ptr = std::ptr::NonNull::new(block.mapped_ptr);
833
834        let key = self.add_block(block);
835
836        Ok(Allocation {
837            memory_type_index: self.memory_type_index,
838            lifetime,
839            block_key: key,
840            chunk_key: None,
841            device_memory,
842            offset: 0,
843            size,
844            mapped_ptr,
845        })
846    }
847
848    #[cfg_attr(feature = "profiling", profiling::function)]
849    unsafe fn allocate<LT: Lifetime>(
850        &mut self,
851        device: &erupt::DeviceLoader,
852        buffer_image_granularity: u64,
853        size: vk::DeviceSize,
854        alignment: vk::DeviceSize,
855        lifetime: LT,
856        is_optimal: bool,
857    ) -> Result<Allocation<LT>> {
858        let mut bucket_index = calculate_bucket_index(size);
859
860        // Make sure that we don't try to allocate a chunk bigger than the block.
861        debug_assert!(bucket_index <= self.max_bucket_index);
862
863        let chunk_type = if is_optimal {
864            ChunkType::Optimal
865        } else {
866            ChunkType::Linear
867        };
868
869        loop {
870            // We couldn't find a suitable empty chunk, so we will allocate a new block.
871            if bucket_index > self.max_bucket_index {
872                self.allocate_new_block(device)?;
873                bucket_index = self.max_bucket_index;
874            }
875
876            let index: usize = bucket_index.try_into()?;
877            let free_list = &self.free_chunks[index];
878
879            // Find best fit in this bucket.
880            let mut best_fit_candidate: Option<BestFitCandidate> = None;
881            for (index, key) in free_list.iter().enumerate() {
882                let chunk = &self.chunks[key.get()]
883                    .as_ref()
884                    .expect("can't find chunk in chunk list");
885                debug_assert!(chunk.chunk_type == ChunkType::Free);
886
887                if chunk.size < size {
888                    continue;
889                }
890
891                let mut aligned_offset = 0;
892
893                // We need to handle the granularity between chunks. See "Buffer-Image Granularity"
894                // in the Vulkan specs.
895                if let Some(previous) = chunk.previous {
896                    let previous = self
897                        .chunks
898                        .get(previous.get())
899                        .ok_or_else(|| {
900                            AllocatorError::Internal("can't find previous chunk".into())
901                        })?
902                        .as_ref()
903                        .ok_or_else(|| {
904                            AllocatorError::Internal("previous chunk was empty".into())
905                        })?;
906
907                    aligned_offset = align_up(chunk.offset, alignment);
908
909                    if previous.chunk_type.granularity_conflict(chunk_type)
910                        && is_on_same_page(
911                            previous.offset,
912                            previous.size,
913                            aligned_offset,
914                            buffer_image_granularity,
915                        )
916                    {
917                        aligned_offset = align_up(aligned_offset, buffer_image_granularity);
918                    }
919                }
920
921                if let Some(next) = chunk.next {
922                    let next = self
923                        .chunks
924                        .get(next.get())
925                        .ok_or_else(|| AllocatorError::Internal("can't find next chunk".into()))?
926                        .as_ref()
927                        .ok_or_else(|| AllocatorError::Internal("next chunk was empty".into()))?;
928
929                    if next.chunk_type.granularity_conflict(chunk_type)
930                        && is_on_same_page(
931                            next.offset,
932                            next.size,
933                            aligned_offset,
934                            buffer_image_granularity,
935                        )
936                    {
937                        continue;
938                    }
939                }
940
941                let padding = aligned_offset - chunk.offset;
942                let aligned_size = padding + size;
943
944                // Try to find the best fitting chunk.
945                if chunk.size >= aligned_size {
946                    let free_size = chunk.size - aligned_size;
947
948                    let best_fit_size = if let Some(best_fit) = &best_fit_candidate {
949                        best_fit.free_size
950                    } else {
951                        u64::MAX
952                    };
953
954                    if free_size < best_fit_size {
955                        best_fit_candidate = Some(BestFitCandidate {
956                            aligned_offset,
957                            key: *key,
958                            free_list_index: index,
959                            free_size,
960                        })
961                    }
962                }
963            }
964
965            // Allocate using the best fit candidate.
966            if let Some(candidate) = &best_fit_candidate {
967                self.free_chunks
968                    .get_mut(index)
969                    .ok_or_else(|| AllocatorError::Internal("can't find free chunk".to_owned()))?
970                    .remove(candidate.free_list_index);
971
972                // Split the lhs chunk and register the rhs as a new free chunk.
973                let new_free_chunk_key = if candidate.free_size != 0 {
974                    let candidate_chunk = self.chunks[candidate.key.get()]
975                        .as_ref()
976                        .expect("can't find candidate in chunk list")
977                        .clone();
978
979                    let new_free_offset = candidate.aligned_offset + size;
980                    let new_free_size =
981                        (candidate_chunk.offset + candidate_chunk.size) - new_free_offset;
982
983                    let new_free_chunk = MemoryChunk {
984                        block_key: candidate_chunk.block_key,
985                        size: new_free_size,
986                        offset: new_free_offset,
987                        previous: Some(candidate.key),
988                        next: candidate_chunk.next,
989                        chunk_type: ChunkType::Free,
990                    };
991
992                    let new_free_chunk_key = self.add_chunk(new_free_chunk);
993
994                    let rhs_bucket_index: usize =
995                        calculate_bucket_index(new_free_size).try_into()?;
996                    self.free_chunks[rhs_bucket_index].push(new_free_chunk_key);
997
998                    Some(new_free_chunk_key)
999                } else {
1000                    None
1001                };
1002
1003                let candidate_chunk = self.chunks[candidate.key.get()]
1004                    .as_mut()
1005                    .expect("can't find chunk in chunk list");
1006                candidate_chunk.chunk_type = chunk_type;
1007                candidate_chunk.offset = candidate.aligned_offset;
1008                candidate_chunk.size = size;
1009
1010                let block = self.blocks[candidate_chunk.block_key.get()]
1011                    .as_ref()
1012                    .expect("can't find block in block list");
1013
1014                let mapped_ptr = if !block.mapped_ptr.is_null() {
1015                    let offset: usize = candidate_chunk.offset.try_into()?;
1016                    let offset_ptr = block.mapped_ptr.add(offset);
1017                    std::ptr::NonNull::new(offset_ptr)
1018                } else {
1019                    None
1020                };
1021
1022                let allocation = Allocation {
1023                    memory_type_index: self.memory_type_index,
1024                    lifetime,
1025                    block_key: candidate_chunk.block_key,
1026                    chunk_key: Some(candidate.key),
1027                    device_memory: block.device_memory,
1028                    offset: candidate_chunk.offset,
1029                    size: candidate_chunk.size,
1030                    mapped_ptr,
1031                };
1032
1033                // Properly link the chain of chunks.
1034                let old_next_key = if let Some(new_free_chunk_key) = new_free_chunk_key {
1035                    let old_next_key = candidate_chunk.next;
1036                    candidate_chunk.next = Some(new_free_chunk_key);
1037                    old_next_key
1038                } else {
1039                    None
1040                };
1041
1042                if let Some(old_next_key) = old_next_key {
1043                    let old_next = self.chunks[old_next_key.get()]
1044                        .as_mut()
1045                        .expect("can't find old next in chunk list");
1046                    old_next.previous = new_free_chunk_key;
1047                }
1048
1049                return Ok(allocation);
1050            }
1051
1052            bucket_index += 1;
1053        }
1054    }
1055
1056    #[cfg_attr(feature = "profiling", profiling::function)]
1057    unsafe fn allocate_new_block(&mut self, device: &erupt::DeviceLoader) -> Result<()> {
1058        let block = MemoryBlock::new(
1059            device,
1060            self.block_size,
1061            self.memory_type_index,
1062            self.is_mappable,
1063            false,
1064        )?;
1065
1066        let block_key = self.add_block(block);
1067
1068        let chunk = MemoryChunk {
1069            block_key,
1070            size: self.block_size,
1071            offset: 0,
1072            previous: None,
1073            next: None,
1074            chunk_type: ChunkType::Free,
1075        };
1076
1077        let chunk_key = self.add_chunk(chunk);
1078
1079        let index: usize = self.max_bucket_index.try_into()?;
1080        self.free_chunks[index].push(chunk_key);
1081
1082        Ok(())
1083    }
1084
1085    #[cfg_attr(feature = "profiling", profiling::function)]
1086    fn free_chunk(&mut self, chunk_key: NonZeroUsize) -> Result<()> {
1087        let (previous_key, next_key, size) = {
1088            let chunk = self.chunks[chunk_key.get()]
1089                .as_mut()
1090                .ok_or(AllocatorError::CantFindChunk)?;
1091            chunk.chunk_type = ChunkType::Free;
1092            (chunk.previous, chunk.next, chunk.size)
1093        };
1094        self.add_to_free_list(chunk_key, size)?;
1095
1096        self.merge_free_neighbor(next_key, chunk_key, false)?;
1097        self.merge_free_neighbor(previous_key, chunk_key, true)?;
1098
1099        Ok(())
1100    }
1101
1102    #[cfg_attr(feature = "profiling", profiling::function)]
1103    fn merge_free_neighbor(
1104        &mut self,
1105        neighbor: Option<NonZeroUsize>,
1106        chunk_key: NonZeroUsize,
1107        neighbor_is_lhs: bool,
1108    ) -> Result<()> {
1109        if let Some(neighbor_key) = neighbor {
1110            if self.chunks[neighbor_key.get()]
1111                .as_ref()
1112                .expect("can't find chunk in chunk list")
1113                .chunk_type
1114                == ChunkType::Free
1115            {
1116                if neighbor_is_lhs {
1117                    self.merge_rhs_into_lhs_chunk(neighbor_key, chunk_key)?;
1118                } else {
1119                    self.merge_rhs_into_lhs_chunk(chunk_key, neighbor_key)?;
1120                }
1121            }
1122        }
1123        Ok(())
1124    }
1125
1126    #[cfg_attr(feature = "profiling", profiling::function)]
1127    fn merge_rhs_into_lhs_chunk(
1128        &mut self,
1129        lhs_chunk_key: NonZeroUsize,
1130        rhs_chunk_key: NonZeroUsize,
1131    ) -> Result<()> {
1132        let (rhs_size, rhs_offset, rhs_next) = {
1133            let chunk = self.chunks[rhs_chunk_key.get()]
1134                .take()
1135                .expect("can't find chunk in chunk list");
1136            self.free_chunk_slots.push(rhs_chunk_key);
1137            debug_assert!(chunk.previous == Some(lhs_chunk_key));
1138
1139            self.remove_from_free_list(rhs_chunk_key, chunk.size)?;
1140
1141            (chunk.size, chunk.offset, chunk.next)
1142        };
1143
1144        let lhs_previous_key = self.chunks[lhs_chunk_key.get()]
1145            .as_mut()
1146            .expect("can't find chunk in chunk list")
1147            .previous;
1148
1149        let lhs_offset = if let Some(lhs_previous_key) = lhs_previous_key {
1150            let lhs_previous = self.chunks[lhs_previous_key.get()]
1151                .as_mut()
1152                .expect("can't find chunk in chunk list");
1153            lhs_previous.offset + lhs_previous.size
1154        } else {
1155            0
1156        };
1157
1158        let lhs_chunk = self.chunks[lhs_chunk_key.get()]
1159            .as_mut()
1160            .expect("can't find chunk in chunk list");
1161
1162        debug_assert!(lhs_chunk.next == Some(rhs_chunk_key));
1163
1164        let old_size = lhs_chunk.size;
1165
1166        lhs_chunk.next = rhs_next;
1167        lhs_chunk.size = (rhs_offset + rhs_size) - lhs_offset;
1168        lhs_chunk.offset = lhs_offset;
1169
1170        let new_size = lhs_chunk.size;
1171
1172        self.remove_from_free_list(lhs_chunk_key, old_size)?;
1173        self.add_to_free_list(lhs_chunk_key, new_size)?;
1174
1175        if let Some(rhs_next) = rhs_next {
1176            let chunk = self.chunks[rhs_next.get()]
1177                .as_mut()
1178                .expect("previous memory chunk was None");
1179            chunk.previous = Some(lhs_chunk_key);
1180        }
1181
1182        Ok(())
1183    }
1184
1185    #[cfg_attr(feature = "profiling", profiling::function)]
1186    unsafe fn free_block(
1187        &mut self,
1188        device: &erupt::DeviceLoader,
1189        block_key: NonZeroUsize,
1190    ) -> Result<()> {
1191        let mut block = self.blocks[block_key.get()]
1192            .take()
1193            .ok_or(AllocatorError::CantFindBlock)?;
1194
1195        block.destroy(device);
1196
1197        self.free_block_slots.push(block_key);
1198
1199        Ok(())
1200    }
1201
1202    #[cfg_attr(feature = "profiling", profiling::function)]
1203    fn add_to_free_list(&mut self, chunk_key: NonZeroUsize, size: vk::DeviceSize) -> Result<()> {
1204        let chunk_bucket_index: usize = calculate_bucket_index(size).try_into()?;
1205        self.free_chunks[chunk_bucket_index].push(chunk_key);
1206        Ok(())
1207    }
1208
1209    #[cfg_attr(feature = "profiling", profiling::function)]
1210    fn remove_from_free_list(
1211        &mut self,
1212        chunk_key: NonZeroUsize,
1213        chunk_size: vk::DeviceSize,
1214    ) -> Result<()> {
1215        let bucket_index: usize = calculate_bucket_index(chunk_size).try_into()?;
1216        let free_list_index = self.free_chunks[bucket_index]
1217            .iter()
1218            .enumerate()
1219            .find(|(_, key)| **key == chunk_key)
1220            .map(|(index, _)| index)
1221            .expect("can't find chunk in chunk list");
1222        self.free_chunks[bucket_index].remove(free_list_index);
1223        Ok(())
1224    }
1225}
1226
1227/// A chunk inside a memory block. Next = None is the start chunk. Previous = None is the end chunk.
1228#[derive(Clone, Debug)]
1229struct MemoryChunk {
1230    block_key: NonZeroUsize,
1231    size: vk::DeviceSize,
1232    offset: vk::DeviceSize,
1233    previous: Option<NonZeroUsize>,
1234    next: Option<NonZeroUsize>,
1235    chunk_type: ChunkType,
1236}
1237
1238/// A reserved memory block.
1239#[derive(Debug)]
1240struct MemoryBlock {
1241    device_memory: vk::DeviceMemory,
1242    size: vk::DeviceSize,
1243    mapped_ptr: *mut c_void,
1244    is_dedicated: bool,
1245}
1246
1247unsafe impl Send for MemoryBlock {}
1248
1249impl MemoryBlock {
1250    #[cfg_attr(feature = "profiling", profiling::function)]
1251    unsafe fn new(
1252        device: &erupt::DeviceLoader,
1253        size: vk::DeviceSize,
1254        memory_type_index: u32,
1255        is_mappable: bool,
1256        is_dedicated: bool,
1257    ) -> Result<Self> {
1258        #[cfg(feature = "vk-buffer-device-address")]
1259        let device_memory = {
1260            let alloc_info = vk::MemoryAllocateInfoBuilder::new()
1261                .allocation_size(size)
1262                .memory_type_index(memory_type_index);
1263
1264            let allocation_flags = vk::MemoryAllocateFlags::DEVICE_ADDRESS;
1265            let mut flags_info = vk::MemoryAllocateFlagsInfoBuilder::new().flags(allocation_flags);
1266            let alloc_info = alloc_info.extend_from(&mut flags_info);
1267
1268            device
1269                .allocate_memory(&alloc_info, None)
1270                .map_err(|_| AllocatorError::OutOfMemory)?
1271        };
1272
1273        #[cfg(not(feature = "vk-buffer-device-address"))]
1274        let device_memory = {
1275            let alloc_info = vk::MemoryAllocateInfoBuilder::new()
1276                .allocation_size(size)
1277                .memory_type_index(memory_type_index);
1278
1279            device
1280                .allocate_memory(&alloc_info, None)
1281                .map_err(|_| AllocatorError::OutOfMemory)?
1282        };
1283
1284        let mapped_ptr = if is_mappable {
1285            let mapped_ptr = device.map_memory(
1286                device_memory,
1287                0,
1288                vk::WHOLE_SIZE,
1289                vk::MemoryMapFlags::empty(),
1290            );
1291
1292            match mapped_ptr.ok() {
1293                Some(mapped_ptr) => mapped_ptr,
1294                None => {
1295                    device.free_memory(device_memory, None);
1296                    return Err(AllocatorError::FailedToMap);
1297                }
1298            }
1299        } else {
1300            ptr::null_mut()
1301        };
1302
1303        Ok(Self {
1304            device_memory,
1305            size,
1306            mapped_ptr,
1307            is_dedicated,
1308        })
1309    }
1310
1311    #[cfg_attr(feature = "profiling", profiling::function)]
1312    unsafe fn destroy(&mut self, device: &erupt::DeviceLoader) {
1313        if !self.mapped_ptr.is_null() {
1314            device.unmap_memory(self.device_memory);
1315        }
1316        device.free_memory(self.device_memory, None);
1317        self.device_memory = vk::DeviceMemory::null()
1318    }
1319}
1320
1321#[inline]
1322fn align_up(offset: vk::DeviceSize, alignment: vk::DeviceSize) -> vk::DeviceSize {
1323    (offset + (alignment - 1)) & !(alignment - 1)
1324}
1325
1326#[inline]
1327fn align_down(offset: vk::DeviceSize, alignment: vk::DeviceSize) -> vk::DeviceSize {
1328    offset & !(alignment - 1)
1329}
1330
1331fn is_on_same_page(offset_a: u64, size_a: u64, offset_b: u64, page_size: u64) -> bool {
1332    let end_a = offset_a + size_a - 1;
1333    let end_page_a = align_down(end_a, page_size);
1334    let start_b = offset_b;
1335    let start_page_b = align_down(start_b, page_size);
1336
1337    end_page_a == start_page_b
1338}
1339
1340#[cfg_attr(feature = "profiling", profiling::function)]
1341unsafe fn query_driver(
1342    instance: &erupt::InstanceLoader,
1343    physical_device: vk::PhysicalDevice,
1344) -> (vk::DriverId, bool, u64) {
1345    let mut vulkan_12_properties = vk::PhysicalDeviceVulkan12Properties::default();
1346    let physical_device_properties =
1347        vk::PhysicalDeviceProperties2Builder::new().extend_from(&mut vulkan_12_properties);
1348
1349    let physical_device_properties = instance.get_physical_device_properties2(
1350        physical_device,
1351        Some(physical_device_properties.build_dangling()),
1352    );
1353    let is_integrated =
1354        physical_device_properties.properties.device_type == vk::PhysicalDeviceType::INTEGRATED_GPU;
1355
1356    let buffer_image_granularity = physical_device_properties
1357        .properties
1358        .limits
1359        .buffer_image_granularity;
1360
1361    (
1362        vulkan_12_properties.driver_id,
1363        is_integrated,
1364        buffer_image_granularity,
1365    )
1366}
1367
1368#[inline]
1369fn memory_type_is_compatible(memory_type_index: usize, memory_type_bits: u32) -> bool {
1370    (1 << memory_type_index) & memory_type_bits != 0
1371}
1372
1373#[cfg(feature = "tracing")]
1374fn print_memory_types(
1375    memory_properties: vk::PhysicalDeviceMemoryProperties,
1376    memory_types: &[vk::MemoryType],
1377) -> Result<()> {
1378    info!("Physical device memory heaps:");
1379    for heap_index in 0..memory_properties.memory_heap_count {
1380        let index: usize = heap_index.try_into()?;
1381        info!(
1382            "Heap {}: {:?}",
1383            heap_index, memory_properties.memory_heaps[index].flags
1384        );
1385        info!(
1386            "\tSize = {} MiB",
1387            memory_properties.memory_heaps[index].size / (1024 * 1024)
1388        );
1389        for (type_index, memory_type) in memory_types
1390            .iter()
1391            .enumerate()
1392            .filter(|(_, t)| t.heap_index == heap_index)
1393        {
1394            info!("\tType {}: {:?}", type_index, memory_type.property_flags);
1395        }
1396    }
1397    Ok(())
1398}
1399
1400#[inline]
1401fn calculate_bucket_index(size: vk::DeviceSize) -> u32 {
1402    if size <= 256 {
1403        0
1404    } else {
1405        64 - MINIMAL_BUCKET_SIZE_LOG2 - (size - 1).leading_zeros() - 1
1406    }
1407}
1408
1409#[inline]
1410fn collect_start_chunks(pool: &Mutex<MemoryPool>) -> Vec<NonZeroUsize> {
1411    pool.lock()
1412        .chunks
1413        .iter()
1414        .enumerate()
1415        .filter(|(_, chunk)| {
1416            if let Some(chunk) = chunk {
1417                chunk.previous.is_none()
1418            } else {
1419                false
1420            }
1421        })
1422        .map(|(id, _)| NonZeroUsize::new(id).expect("id was zero"))
1423        .collect()
1424}