Summary
Reproducible EXCEPTION_ACCESS_VIOLATION in the D3D12 RHI while binding sampler
descriptors for a Nanite base-pass compute shader. The faulting instruction
dereferences a FD3D12SamplerState* that passes a null check but points to freed
memory.
Reproduces on 10+ machines, on both AMD and NVIDIA GPUs. The faulting address
resolves inside the game executable, not in any driver module.
Setting r.Nanite=0 makes it stop reproducing (see KEY LOCALIZATION below).
ENVIRONMENT
Engine : 5.7.4-51494982+++UE5+Release-5.7 (Launcher binary install)
Configurations : reproduced in DebugGame, Development and Shipping
Platform : Win64
OS (capture rig) : Windows 11 25H2 [10.0.26200.9168]
CPU (capture rig) : AMD Ryzen 7 9800X3D
GPU (capture rig) : NVIDIA GeForce RTX 5090, driver 32.0.16.1088
Also reproduced : 10+ other machines, AMD and NVIDIA GPUs, various tiers
Project : open world, World Partition, Nanite + Lumen + VSM
Relevant settings : r.RayTracing=False
Bindless enabled (Configuration=RayTracing)
“Bindless is supported. MaxNonSamplerDescriptors: 1000000,
MaxSamplerDescriptors: 2048”
“Bindless is enabled (Configuration=RayTracing).
Initializing allocators with 32768 Resource and
2048 Sampler descriptors”
What Type of Bug are you experiencing?
Rendering (Graphics / Niagara)
Steps to Reproduce
Load the open-world map and traverse it so World Partition streams cells in and
out. The crash occurs after roughly 100-210 seconds of play. It is not tied to a
specific location, actor or asset (see ELIMINATED below).
SecondsSinceStart across the 11 captures:
103, 105, 106, 109, 112, 114, 125, 154, 186, 206, 484
Expected Result
KEY LOCALIZATION: r.Nanite 0 AVOIDS THE CRASH
Setting r.Nanite=0 at runtime (the cvar is ECVF_Scalability | ECVF_RenderThreadSafe,
so it can be toggled live) makes the crash stop reproducing.
This was verified as a reversible A/B/A test on one single build, same route,
same machine, within a 10 minute window:
A Nanite on -> crash (11 captures, SecondsSinceStart 103 to 484)
B r.Nanite=0 -> clean 182 s, no crash
B r.Nanite=0 -> clean 186 s, no crash
B r.Nanite=0 -> clean 441 s, no crash, while flying the entire World
Partition map at 55000 speed, i.e. maximum cell
streaming churn, the exact stress that should
trigger the bug
A Nanite on -> crash 125 s, identical signature
A Nanite on -> crash 484 s, identical signature
Observed Result
ELIMINATED, WITH THE METHOD USED
Each of these was verified as actually applied before drawing a conclusion.
GPU vendor / driver / overlays
The minidump exception address resolves inside
Frommisfortunecomes-Win64-Shipping.exe, not in nvwgf2umx.dll, D3D12Core.dll
or dxgi.dll. Reproduces on AMD and NVIDIA across 10+ machines.
RHI breadcrumbs / diagnostic buffer
Shipping has them off by default. The crash persists and
BindDiagnosticBuffer disappears from the stack, proving that frame was not
the cause.
Nanite tessellation
Built with r.Nanite.AllowTessellation=0 and r.Nanite.Tessellation=0.
Still crashes.
Text3D plugin
Plugin disabled in the build. Still crashes.
Gameplay actors
All NPCs and all traffic destroyed at session start via debug commands, and
prevented from respawning. Still crashes.
RHI thread boundary
Launched with -norhithread. Verified applied: the crashing thread changed
from “Background Worker #0” to “RenderThread 0”, and frame 8 changed from
LowLevelTasks::TTaskDelegate to FNamedTaskThread::ProcessTasksUntilQuit.
Still crashes, with the same faulting instruction. So the corruption is not
a race across the RHI thread boundary.
Location in the level
Reproduces anywhere in the map, not tied to a cell or an asset.
Sampler cache LRU eviction
RHICreateSamplerState in D3D12State.cpp frees a descriptor immediately when
the cache reaches D3D12_MAX_SHADER_VISIBLE_SAMPLER_HEAP_SIZE:
SamplerCache.Find(CurrentKey)->GetReference()->FreeDescriptor();
SamplerCache.Remove(CurrentKey);
This looked like an exact fit for a dangling raw pointer. It is NOT what is
happening here: the accompanying warning
"New SamplerState would exceed cache limit"
does not appear in any of the 13 captured logs, and LogD3D12RHI is not
suppressed in the project's [Core.Log]. Ruling this out may still be useful
to you, since the immediate-free path does leave raw pointers in the
per-context sampler cache and looks unsafe by inspection.
Affects Versions
5.7
Platform(s)
Windows
For crash reports, include your callstack
CRASH SIGNATURE
Error : Unhandled Exception: EXCEPTION_ACCESS_VIOLATION reading address
0xffffffffffffffff (one capture: 0x000000030003a796)
GPU breadcrumb (identical in every capture that produced one):
Breadcrumbs 'Parallel'
- Nanite::BasePass
- NaniteBasePass
- BasePass
- Scene
- RenderGraphExecute - /ViewFamilies
- SceneRender - ViewFamilies
- Frame <n>
Callstack (Shipping, symbolicated with matching PDB):
FD3D12DescriptorCache::SetSamplerTable()
FD3D12CommandContext::RHISetShaderParameters()
FRHICommandSetShaderParameters<FRHIComputeShader>::Execute()
FRHICommandListExecutor::FTranslateState::Translate()
FRHICommandListExecutor::FTaskPipe::Execute()
TGraphTask<TFunctionGraphTaskImpl<void __cdecl(ENamedThreads::Type,
TRefCountPtr<FBaseGraphTask> const&),0> >::ExecuteTask()
UE::Tasks::Private::FTaskBase::TryExecuteTask()
LowLevelTasks::FScheduler::WorkerLoop()
FThreadImpl::Run()
FRunnableThreadWin::Run()
Additional Notes
In DebugGame builds an additional top frame appears,
FD3D12ContextCommon::BindDiagnosticBuffer(). That frame is inlining noise: it
disappears in Shipping (where RHI breadcrumbs are off by default) while the
crash and the faulting instruction stay identical.
PCallStackHash per configuration (differs only because the binaries differ):
DebugGame 0378955D700B67513AEA2022718E66810E523220
Shipping 138150CF9CC9C6D5B3E72C41FAA0E22772180F8F
Shipping -norhithread 61EFA5F0D7A44ABF3CBDFA99245B32C86FBEF9EF
Development D370C92727D9D35B7C6B52904B346E43112DF1CC
Crashing thread: “Foreground Worker #1” / “Background Worker #0”, or
“RenderThread 0” when launched with -norhithread.
ROOT CAUSE — EXACT INSTRUCTION AND SOURCE LINE
Faulting address, Shipping build: module RVA 0x2d4e517.
Bytes around the fault, taken from the minidump:
4d 8b c1 mov r8, r9
49 c1 e0 04 shl r8, 4
4c 03 c2 add r8, rdx
48 85 c9 test rcx, rcx
74 2f je +0x2f
→ 48 8b 81 10 01 00 00 mov rax, [rcx+0x110] ← FAULTS
48 85 c0 test rax, rax
74 23 je +0x23
4a 3b 84 c5 c8 23 00 00 cmp rax, [rbp + r88 + 0x23c8]
74 49 je +0x49
4a 89 84 c5 c8 23 … mov [rbp + r88 + 0x23c8], rax
This matches, instruction for instruction, this code in
Engine/Source/Runtime/D3D12RHI/Private/D3D12DescriptorCache.cpp
(FD3D12DescriptorCache::BuildSamplerTable, around lines 350-410):
Desc.SamplerID[SlotIndex] = Samplers[SlotIndex] ? Samplers[SlotIndex]->ID : 0;
...
if (Samplers[SlotIndex] != nullptr)
{
SrcDescriptors[SlotIndex] = Samplers[SlotIndex]->OfflineDescriptor;
}
test rcx,rcx / je == the "Samplers[SlotIndex] != nullptr" guard
mov rax,[rcx+0x110] == reading OfflineDescriptor off that pointer
cmp/mov [rbp+r8*8+..] == writing into the per-slot array
So rcx is Samplers[SlotIndex], an FD3D12SamplerState*.
WHY THIS IS A USE-AFTER-FREE, NOT A NULL DEREFERENCE
-
The pointer PASSES the explicit “!= nullptr” check immediately before the
faulting instruction. It is not null. -
The garbage value CHANGES between runs while the faulting instruction stays
the same:
run A: fault address 0xFFFFFFFFFFFFFFFF (non-canonical → #GP, Windows
reports -1 because there is no valid CR2)
run B: fault address 0x000000030003a796
A fixed poison value or an uninitialised constant would be identical every
run. A value that varies is characteristic of reading recycled memory.
Conclusion: an FD3D12SamplerState is destroyed while a raw pointer to it is
still held in the per-context FD3D12SamplerStateCache, and the next
BuildSamplerTable dereferences it.
ELIMINATED, WITH THE METHOD USED
Each of these was verified as actually applied before drawing a conclusion.
GPU vendor / driver / overlays
The minidump exception address resolves inside
Frommisfortunecomes-Win64-Shipping.exe, not in nvwgf2umx.dll, D3D12Core.dll
or dxgi.dll. Reproduces on AMD and NVIDIA across 10+ machines.
RHI breadcrumbs / diagnostic buffer
Shipping has them off by default. The crash persists and
BindDiagnosticBuffer disappears from the stack, proving that frame was not
the cause.
Nanite tessellation
Built with r.Nanite.AllowTessellation=0 and r.Nanite.Tessellation=0.
Still crashes.
Text3D plugin
Plugin disabled in the build. Still crashes.
Gameplay actors
All NPCs and all traffic destroyed at session start via debug commands, and
prevented from respawning. Still crashes.
RHI thread boundary
Launched with -norhithread. Verified applied: the crashing thread changed
from “Background Worker #0” to “RenderThread 0”, and frame 8 changed from
LowLevelTasks::TTaskDelegate to FNamedTaskThread::ProcessTasksUntilQuit.
Still crashes, with the same faulting instruction. So the corruption is not
a race across the RHI thread boundary.
Location in the level
Reproduces anywhere in the map, not tied to a cell or an asset.
Sampler cache LRU eviction
RHICreateSamplerState in D3D12State.cpp frees a descriptor immediately when
the cache reaches D3D12_MAX_SHADER_VISIBLE_SAMPLER_HEAP_SIZE:
SamplerCache.Find(CurrentKey)->GetReference()->FreeDescriptor();
SamplerCache.Remove(CurrentKey);
This looked like an exact fit for a dangling raw pointer. It is NOT what is
happening here: the accompanying warning
"New SamplerState would exceed cache limit"
does not appear in any of the 13 captured logs, and LogD3D12RHI is not
suppressed in the project's [Core.Log]. Ruling this out may still be useful
to you, since the immediate-free path does leave raw pointers in the
per-context sampler cache and looks unsafe by inspection.
KEY LOCALIZATION: r.Nanite 0 AVOIDS THE CRASH
Setting r.Nanite=0 at runtime (the cvar is ECVF_Scalability | ECVF_RenderThreadSafe,
so it can be toggled live) makes the crash stop reproducing.
This was verified as a reversible A/B/A test on one single build, same route,
same machine, within a 10 minute window:
A Nanite on -> crash (11 captures, SecondsSinceStart 103 to 484)
B r.Nanite=0 -> clean 182 s, no crash
B r.Nanite=0 -> clean 186 s, no crash
B r.Nanite=0 -> clean 441 s, no crash, while flying the entire World
Partition map at 55000 speed, i.e. maximum cell
streaming churn, the exact stress that should
trigger the bug
A Nanite on -> crash 125 s, identical signature
A Nanite on -> crash 484 s, identical signature
The 441 s clean run under maximum streaming stress is longer than 10 of the 11
observed crash times, and the crash returned both times Nanite was re-enabled. Combined with the GPU breadcrumb, which reads
Nanite::BasePass in every single capture without exception, this points at the
Nanite compute-material shading path as the trigger rather than shader parameter
binding in general.
Note that FD3D12DescriptorCache::BuildSamplerTable is generic code used by all
shader parameter binding, so this is a localization of the trigger, not proof
that the lifetime bug itself is Nanite-specific. The Nanite base pass is the
path that binds material parameters through
FRHICommandSetShaderParameters.
This is not a viable workaround for us: the project has 5272 Nanite static
meshes out of 15434, the entire environment is built on it.
CONTENT RULED OUT BY A CLEAN BUILD
Before concluding this is an engine issue we cleaned every content-side warning
that touched the same subsystem, then reproduced the crash on the clean build:
Invalid material [...] used on Nanite static mesh 3800 -> 0
Invalid GameplayTag 142 -> 0
Materials failing to compile (null ShaderMap) 8 -> 0
Plugin dependency warnings 34 -> 0
Total log lines per session 226178 -> 7473
The crash still reproduced on that build (Development, 154 s,
PCallStackHash 3824BBEB7D5EC05E4675C5F81F91A20060B803B3, same instruction, same
Nanite::BasePass breadcrumb). No translucent-on-Nanite material and no material
with a null shadermap remains in the affected map.
WHAT WE COULD NOT DO
AddressSanitizer would identify the exact free site, but the bug is in engine
D3D12RHI code and this is a Launcher binary install: Engine/Source is present
but there is no engine solution to rebuild with bUseAddressSanitizer. A
source-build repro on your side would pin down the free site directly.
ATTACHMENTS TO INCLUDE
Crash captures (CrashContext.runtime-xml + UEMinidump.dmp, plus
Frommisfortunecomes.log where the configuration produced one):
Desktop\Crashes\Event1
Desktop\Crashes\Event2\UECC-Windows-6EEB4BC0411D88A4FEBC9EAE21210887_0001
Desktop\Crashes\Event3\UECC-Windows-D214B83C4349176946C18C8B93E1F992_0001
Desktop\NewCrash\UECC-Windows-D7B9A8544DDB2CBA44C91EB5AC44B8EC_0001
Desktop\NewCrashv2\UECC-Windows-9033BC6A4AE17C381F721189F0A3A5B9_0001
Desktop\NewCrashv3\UECC-Windows-A6229CCE427F51BCE19AB9856D44FA8F_0001
Desktop\NewCrashv4 shipping\Crashes\UECC-Windows-CCBB9C0C4371014628EBF2BAA492F283_0000
%LOCALAPPDATA%\Frommisfortunecomes\Saved\Crashes\
UECC-Windows-1F6655384EEF5DABB6F990B788D74363_0000 (Shipping)
UECC-Windows-CA5F259C4078028741F610B5F7DB0BA4_0000 (Shipping -norhithread)
<SteamInstall>/Frommisfortunecomes/Saved/Crashes/
UECC-Windows-93A0AFC840CBFACF8C54309DE918510D_0000
This last one is the single most useful capture: Development configuration,
built after every content-side warning was cleaned, full 8502 line log, and it
is the second “A” of the A/B/A Nanite test above. It is the only capture with a
symbolicated stack, a complete log, and no content warnings left to distract
from the actual defect.
Also attach the matching Frommisfortunecomes-Win64-Shipping.pdb for the
Shipping captures.
WHAT WOULD HELP MOST
Identification of where the FD3D12SamplerState referenced by
FD3D12SamplerStateCache::Samplers can be destroyed without the cache slot
being invalidated. The cache holds raw pointers and does not participate in the
refcount, so any path that destroys a sampler while a context still has it bound
produces exactly this crash.