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