gpu_alloc/
allocator.rs

1use {
2    crate::{
3        align_down,
4        block::{MemoryBlock, MemoryBlockFlavor},
5        buddy::{BuddyAllocator, BuddyBlock},
6        config::Config,
7        error::AllocationError,
8        freelist::{FreeListAllocator, FreeListBlock},
9        heap::Heap,
10        usage::{MemoryForUsage, UsageFlags},
11        MemoryBounds, Request,
12    },
13    alloc::boxed::Box,
14    core::convert::TryFrom as _,
15    gpu_alloc_types::{
16        AllocationFlags, DeviceProperties, MemoryDevice, MemoryPropertyFlags, MemoryType,
17        OutOfMemory,
18    },
19};
20
21/// Memory allocator for Vulkan-like APIs.
22#[derive(Debug)]
23pub struct GpuAllocator<M> {
24    dedicated_threshold: u64,
25    preferred_dedicated_threshold: u64,
26    transient_dedicated_threshold: u64,
27    max_memory_allocation_size: u64,
28    memory_for_usage: MemoryForUsage,
29    memory_types: Box<[MemoryType]>,
30    memory_heaps: Box<[Heap]>,
31    allocations_remains: u32,
32    non_coherent_atom_mask: u64,
33    starting_free_list_chunk: u64,
34    final_free_list_chunk: u64,
35    minimal_buddy_size: u64,
36    initial_buddy_dedicated_size: u64,
37    buffer_device_address: bool,
38
39    buddy_allocators: Box<[Option<BuddyAllocator<M>>]>,
40    freelist_allocators: Box<[Option<FreeListAllocator<M>>]>,
41}
42
43/// Hints for allocator to decide on allocation strategy.
44#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
45#[non_exhaustive]
46pub enum Dedicated {
47    /// Allocation directly from device.\
48    /// Very slow.
49    /// Count of allocations is limited.\
50    /// Use with caution.\
51    /// Must be used if resource has to be bound to dedicated memory object.
52    Required,
53
54    /// Hint for allocator that dedicated memory object is preferred.\
55    /// Should be used if it is known that resource placed in dedicated memory object
56    /// would allow for better performance.\
57    /// Implementation is allowed to return block to shared memory object.
58    Preferred,
59}
60
61impl<M> GpuAllocator<M>
62where
63    M: MemoryBounds + 'static,
64{
65    /// Creates  new instance of `GpuAllocator`.
66    /// Provided `DeviceProperties` should match properties of `MemoryDevice` that will be used
67    /// with created `GpuAllocator` instance.
68    #[cfg_attr(feature = "tracing", tracing::instrument)]
69    pub fn new(config: Config, props: DeviceProperties<'_>) -> Self {
70        assert!(
71            props.non_coherent_atom_size.is_power_of_two(),
72            "`non_coherent_atom_size` must be power of two"
73        );
74
75        assert!(
76            isize::try_from(props.non_coherent_atom_size).is_ok(),
77            "`non_coherent_atom_size` must fit host address space"
78        );
79
80        GpuAllocator {
81            dedicated_threshold: config.dedicated_threshold,
82            preferred_dedicated_threshold: config
83                .preferred_dedicated_threshold
84                .min(config.dedicated_threshold),
85
86            transient_dedicated_threshold: config
87                .transient_dedicated_threshold
88                .max(config.dedicated_threshold),
89
90            max_memory_allocation_size: props.max_memory_allocation_size,
91
92            memory_for_usage: MemoryForUsage::new(props.memory_types.as_ref()),
93
94            memory_types: props.memory_types.as_ref().iter().copied().collect(),
95            memory_heaps: props
96                .memory_heaps
97                .as_ref()
98                .iter()
99                .map(|heap| Heap::new(heap.size))
100                .collect(),
101
102            buffer_device_address: props.buffer_device_address,
103
104            allocations_remains: props.max_memory_allocation_count,
105            non_coherent_atom_mask: props.non_coherent_atom_size - 1,
106
107            starting_free_list_chunk: config.starting_free_list_chunk,
108            final_free_list_chunk: config.final_free_list_chunk,
109            minimal_buddy_size: config.minimal_buddy_size,
110            initial_buddy_dedicated_size: config.initial_buddy_dedicated_size,
111
112            buddy_allocators: props.memory_types.as_ref().iter().map(|_| None).collect(),
113            freelist_allocators: props.memory_types.as_ref().iter().map(|_| None).collect(),
114        }
115    }
116
117    /// Allocates memory block from specified `device` according to the `request`.
118    ///
119    /// # Safety
120    ///
121    /// * `device` must be one with `DeviceProperties` that were provided to create this `GpuAllocator` instance.
122    /// * Same `device` instance must be used for all interactions with one `GpuAllocator` instance
123    ///   and memory blocks allocated from it.
124    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, device)))]
125    pub unsafe fn alloc(
126        &mut self,
127        device: &impl MemoryDevice<M>,
128        request: Request,
129    ) -> Result<MemoryBlock<M>, AllocationError> {
130        self.alloc_internal(device, request, None)
131    }
132
133    /// Allocates memory block from specified `device` according to the `request`.
134    /// This function allows user to force specific allocation strategy.
135    /// Improper use can lead to suboptimal performance or too large overhead.
136    /// Prefer `GpuAllocator::alloc` if doubt.
137    ///
138    /// # Safety
139    ///
140    /// * `device` must be one with `DeviceProperties` that were provided to create this `GpuAllocator` instance.
141    /// * Same `device` instance must be used for all interactions with one `GpuAllocator` instance
142    ///   and memory blocks allocated from it.
143    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, device)))]
144    pub unsafe fn alloc_with_dedicated(
145        &mut self,
146        device: &impl MemoryDevice<M>,
147        request: Request,
148        dedicated: Dedicated,
149    ) -> Result<MemoryBlock<M>, AllocationError> {
150        self.alloc_internal(device, request, Some(dedicated))
151    }
152
153    unsafe fn alloc_internal(
154        &mut self,
155        device: &impl MemoryDevice<M>,
156        mut request: Request,
157        dedicated: Option<Dedicated>,
158    ) -> Result<MemoryBlock<M>, AllocationError> {
159        enum Strategy {
160            Buddy,
161            Dedicated,
162            FreeList,
163        }
164
165        request.usage = with_implicit_usage_flags(request.usage);
166
167        if request.usage.contains(UsageFlags::DEVICE_ADDRESS) {
168            assert!(self.buffer_device_address, "`DEVICE_ADDRESS` cannot be requested when `DeviceProperties::buffer_device_address` is false");
169        }
170
171        if request.size > self.max_memory_allocation_size {
172            return Err(AllocationError::OutOfDeviceMemory);
173        }
174
175        if let Some(Dedicated::Required) = dedicated {
176            if self.allocations_remains == 0 {
177                return Err(AllocationError::TooManyObjects);
178            }
179        }
180
181        if 0 == self.memory_for_usage.mask(request.usage) & request.memory_types {
182            #[cfg(feature = "tracing")]
183            tracing::error!(
184                "Cannot serve request {:?}, no memory among bitset `{}` support usage {:?}",
185                request,
186                request.memory_types,
187                request.usage
188            );
189
190            return Err(AllocationError::NoCompatibleMemoryTypes);
191        }
192
193        let transient = request.usage.contains(UsageFlags::TRANSIENT);
194
195        for &index in self.memory_for_usage.types(request.usage) {
196            if 0 == request.memory_types & (1 << index) {
197                // Skip memory type incompatible with the request.
198                continue;
199            }
200
201            let memory_type = &self.memory_types[index as usize];
202            let heap = memory_type.heap;
203            let heap = &mut self.memory_heaps[heap as usize];
204
205            if request.size > heap.size() {
206                // Impossible to use memory type from this heap.
207                continue;
208            }
209
210            let atom_mask = if host_visible_non_coherent(memory_type.props) {
211                self.non_coherent_atom_mask
212            } else {
213                0
214            };
215
216            let flags = if self.buffer_device_address {
217                AllocationFlags::DEVICE_ADDRESS
218            } else {
219                AllocationFlags::empty()
220            };
221
222            let strategy = match (dedicated, transient) {
223                (Some(Dedicated::Required), _) => Strategy::Dedicated,
224                (Some(Dedicated::Preferred), _)
225                    if request.size >= self.preferred_dedicated_threshold =>
226                {
227                    Strategy::Dedicated
228                }
229                (_, true) => {
230                    let threshold = self.transient_dedicated_threshold.min(heap.size() / 32);
231
232                    if request.size < threshold {
233                        Strategy::FreeList
234                    } else {
235                        Strategy::Dedicated
236                    }
237                }
238                (_, false) => {
239                    let threshold = self.dedicated_threshold.min(heap.size() / 32);
240
241                    if request.size < threshold {
242                        Strategy::Buddy
243                    } else {
244                        Strategy::Dedicated
245                    }
246                }
247            };
248
249            match strategy {
250                Strategy::Dedicated => {
251                    #[cfg(feature = "tracing")]
252                    tracing::debug!(
253                        "Allocating memory object `{}@{:?}`",
254                        request.size,
255                        memory_type
256                    );
257
258                    match device.allocate_memory(request.size, index, flags) {
259                        Ok(memory) => {
260                            self.allocations_remains -= 1;
261                            heap.alloc(request.size);
262
263                            return Ok(MemoryBlock::new(
264                                index,
265                                memory_type.props,
266                                0,
267                                request.size,
268                                atom_mask,
269                                MemoryBlockFlavor::Dedicated { memory },
270                            ));
271                        }
272                        Err(OutOfMemory::OutOfDeviceMemory) => continue,
273                        Err(OutOfMemory::OutOfHostMemory) => {
274                            return Err(AllocationError::OutOfHostMemory)
275                        }
276                    }
277                }
278                Strategy::FreeList => {
279                    let allocator = match &mut self.freelist_allocators[index as usize] {
280                        Some(allocator) => allocator,
281                        slot => {
282                            let starting_free_list_chunk = match align_down(
283                                self.starting_free_list_chunk.min(heap.size() / 32),
284                                atom_mask,
285                            ) {
286                                0 => atom_mask,
287                                other => other,
288                            };
289
290                            let final_free_list_chunk = match align_down(
291                                self.final_free_list_chunk
292                                    .max(self.starting_free_list_chunk)
293                                    .max(self.transient_dedicated_threshold)
294                                    .min(heap.size() / 32),
295                                atom_mask,
296                            ) {
297                                0 => atom_mask,
298                                other => other,
299                            };
300
301                            slot.get_or_insert(FreeListAllocator::new(
302                                starting_free_list_chunk,
303                                final_free_list_chunk,
304                                index,
305                                memory_type.props,
306                                if host_visible_non_coherent(memory_type.props) {
307                                    self.non_coherent_atom_mask
308                                } else {
309                                    0
310                                },
311                            ))
312                        }
313                    };
314                    let result = allocator.alloc(
315                        device,
316                        request.size,
317                        request.align_mask,
318                        flags,
319                        heap,
320                        &mut self.allocations_remains,
321                    );
322
323                    match result {
324                        Ok(block) => {
325                            return Ok(MemoryBlock::new(
326                                index,
327                                memory_type.props,
328                                block.offset,
329                                block.size,
330                                atom_mask,
331                                MemoryBlockFlavor::FreeList {
332                                    chunk: block.chunk,
333                                    ptr: block.ptr,
334                                    memory: block.memory,
335                                },
336                            ))
337                        }
338                        Err(AllocationError::OutOfDeviceMemory) => continue,
339                        Err(err) => return Err(err),
340                    }
341                }
342
343                Strategy::Buddy => {
344                    let allocator = match &mut self.buddy_allocators[index as usize] {
345                        Some(allocator) => allocator,
346                        slot => {
347                            let minimal_buddy_size = self
348                                .minimal_buddy_size
349                                .min(heap.size() / 1024)
350                                .next_power_of_two();
351
352                            let initial_buddy_dedicated_size = self
353                                .initial_buddy_dedicated_size
354                                .min(heap.size() / 32)
355                                .next_power_of_two();
356
357                            slot.get_or_insert(BuddyAllocator::new(
358                                minimal_buddy_size,
359                                initial_buddy_dedicated_size,
360                                index,
361                                memory_type.props,
362                                if host_visible_non_coherent(memory_type.props) {
363                                    self.non_coherent_atom_mask
364                                } else {
365                                    0
366                                },
367                            ))
368                        }
369                    };
370                    let result = allocator.alloc(
371                        device,
372                        request.size,
373                        request.align_mask,
374                        flags,
375                        heap,
376                        &mut self.allocations_remains,
377                    );
378
379                    match result {
380                        Ok(block) => {
381                            return Ok(MemoryBlock::new(
382                                index,
383                                memory_type.props,
384                                block.offset,
385                                block.size,
386                                atom_mask,
387                                MemoryBlockFlavor::Buddy {
388                                    chunk: block.chunk,
389                                    ptr: block.ptr,
390                                    index: block.index,
391                                    memory: block.memory,
392                                },
393                            ))
394                        }
395                        Err(AllocationError::OutOfDeviceMemory) => continue,
396                        Err(err) => return Err(err),
397                    }
398                }
399            }
400        }
401
402        Err(AllocationError::OutOfDeviceMemory)
403    }
404
405    /// Creates a memory block from an existing memory allocation, transferring ownership to the allocator.
406    ///
407    /// This function allows the [`GpuAllocator`] to manage memory allocated outside of the typical
408    /// [`GpuAllocator::alloc`] family of functions.
409    ///
410    /// # Usage
411    ///
412    /// If you need to import external memory, such as a Win32 `HANDLE` or a Linux `dmabuf`, import the device
413    /// memory using the graphics api and platform dependent functions. Once that is done, call this function
414    /// to make the [`GpuAllocator`] take ownership of the imported memory.
415    ///
416    /// When calling this function, you **must** ensure there are [enough remaining allocations](GpuAllocator::remaining_allocations).
417    ///
418    /// # Safety
419    ///
420    /// - The `memory` must be allocated with the same device that was provided to create this [`GpuAllocator`]
421    ///   instance.
422    /// - The `memory` must be valid.
423    /// - The `props`, `offset` and `size` must match the properties, offset and size of the memory allocation.
424    /// - The memory must have been allocated with the specified `memory_type`.
425    /// - There must be enough remaining allocations.
426    /// - The memory allocation must not come from an existing memory block created by this allocator.
427    /// - The underlying memory object must be deallocated using the returned [`MemoryBlock`] with
428    ///   [`GpuAllocator::dealloc`].
429    pub unsafe fn import_memory(
430        &mut self,
431        memory: M,
432        memory_type: u32,
433        props: MemoryPropertyFlags,
434        offset: u64,
435        size: u64,
436    ) -> MemoryBlock<M> {
437        // Get the heap which the imported memory is from.
438        let heap = self
439            .memory_types
440            .get(memory_type as usize)
441            .expect("Invalid memory type specified when importing memory")
442            .heap;
443        let heap = &mut self.memory_heaps[heap as usize];
444
445        #[cfg(feature = "tracing")]
446        tracing::debug!(
447            "Importing memory object {:?} `{}@{:?}`",
448            memory,
449            size,
450            memory_type
451        );
452
453        assert_ne!(
454            self.allocations_remains, 0,
455            "Out of allocations when importing a memory block. Ensure you check GpuAllocator::remaining_allocations before import."
456        );
457        self.allocations_remains -= 1;
458
459        let atom_mask = if host_visible_non_coherent(props) {
460            self.non_coherent_atom_mask
461        } else {
462            0
463        };
464
465        heap.alloc(size);
466
467        MemoryBlock::new(
468            memory_type,
469            props,
470            offset,
471            size,
472            atom_mask,
473            MemoryBlockFlavor::Dedicated { memory },
474        )
475    }
476
477    /// Deallocates memory block previously allocated from this `GpuAllocator` instance.
478    ///
479    /// # Safety
480    ///
481    /// * Memory block must have been allocated by this `GpuAllocator` instance
482    /// * `device` must be one with `DeviceProperties` that were provided to create this `GpuAllocator` instance
483    /// * Same `device` instance must be used for all interactions with one `GpuAllocator` instance
484    ///   and memory blocks allocated from it
485    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, device)))]
486    pub unsafe fn dealloc(&mut self, device: &impl MemoryDevice<M>, block: MemoryBlock<M>) {
487        let memory_type = block.memory_type();
488        let offset = block.offset();
489        let size = block.size();
490        let flavor = block.deallocate();
491        match flavor {
492            MemoryBlockFlavor::Dedicated { memory } => {
493                let heap = self.memory_types[memory_type as usize].heap;
494                device.deallocate_memory(memory);
495                self.allocations_remains += 1;
496                self.memory_heaps[heap as usize].dealloc(size);
497            }
498            MemoryBlockFlavor::Buddy {
499                chunk,
500                ptr,
501                index,
502                memory,
503            } => {
504                let heap = self.memory_types[memory_type as usize].heap;
505                let heap = &mut self.memory_heaps[heap as usize];
506
507                let allocator = self.buddy_allocators[memory_type as usize]
508                    .as_mut()
509                    .expect("Allocator should exist");
510
511                allocator.dealloc(
512                    device,
513                    BuddyBlock {
514                        memory,
515                        ptr,
516                        offset,
517                        size,
518                        chunk,
519                        index,
520                    },
521                    heap,
522                    &mut self.allocations_remains,
523                );
524            }
525            MemoryBlockFlavor::FreeList { chunk, ptr, memory } => {
526                let heap = self.memory_types[memory_type as usize].heap;
527                let heap = &mut self.memory_heaps[heap as usize];
528
529                let allocator = self.freelist_allocators[memory_type as usize]
530                    .as_mut()
531                    .expect("Allocator should exist");
532
533                allocator.dealloc(
534                    device,
535                    FreeListBlock {
536                        memory,
537                        ptr,
538                        chunk,
539                        offset,
540                        size,
541                    },
542                    heap,
543                    &mut self.allocations_remains,
544                );
545            }
546        }
547    }
548
549    /// Returns the maximum allocation size supported.
550    pub fn max_allocation_size(&self) -> u64 {
551        self.max_memory_allocation_size
552    }
553
554    /// Returns the number of remaining available allocations.
555    ///
556    /// This may be useful if you need know if the allocator can allocate a number of allocations ahead of
557    /// time. This function is also useful for ensuring you do not allocate too much memory outside allocator
558    /// (such as external memory).
559    pub fn remaining_allocations(&self) -> u32 {
560        self.allocations_remains
561    }
562
563    /// Sets the number of remaining available allocations.
564    ///
565    /// # Safety
566    ///
567    /// The caller is responsible for ensuring the number of remaining allocations does not exceed how many
568    /// remaining allocations there actually are on the memory device.
569    pub unsafe fn set_remaining_allocations(&mut self, remaining: u32) {
570        self.allocations_remains = remaining;
571    }
572
573    /// Deallocates leftover memory objects.
574    /// Should be used before dropping.
575    ///
576    /// # Safety
577    ///
578    /// * `device` must be one with `DeviceProperties` that were provided to create this `GpuAllocator` instance
579    /// * Same `device` instance must be used for all interactions with one `GpuAllocator` instance
580    ///   and memory blocks allocated from it
581    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, device)))]
582    pub unsafe fn cleanup(&mut self, device: &impl MemoryDevice<M>) {
583        for (index, allocator) in self
584            .freelist_allocators
585            .iter_mut()
586            .enumerate()
587            .filter_map(|(index, allocator)| Some((index, allocator.as_mut()?)))
588        {
589            let memory_type = &self.memory_types[index];
590            let heap = memory_type.heap;
591            let heap = &mut self.memory_heaps[heap as usize];
592
593            allocator.cleanup(device, heap, &mut self.allocations_remains);
594        }
595    }
596}
597
598fn host_visible_non_coherent(props: MemoryPropertyFlags) -> bool {
599    (props & (MemoryPropertyFlags::HOST_COHERENT | MemoryPropertyFlags::HOST_VISIBLE))
600        == MemoryPropertyFlags::HOST_VISIBLE
601}
602
603fn with_implicit_usage_flags(usage: UsageFlags) -> UsageFlags {
604    if usage.is_empty() {
605        UsageFlags::FAST_DEVICE_ACCESS
606    } else if usage.intersects(UsageFlags::DOWNLOAD | UsageFlags::UPLOAD) {
607        usage | UsageFlags::HOST_ACCESS
608    } else {
609        usage
610    }
611}