Unbounded Memory Growth in D3D12 Bindless UpdatedHandles Array

Summary

Unbounded memory growth and severe CPU hitches/GPU timeouts in D3D12 Bindless Resource Manager due to UpdatedHandles.Append on long-lived inactive heaps.

Affects Versions

UE 5.5 ~ UE 5.7

Steps to Reproduce

  1. Add a custom UE_LOG inside FD3D12BindlessResourceManager::CheckRequestNewActiveGPUHeap() to print the array size of ActiveGpuHeaps[i].UpdatedHandles.Num() for each heap.
		for (int32 GpuHeapIndex = 0; GpuHeapIndex < NumGpuHeaps; ++GpuHeapIndex)
		{
+			UE_LOG(LogD3D12RHI, Log, TEXT("GpuHeapIndex %d : %d dirty handles"), GpuHeapIndex, ActiveGpuHeaps[GpuHeapIndex].UpdatedHandles.Num());
			if (GpuHeapIndex != ActiveGpuHeapIndex)
			{
				ActiveGpuHeaps[GpuHeapIndex].UpdatedHandles.Append(ActiveGpuHeaps[ActiveGpuHeapIndex].UpdatedHandles);
			}
		}

  1. Create a new project with default settings.
  2. Launch the project and leave it running idle for an extended period.
  3. Observe the memory graph in Windows Performance Monitor (Private Bytes).
  4. Observe the custom log output over time.

Results

The “Private Bytes” memory usage shows a continuous, stair-step growth pattern.

By observing the custom logs, it becomes clear that while some GPU heaps are frequently recycled and cleared, certain heaps remain inactive for long periods. These long-lived inactive heaps continuously accumulate duplicate handles indefinitely.

Here is a log snippet demonstrating the issue:

6.03.30-03.15.36:841][441]LogD3D12RHI: GpuHeapIndex 0 : 4807 dirty handles
[2026.03.30-03.15.36:841][441]LogD3D12RHI: GpuHeapIndex 1 : 4798 dirty handles
[2026.03.30-03.15.36:841][441]LogD3D12RHI: GpuHeapIndex 2 : 3 dirty handles
[2026.03.30-03.15.36:841][441]LogD3D12RHI: GpuHeapIndex 3 : 672 dirty handles
[2026.03.30-03.15.36:841][441]LogD3D12RHI: GpuHeapIndex 4 : 665 dirty handles
[2026.03.30-03.15.36:841][441]LogD3D12RHI: GpuHeapIndex 5 : 212 dirty handles
[2026.03.30-03.15.36:883][442]LogD3D12RHI: GpuHeapIndex 0 : 4810 dirty handles
[2026.03.30-03.15.36:883][442]LogD3D12RHI: GpuHeapIndex 1 : 4801 dirty handles
[2026.03.30-03.15.36:883][442]LogD3D12RHI: GpuHeapIndex 2 : 3 dirty handles
[2026.03.30-03.15.36:883][442]LogD3D12RHI: GpuHeapIndex 3 : 105 dirty handles
[2026.03.30-03.15.36:883][442]LogD3D12RHI: GpuHeapIndex 4 : 668 dirty handles
[2026.03.30-03.15.36:883][442]LogD3D12RHI: GpuHeapIndex 5 : 215 dirty handles
...
2026.03.30-03.15.49:145][855]LogD3D12RHI: GpuHeapIndex 0 : 96381 dirty handles
[2026.03.30-03.15.49:145][855]LogD3D12RHI: GpuHeapIndex 1 : 96372 dirty handles
[2026.03.30-03.15.49:145][855]LogD3D12RHI: GpuHeapIndex 2 : 565 dirty handles
[2026.03.30-03.15.49:145][855]LogD3D12RHI: GpuHeapIndex 3 : 560 dirty handles
[2026.03.30-03.15.49:145][855]LogD3D12RHI: GpuHeapIndex 4 : 90 dirty handles
[2026.03.30-03.15.49:145][855]LogD3D12RHI: GpuHeapIndex 5 : 119 dirty handles
[2026.03.30-03.15.49:478][895]LogD3D12RHI: GpuHeapIndex 0 : 96500 dirty handles
[2026.03.30-03.15.49:478][895]LogD3D12RHI: GpuHeapIndex 1 : 96491 dirty handles
[2026.03.30-03.15.49:478][895]LogD3D12RHI: GpuHeapIndex 2 : 3 dirty handles
[2026.03.30-03.15.49:478][895]LogD3D12RHI: GpuHeapIndex 3 : 679 dirty handles
[2026.03.30-03.15.49:478][895]LogD3D12RHI: GpuHeapIndex 4 : 209 dirty handles
[2026.03.30-03.15.49:478][895]LogD3D12RHI: GpuHeapIndex 5 : 119 dirty handles

As seen above, Heaps 2-5 fluctuate as they are recycled, but Heaps 0 and 1 are stuck accumulating nearly 100,000 handles (and this number keeps growing over hours).

Eventually, appending to these massive arrays causes severe CPU thread hitches, leading to GPU Timeouts (TDR) and crashes.

Issue

The engine eventually selects ONE heap to be the NewActiveGpuHeapIndex and calls Reset() on its UpdatedHandles. However, as proven by the logs, some heaps are NOT selected for a very long time.
Because there is no deduplication or size limit, the UpdatedHandles array in these dormant heaps will continuously Append garbage data. The TArray grows exponentially with duplicate handles.
As these arrays grow to contain hundreds of thousands or millions of elements, the Append operation and memory reallocations become extremely expensive on the CPU. This blocks the render thread, starves the GPU, and acts as the primary culprit for random GPU Timeouts (TDR) in long-running sessions.

Temporary Workaround

Commenting out the pooled heap reuse/append logic entirely and forcing the engine to fallback to NewActiveGpuHeapIndex = AddActiveGPUHeap(); stops the unbounded growth and stabilizes the project.