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