Skip to main content

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    {
131        self.alloc_internal(device, request, None)
132    }
133
134    /// Allocates memory block from specified `device` according to the `request`.
135    /// This function allows user to force specific allocation strategy.
136    /// Improper use can lead to suboptimal performance or too large overhead.
137    /// Prefer `GpuAllocator::alloc` if doubt.
138    ///
139    /// # Safety
140    ///
141    /// * `device` must be one with `DeviceProperties` that were provided to create this `GpuAllocator` instance.
142    /// * Same `device` instance must be used for all interactions with one `GpuAllocator` instance
143    ///   and memory blocks allocated from it.
144    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, device)))]
145    pub unsafe fn alloc_with_dedicated(
146        &mut self,
147        device: &impl MemoryDevice<M>,
148        request: Request,
149        dedicated: Dedicated,
150    ) -> Result<MemoryBlock<M>, AllocationError>
151    {
152        self.alloc_internal(device, request, Some(dedicated))
153    }
154
155    unsafe fn alloc_internal(
156        &mut self,
157        device: &impl MemoryDevice<M>,
158        mut request: Request,
159        dedicated: Option<Dedicated>,
160    ) -> Result<MemoryBlock<M>, AllocationError> {
161        enum Strategy {
162            Buddy,
163            Dedicated,
164            FreeList,
165        }
166
167        request.usage = with_implicit_usage_flags(request.usage);
168
169        if request.usage.contains(UsageFlags::DEVICE_ADDRESS) {
170            assert!(self.buffer_device_address, "`DEVICE_ADDRESS` cannot be requested when `DeviceProperties::buffer_device_address` is false");
171        }
172
173        if request.size > self.max_memory_allocation_size {
174            return Err(AllocationError::OutOfDeviceMemory);
175        }
176
177        if let Some(Dedicated::Required) = dedicated {
178            if self.allocations_remains == 0 {
179                return Err(AllocationError::TooManyObjects);
180            }
181        }
182
183        if 0 == self.memory_for_usage.mask(request.usage) & request.memory_types {
184            #[cfg(feature = "tracing")]
185            tracing::error!(
186                "Cannot serve request {:?}, no memory among bitset `{}` support usage {:?}",
187                request,
188                request.memory_types,
189                request.usage
190            );
191
192            return Err(AllocationError::NoCompatibleMemoryTypes);
193        }
194
195        let transient = request.usage.contains(UsageFlags::TRANSIENT);
196
197        for &index in self.memory_for_usage.types(request.usage) {
198            if 0 == request.memory_types & (1 << index) {
199                // Skip memory type incompatible with the request.
200                continue;
201            }
202
203            let memory_type = &self.memory_types[index as usize];
204            let heap = memory_type.heap;
205            let heap = &mut self.memory_heaps[heap as usize];
206
207            if request.size > heap.size() {
208                // Impossible to use memory type from this heap.
209                continue;
210            }
211
212            let atom_mask = if host_visible_non_coherent(memory_type.props) {
213                self.non_coherent_atom_mask
214            } else {
215                0
216            };
217
218            let flags = if self.buffer_device_address {
219                AllocationFlags::DEVICE_ADDRESS
220            } else {
221                AllocationFlags::empty()
222            };
223
224            let strategy = match (dedicated, transient) {
225                (Some(Dedicated::Required), _) => Strategy::Dedicated,
226                (Some(Dedicated::Preferred), _)
227                    if request.size >= self.preferred_dedicated_threshold =>
228                {
229                    Strategy::Dedicated
230                }
231                (_, true) => {
232                    let threshold = self.transient_dedicated_threshold.min(heap.size() / 32);
233
234                    if request.size < threshold {
235                        Strategy::FreeList
236                    } else {
237                        Strategy::Dedicated
238                    }
239                }
240                (_, false) => {
241                    let threshold = self.dedicated_threshold.min(heap.size() / 32);
242
243                    if request.size < threshold {
244                        Strategy::Buddy
245                    } else {
246                        Strategy::Dedicated
247                    }
248                }
249            };
250
251            match strategy {
252                Strategy::Dedicated => {
253                    #[cfg(feature = "tracing")]
254                    tracing::debug!(
255                        "Allocating memory object `{}@{:?}`",
256                        request.size,
257                        memory_type
258                    );
259
260                    match device.allocate_memory(request.size, index, flags) {
261                        Ok(memory) => {
262                            self.allocations_remains -= 1;
263                            heap.alloc(request.size);
264
265                            return Ok(MemoryBlock::new(
266                                index,
267                                memory_type.props,
268                                0,
269                                request.size,
270                                atom_mask,
271                                MemoryBlockFlavor::Dedicated { memory },
272                            ));
273                        }
274                        Err(OutOfMemory::OutOfDeviceMemory) => continue,
275                        Err(OutOfMemory::OutOfHostMemory) => {
276                            return Err(AllocationError::OutOfHostMemory)
277                        }
278                    }
279                }
280                Strategy::FreeList => {
281                    let allocator = match &mut self.freelist_allocators[index as usize] {
282                        Some(allocator) => allocator,
283                        slot => {
284                            let starting_free_list_chunk = match align_down(
285                                self.starting_free_list_chunk.min(heap.size() / 32),
286                                atom_mask,
287                            ) {
288                                0 => atom_mask,
289                                other => other,
290                            };
291
292                            let final_free_list_chunk = match align_down(
293                                self.final_free_list_chunk
294                                    .max(self.starting_free_list_chunk)
295                                    .max(self.transient_dedicated_threshold)
296                                    .min(heap.size() / 32),
297                                atom_mask,
298                            ) {
299                                0 => atom_mask,
300                                other => other,
301                            };
302
303                            slot.get_or_insert(FreeListAllocator::new(
304                                starting_free_list_chunk,
305                                final_free_list_chunk,
306                                index,
307                                memory_type.props,
308                                if host_visible_non_coherent(memory_type.props) {
309                                    self.non_coherent_atom_mask
310                                } else {
311                                    0
312                                },
313                            ))
314                        }
315                    };
316                    let result = allocator.alloc(
317                        device,
318                        request.size,
319                        request.align_mask,
320                        flags,
321                        heap,
322                        &mut self.allocations_remains,
323                    );
324
325                    match result {
326                        Ok(block) => {
327                            return Ok(MemoryBlock::new(
328                                index,
329                                memory_type.props,
330                                block.offset,
331                                block.size,
332                                atom_mask,
333                                MemoryBlockFlavor::FreeList {
334                                    chunk: block.chunk,
335                                    ptr: block.ptr,
336                                    memory: block.memory,
337                                },
338                            ))
339                        }
340                        Err(AllocationError::OutOfDeviceMemory) => continue,
341                        Err(err) => return Err(err),
342                    }
343                }
344
345                Strategy::Buddy => {
346                    let allocator = match &mut self.buddy_allocators[index as usize] {
347                        Some(allocator) => allocator,
348                        slot => {
349                            let minimal_buddy_size = self
350                                .minimal_buddy_size
351                                .min(heap.size() / 1024)
352                                .next_power_of_two();
353
354                            let initial_buddy_dedicated_size = self
355                                .initial_buddy_dedicated_size
356                                .min(heap.size() / 32)
357                                .next_power_of_two();
358
359                            slot.get_or_insert(BuddyAllocator::new(
360                                minimal_buddy_size,
361                                initial_buddy_dedicated_size,
362                                index,
363                                memory_type.props,
364                                if host_visible_non_coherent(memory_type.props) {
365                                    self.non_coherent_atom_mask
366                                } else {
367                                    0
368                                },
369                            ))
370                        }
371                    };
372                    let result = allocator.alloc(
373                        device,
374                        request.size,
375                        request.align_mask,
376                        flags,
377                        heap,
378                        &mut self.allocations_remains,
379                    );
380
381                    match result {
382                        Ok(block) => {
383                            return Ok(MemoryBlock::new(
384                                index,
385                                memory_type.props,
386                                block.offset,
387                                block.size,
388                                atom_mask,
389                                MemoryBlockFlavor::Buddy {
390                                    chunk: block.chunk,
391                                    ptr: block.ptr,
392                                    index: block.index,
393                                    memory: block.memory,
394                                },
395                            ))
396                        }
397                        Err(AllocationError::OutOfDeviceMemory) => continue,
398                        Err(err) => return Err(err),
399                    }
400                }
401            }
402        }
403
404        Err(AllocationError::OutOfDeviceMemory)
405    }
406
407    /// Creates a memory block from an existing memory allocation, transferring ownership to the allocator.
408    ///
409    /// This function allows the [`GpuAllocator`] to manage memory allocated outside of the typical
410    /// [`GpuAllocator::alloc`] family of functions.
411    ///
412    /// # Usage
413    ///
414    /// If you need to import external memory, such as a Win32 `HANDLE` or a Linux `dmabuf`, import the device
415    /// memory using the graphics api and platform dependent functions. Once that is done, call this function
416    /// to make the [`GpuAllocator`] take ownership of the imported memory.
417    ///
418    /// When calling this function, you **must** ensure there are [enough remaining allocations](GpuAllocator::remaining_allocations).
419    ///
420    /// # Safety
421    ///
422    /// - The `memory` must be allocated with the same device that was provided to create this [`GpuAllocator`]
423    ///   instance.
424    /// - The `memory` must be valid.
425    /// - The `props`, `offset` and `size` must match the properties, offset and size of the memory allocation.
426    /// - The memory must have been allocated with the specified `memory_type`.
427    /// - There must be enough remaining allocations.
428    /// - The memory allocation must not come from an existing memory block created by this allocator.
429    /// - The underlying memory object must be deallocated using the returned [`MemoryBlock`] with
430    ///   [`GpuAllocator::dealloc`].
431    pub unsafe fn import_memory(
432        &mut self,
433        memory: M,
434        memory_type: u32,
435        props: MemoryPropertyFlags,
436        offset: u64,
437        size: u64,
438    ) -> MemoryBlock<M> {
439        // Get the heap which the imported memory is from.
440        let heap = self
441            .memory_types
442            .get(memory_type as usize)
443            .expect("Invalid memory type specified when importing memory")
444            .heap;
445        let heap = &mut self.memory_heaps[heap as usize];
446
447        #[cfg(feature = "tracing")]
448        tracing::debug!(
449            "Importing memory object {:?} `{}@{:?}`",
450            memory,
451            size,
452            memory_type
453        );
454
455        assert_ne!(
456            self.allocations_remains, 0,
457            "Out of allocations when importing a memory block. Ensure you check GpuAllocator::remaining_allocations before import."
458        );
459        self.allocations_remains -= 1;
460
461        let atom_mask = if host_visible_non_coherent(props) {
462            self.non_coherent_atom_mask
463        } else {
464            0
465        };
466
467        heap.alloc(size);
468
469        MemoryBlock::new(
470            memory_type,
471            props,
472            offset,
473            size,
474            atom_mask,
475            MemoryBlockFlavor::Dedicated { memory },
476        )
477    }
478
479    /// Deallocates memory block previously allocated from this `GpuAllocator` instance.
480    ///
481    /// # Safety
482    ///
483    /// * Memory block must have been allocated by this `GpuAllocator` instance
484    /// * `device` must be one with `DeviceProperties` that were provided to create this `GpuAllocator` instance
485    /// * Same `device` instance must be used for all interactions with one `GpuAllocator` instance
486    ///   and memory blocks allocated from it
487    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, device)))]
488    pub unsafe fn dealloc(&mut self, device: &impl MemoryDevice<M>, block: MemoryBlock<M>)
489    {
490        let memory_type = block.memory_type();
491        let offset = block.offset();
492        let size = block.size();
493        let flavor = block.deallocate();
494        match flavor {
495            MemoryBlockFlavor::Dedicated { memory } => {
496                let heap = self.memory_types[memory_type as usize].heap;
497                device.deallocate_memory(memory);
498                self.allocations_remains += 1;
499                self.memory_heaps[heap as usize].dealloc(size);
500            }
501            MemoryBlockFlavor::Buddy {
502                chunk,
503                ptr,
504                index,
505                memory,
506            } => {
507                let heap = self.memory_types[memory_type as usize].heap;
508                let heap = &mut self.memory_heaps[heap as usize];
509
510                let allocator = self.buddy_allocators[memory_type as usize]
511                    .as_mut()
512                    .expect("Allocator should exist");
513
514                allocator.dealloc(
515                    device,
516                    BuddyBlock {
517                        memory,
518                        ptr,
519                        offset,
520                        size,
521                        chunk,
522                        index,
523                    },
524                    heap,
525                    &mut self.allocations_remains,
526                );
527            }
528            MemoryBlockFlavor::FreeList { chunk, ptr, memory } => {
529                let heap = self.memory_types[memory_type as usize].heap;
530                let heap = &mut self.memory_heaps[heap as usize];
531
532                let allocator = self.freelist_allocators[memory_type as usize]
533                    .as_mut()
534                    .expect("Allocator should exist");
535
536                allocator.dealloc(
537                    device,
538                    FreeListBlock {
539                        memory,
540                        ptr,
541                        chunk,
542                        offset,
543                        size,
544                    },
545                    heap,
546                    &mut self.allocations_remains,
547                );
548            }
549        }
550    }
551
552    /// Returns the maximum allocation size supported.
553    pub fn max_allocation_size(&self) -> u64 {
554        self.max_memory_allocation_size
555    }
556
557    /// Returns the number of remaining available allocations.
558    ///
559    /// This may be useful if you need know if the allocator can allocate a number of allocations ahead of
560    /// time. This function is also useful for ensuring you do not allocate too much memory outside allocator
561    /// (such as external memory).
562    pub fn remaining_allocations(&self) -> u32 {
563        self.allocations_remains
564    }
565
566    /// Sets the number of remaining available allocations.
567    ///
568    /// # Safety
569    ///
570    /// The caller is responsible for ensuring the number of remaining allocations does not exceed how many
571    /// remaining allocations there actually are on the memory device.
572    pub unsafe fn set_remaining_allocations(&mut self, remaining: u32) {
573        self.allocations_remains = remaining;
574    }
575
576    /// Deallocates leftover memory objects.
577    /// Should be used before dropping.
578    ///
579    /// # Safety
580    ///
581    /// * `device` must be one with `DeviceProperties` that were provided to create this `GpuAllocator` instance
582    /// * Same `device` instance must be used for all interactions with one `GpuAllocator` instance
583    ///   and memory blocks allocated from it
584    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, device)))]
585    pub unsafe fn cleanup(&mut self, device: &impl MemoryDevice<M>)
586    {
587        for (index, allocator) in self
588            .freelist_allocators
589            .iter_mut()
590            .enumerate()
591            .filter_map(|(index, allocator)| Some((index, allocator.as_mut()?)))
592        {
593            let memory_type = &self.memory_types[index];
594            let heap = memory_type.heap;
595            let heap = &mut self.memory_heaps[heap as usize];
596
597            allocator.cleanup(device, heap, &mut self.allocations_remains);
598        }
599    }
600}
601
602fn host_visible_non_coherent(props: MemoryPropertyFlags) -> bool {
603    (props & (MemoryPropertyFlags::HOST_COHERENT | MemoryPropertyFlags::HOST_VISIBLE))
604        == MemoryPropertyFlags::HOST_VISIBLE
605}
606
607fn with_implicit_usage_flags(usage: UsageFlags) -> UsageFlags {
608    if usage.is_empty() {
609        UsageFlags::FAST_DEVICE_ACCESS
610    } else if usage.intersects(UsageFlags::DOWNLOAD | UsageFlags::UPLOAD) {
611        usage | UsageFlags::HOST_ACCESS
612    } else {
613        usage
614    }
615}