Direct3D 12 Memory Allocator
|
Interleaved allocations and deallocations of many objects of varying size can cause fragmentation over time, which can lead to a situation where the library is unable to find a continuous range of free memory for a new allocation despite there is enough free space, just scattered across many small free ranges between existing allocations.
To mitigate this problem, you can use defragmentation feature. It doesn't happen automatically though and needs your cooperation, because D3D12MA is a low level library that only allocates memory. It cannot recreate buffers and textures in a new place as it doesn't remember the contents of D3D12_RESOURCE_DESC
structure. It cannot copy their contents as it doesn't record any commands to a command list.
Example:
Although functions like D3D12MA::Allocator::CreateResource() create an allocation and a buffer/texture at once, these are just a shortcut for allocating memory and creating a placed resource. Defragmentation works on memory allocations only. You must handle the rest manually. Defragmentation is an iterative process that should repreat "passes" as long as related functions return S_FALSE
not S_OK
. In each pass:
ID3D12Heap
+ offset using methods like D3D12MA::Allocation::GetHeap().ID3D12Heap
blocks that became empty.Defragmentation algorithm tries to move all suitable allocations. You can, however, refuse to move some of them inside a defragmentation pass, by setting pass.pMoves[i].Operation
to D3D12MA::DEFRAGMENTATION_MOVE_OPERATION_IGNORE. This is not recommended and may result in suboptimal packing of the allocations after defragmentation. If you cannot ensure any allocation can be moved, it is better to keep movable allocations separate in a custom pool.
Inside a pass, for each allocation that should be moved:
CopyResource()
.D3D12_HEAP_TYPE_READBACK
memory, you can copy its data on the CPU using memcpy()
.pass.pMoves[i].Operation
to D3D12MA::DEFRAGMENTATION_MOVE_OPERATION_IGNORE. This will cancel the move.pass.pMoves[i].Operation
to D3D12MA::DEFRAGMENTATION_MOVE_OPERATION_DESTROY.You can defragment a specific custom pool by calling D3D12MA::Pool::BeginDefragmentation or all the default pools by calling D3D12MA::Allocator::BeginDefragmentation (like in the example above).
Defragmentation is always performed in each pool separately. Allocations are never moved between different heap types. The size of the destination memory reserved for a moved allocation is the same as the original one. Alignment of an allocation as it was determined using GetResourceAllocationInfo()
is also respected after defragmentation. Buffers/textures should be recreated with the same D3D12_RESOURCE_DESC
parameters as the original ones.
You can perform the defragmentation incrementally to limit the number of allocations and bytes to be moved in each pass, e.g. to call it in sync with render frames and not to experience too big hitches. See members: D3D12MA::DEFRAGMENTATION_DESC::MaxBytesPerPass, D3D12MA::DEFRAGMENTATION_DESC::MaxAllocationsPerPass.
Thread safety: It is safe to perform the defragmentation asynchronously to render frames and other Direct3D 12 and D3D12MA usage, possibly from multiple threads, with the exception that allocations returned in D3D12MA::DEFRAGMENTATION_PASS_MOVE_INFO::pMoves shouldn't be released until the defragmentation pass is ended. During the call to D3D12MA::DefragmentationContext::BeginPass(), any operations on the memory pool affected by the defragmentation are blocked by a mutex.
What it means in practice is that you shouldn't free any allocations from the defragmented pool since the moment a call to BeginPass
begins. Otherwise, a thread performing the allocation->Release()
would block for the time BeginPass
executes and then free the allocation when it finishes, while the allocation could have ended up on the list of allocations to move. A solution to freeing allocations during defragmentation is to find such allocation on the list pass.pMoves[i]
and set its operation to D3D12MA::DEFRAGMENTATION_MOVE_OPERATION_DESTROY instead of calling allocation->Release()
, or simply deferring the release to the time after defragmentation finished.
Mapping is out of scope of this library and so it is not preserved after an allocation is moved during defragmentation. You need to map the new resource yourself if needed.