@@ -6,10 +6,12 @@ use std::backtrace::Backtrace;
66
77use log:: debug;
88use objc2:: { rc:: Retained , runtime:: ProtocolObject } ;
9- use objc2_foundation:: NSString ;
9+ use objc2_foundation:: { ns_string, NSString } ;
10+ #[ cfg( doc) ]
11+ use objc2_metal:: { MTLAllocation , MTLResource } ;
1012use objc2_metal:: {
11- MTLCPUCacheMode , MTLDevice , MTLHeap , MTLHeapDescriptor , MTLHeapType , MTLResourceOptions ,
12- MTLStorageMode , MTLTextureDescriptor ,
13+ MTLCPUCacheMode , MTLDevice , MTLHeap , MTLHeapDescriptor , MTLHeapType , MTLResidencySet ,
14+ MTLResourceOptions , MTLStorageMode , MTLTextureDescriptor ,
1315} ;
1416
1517#[ cfg( feature = "visualizer" ) ]
@@ -154,6 +156,7 @@ impl<'a> AllocationCreateDesc<'a> {
154156
155157pub struct Allocator {
156158 device : Retained < ProtocolObject < dyn MTLDevice > > ,
159+ global_residency_set : Option < Retained < ProtocolObject < dyn MTLResidencySet > > > ,
157160 debug_settings : AllocatorDebugSettings ,
158161 memory_types : Vec < MemoryType > ,
159162 allocation_sizes : AllocationSizes ,
@@ -170,6 +173,9 @@ pub struct AllocatorCreateDesc {
170173 pub device : Retained < ProtocolObject < dyn MTLDevice > > ,
171174 pub debug_settings : AllocatorDebugSettings ,
172175 pub allocation_sizes : AllocationSizes ,
176+ /// Whether to create a [`MTLResidencySet`] containing all live heaps, that can be retrieved via
177+ /// [`Allocator::residency_set()`]. Only supported on `MacOS 15.0+` / `iOS 18.0+`.
178+ pub create_residency_set : bool ,
173179}
174180
175181#[ derive( Debug ) ]
@@ -219,6 +225,7 @@ impl MemoryBlock {
219225
220226#[ derive( Debug ) ]
221227struct MemoryType {
228+ global_residency_set : Option < Retained < ProtocolObject < dyn MTLResidencySet > > > ,
222229 memory_blocks : Vec < Option < MemoryBlock > > ,
223230 _committed_allocations : CommittedAllocationStatistics ,
224231 memory_location : MemoryLocation ,
@@ -253,6 +260,10 @@ impl MemoryType {
253260 self . memory_location ,
254261 ) ?;
255262
263+ if let Some ( rs) = & self . global_residency_set {
264+ unsafe { rs. addAllocation ( mem_block. heap . as_ref ( ) ) }
265+ }
266+
256267 let block_index = self . memory_blocks . iter ( ) . position ( |block| block. is_none ( ) ) ;
257268 let block_index = match block_index {
258269 Some ( i) => {
@@ -323,19 +334,23 @@ impl MemoryType {
323334 }
324335 }
325336
326- let new_memory_block = MemoryBlock :: new (
337+ let mem_block = MemoryBlock :: new (
327338 device,
328339 memblock_size,
329340 & self . heap_properties ,
330341 false ,
331342 self . memory_location ,
332343 ) ?;
333344
345+ if let Some ( rs) = & self . global_residency_set {
346+ unsafe { rs. addAllocation ( mem_block. heap . as_ref ( ) ) }
347+ }
348+
334349 let new_block_index = if let Some ( block_index) = empty_block_index {
335- self . memory_blocks [ block_index] = Some ( new_memory_block ) ;
350+ self . memory_blocks [ block_index] = Some ( mem_block ) ;
336351 block_index
337352 } else {
338- self . memory_blocks . push ( Some ( new_memory_block ) ) ;
353+ self . memory_blocks . push ( Some ( mem_block ) ) ;
339354 self . memory_blocks . len ( ) - 1
340355 } ;
341356
@@ -380,28 +395,26 @@ impl MemoryType {
380395
381396 mem_block. sub_allocator . free ( allocation. chunk_id ) ?;
382397
383- if mem_block. sub_allocator . is_empty ( ) {
384- if mem_block. sub_allocator . supports_general_allocations ( ) {
385- if self . active_general_blocks > 1 {
386- let block = self . memory_blocks [ block_idx] . take ( ) ;
387- if block. is_none ( ) {
388- return Err ( AllocationError :: Internal (
389- "Memory block must be Some." . into ( ) ,
390- ) ) ;
391- }
392- // Note that `block` will be destroyed on `drop` here
398+ // We only want to destroy this now-empty block if it is either a dedicated/personal
399+ // allocation, or a block supporting sub-allocations that is not the last one (ensuring
400+ // there's always at least one block/allocator readily available).
401+ let is_dedicated_or_not_last_general_block =
402+ !mem_block. sub_allocator . supports_general_allocations ( )
403+ || self . active_general_blocks > 1 ;
404+ if mem_block. sub_allocator . is_empty ( ) && is_dedicated_or_not_last_general_block {
405+ let block = self . memory_blocks [ block_idx]
406+ . take ( )
407+ . ok_or_else ( || AllocationError :: Internal ( "Memory block must be Some." . into ( ) ) ) ?;
408+
409+ if block. sub_allocator . supports_general_allocations ( ) {
410+ self . active_general_blocks -= 1 ;
411+ }
393412
394- self . active_general_blocks -= 1 ;
395- }
396- } else {
397- let block = self . memory_blocks [ block_idx] . take ( ) ;
398- if block. is_none ( ) {
399- return Err ( AllocationError :: Internal (
400- "Memory block must be Some." . into ( ) ,
401- ) ) ;
402- }
403- // Note that `block` will be destroyed on `drop` here
413+ if let Some ( rs) = & self . global_residency_set {
414+ unsafe { rs. removeAllocation ( block. heap . as_ref ( ) ) }
404415 }
416+
417+ // Note that `block` will be destroyed on `drop` here
405418 }
406419
407420 Ok ( ( ) )
@@ -434,10 +447,23 @@ impl Allocator {
434447 } ) ,
435448 ] ;
436449
450+ let global_residency_set = if desc. create_residency_set {
451+ Some ( unsafe {
452+ let rs_desc = objc2_metal:: MTLResidencySetDescriptor :: new ( ) ;
453+ rs_desc. setLabel ( Some ( ns_string ! ( "gpu-allocator global residency set" ) ) ) ;
454+ desc. device
455+ . newResidencySetWithDescriptor_error ( & rs_desc)
456+ . expect ( "Failed to create MTLResidencySet. Unsupported MacOS/iOS version?" )
457+ } )
458+ } else {
459+ None
460+ } ;
461+
437462 let memory_types = heap_types
438463 . into_iter ( )
439464 . enumerate ( )
440465 . map ( |( i, ( memory_location, heap_descriptor) ) | MemoryType {
466+ global_residency_set : global_residency_set. clone ( ) ,
441467 memory_blocks : vec ! [ ] ,
442468 _committed_allocations : CommittedAllocationStatistics {
443469 num_allocations : 0 ,
@@ -455,6 +481,7 @@ impl Allocator {
455481 debug_settings : desc. debug_settings ,
456482 memory_types,
457483 allocation_sizes : desc. allocation_sizes ,
484+ global_residency_set,
458485 } )
459486 }
460487
@@ -573,4 +600,31 @@ impl Allocator {
573600
574601 total_capacity_bytes
575602 }
603+
604+ /// Optional residency set containing all heap allocations created/owned by this allocator to
605+ /// be made resident at once when its allocations are used on the GPU. The caller _must_ invoke
606+ /// [`MTLResidencySet::commit()`] whenever these resources are used to make sure the latest
607+ /// changes are visible to Metal, e.g. before committing a command buffer.
608+ ///
609+ /// This residency set can be attached to individual command buffers or to a queue directly
610+ /// since usage of allocated resources is expected to be global.
611+ ///
612+ /// Alternatively callers can build up their own residency set(s) based on individual
613+ /// [`MTLAllocation`]s [^heap-allocation] rather than making all heaps allocated via
614+ /// `gpu-allocator` resident at once.
615+ ///
616+ /// [^heap-allocation]: Note that [`MTLHeap`]s returned by [`Allocator::heaps()`] are also
617+ /// allocations. If individual placed [`MTLResource`]s on a heap are made resident, the entire
618+ /// heap will be made resident.
619+ ///
620+ /// Callers still need to be careful to make resources created outside of `gpu-allocator`
621+ /// resident on the GPU, such as indirect command buffers.
622+ ///
623+ /// This residency set is only available when requested via
624+ /// [`AllocatorCreateDesc::create_residency_set`], otherwise this function returns [`None`].
625+ pub fn residency_set ( & self ) -> Option < & Retained < ProtocolObject < dyn MTLResidencySet > > > {
626+ // Return the retained object so that the caller also has a way to store it, since we will
627+ // keep using and updating the same object going forward.
628+ self . global_residency_set . as_ref ( )
629+ }
576630}
0 commit comments