[GAME BUG]: Uncharted: The Nathan Drake Collection (CUSA02320) — three root-caused crashes (GPU hang, menu crash, EOP assert) + workarounds
Related: #4353, #4347, #4178, compatibility #1000 / #2645
Summary
Out of the box, CUSA02320 goes to a black screen and dies with Device lost within ~10 s. I debugged it with a local build (checkpoint markers, VK_EXT_device_fault, a Windows crash-context dump and a software write-watchpoint on guest memory) and found three separate root causes. With the workarounds below, the game now reaches the main menu, and starting Uncharted 1 plays the first cutscene. It still isn't playable past that point (see "Still broken").
| # | Symptom | Root cause | Workaround that works on v0.18.0 |
|---|---|---|---|
| 1 | Black screen → Device lost (NVIDIA TDR) ~10 s after boot |
Compute shader loop bounds come from a dynamic ReadConst. With DMA disabled, that read silently becomes flatbuf[0], so the loop counts are garbage and the GPU hangs |
directMemoryAccess = true |
| 2 | Crash ~2 s into the main menu (null deref / call to 0 in the renderer) | A guest leaf function keeps its counters in the SysV red zone. On Windows, exception dispatch clobbers it, so a shader-parameter layout table gets built with a hole | WindowsGuestRedZoneProtection: StaticPatching (per-game config) |
| 3 | Crash when starting Uncharted 1 (int 0x41) |
Game assert m_gfxEopTick fires because the EOP fence for an older frame hasn't been processed yet (GPU thread lagging while compiling shaders) |
1-byte patch (below) |
Environment
- shadPS4 v0.18.0 (
e3ce810f); issue 2 also reproduced on pre-releasea5d307ee(2026-09-14) - Game: CUSA02320, app version 01.00, no firmware modules or fonts dumped
- Windows 11 Home, i5-12500H, 16 GB RAM, RTX 3050 Ti Laptop (4 GB), NVIDIA driver 616.56
- Default config unless stated otherwise (readbacks 0, DMA off, copyGPUBuffers off)
1. GPU hang: dynamic ReadConst falls back to flat buffer word 0 when DMA is off
Evidence
VK_EXT_device_fault(added locally) reports onlyInstructionPointerUnknownaddresses and no Read/WriteInvalid, i.e. a hang rather than a page fault.nvlddmkmevent 153 is logged at every crash. RaisingTdrDelayto 10 s changes nothing.VK_NV_device_diagnostic_checkpoints(added locally, marker = shader hash) shows that the last executed command is a dispatch of cs0x43b8ee5e(stage0x2000= compute).- In the IR dump of
cs_0x43b8ee5e, the shader has 8 loops (several nested). Their bounds come fromReadConst (flags=0x0) %ptr, #idx, which is a dynamic read through a guest pointer (e.g. iterating 224-byte records atptr + i*224whilei < ReadConst(ptr, 2)). EmitReadConst(emit_spirv_context_get_set.cpp):
For dynamic readsif (!EmulatorSettings.IsDirectMemoryAccessEnabled()) { return ctx.EmitFlatbufferLoad(ctx.ConstU32(flatbuf_off_dw)); // flatbuf_off_dw == 0 for dynamic reads }flatbuf_off_dwis 0, so the shader reads flat buffer word 0, which holds unrelated data. The loop count becomes garbage and the nested loops spin until the driver resets the device.- With
directMemoryAccess = true, the same shader goes throughread_const_dynamic, which returns 0 on a BDA miss. The hang disappears and the game gets past the logos.
Suggested fix: when DMA is disabled, a dynamic ReadConst (flags == 0) should return 0 (the same fallback read_const_dynamic uses on a miss), and ideally log once, rather than loading flatbuf[0]. Alternatively, force DMA for shaders that contain dynamic ReadConst.
1b. Related: unbounded guest buffer, Buffer::GetSize() overflow and multi-GB allocation
The same shader binds a read-only storage buffer with stride=4, num_records=0xffffffff:
BindBuffers: stage=Compute written=false addr=0x111042c400 stride=4 num_records=0xffffffff
size=0xfffffffc clamped=0xaa7d3c00 max_ssbo=0xffffffff
Buffer alloc: size=0xaa7d4000 DeviceLocal -> landed in a non-device-local memory type
Buffer alloc: size=0xa8fc9000 Upload (staging copy)
... Failed allocating image with error ErrorOutOfDeviceMemory
AmdGpu::Buffer::GetSize()returnsu32(stride * num_records), so4 * 0xffffffffwraps to0xfffffffc.ClampRangeSizethen clamps to the remaining VMA (~2.66 GB), and the buffer cache creates a DeviceLocal buffer that size plus a same-size upload buffer. On a 4 GB card this spills into non-device-local memory and later OOMs.- Suggestions: make
GetSize()64-bit, or treatnum_records == 0xffffffffas "unbounded". Unbounded read-only guest buffers are good candidates for the BDA/DMA path instead of a full mirror. Capping the binding locally to 256 MB–1 GB made no visible difference to the crash, so this is a memory/perf problem rather than the hang.
2. Main-menu crash: red zone clobbered on Windows, so the shader parameter layout gets built with a hole
Crash sites (all in the same renderer function, offsets relative to the eboot base):
eboot+0x63f61e mov rbx,[rsi]withrsi=0(read at 0)eboot+0x63f5d0 mov rsi,[r8+rsi*8+0x380]withrsi= a code address (garbage slot)eboot+0x64011freturn address aftercall rcxwithrcx=0. The member-function table is ateboot+0x94a350, and entries 0–19 are legitimately empty.
The same offsets appear on every run, and the crash also reproduces on pre-release a5d307ee. It happens with readbacks 0 and 2, and with copyGPUBuffers on or off.
What's wrong: the layout table (r11) header says 5 entries (group boundaries 2,2,4,4,5 at +0x14..+0x24), entries 0–3 are valid 48-byte records, and entry 4 is all zero (handler id 0 → call 0).
Builder: eboot+0x63df10 is a leaf function. Pass 1 counts descriptors into 5 groups and writes the header. Pass 2 writes one 48-byte entry per descriptor at layout + 0x28 + idx*48. The per-group write indices live in the red zone: [rsp-0x4], [rsp-0x8], [rsp-0xc], [rsp-0x10], [rsp-0x2c].
Watchpoint trace (int3 at the builder's exit eboot+0x63e24c, built tables made read-only, writes logged): for the crashing table, the group-E entry was written by eboot+0x63e234 at offset +0x28 (index 0) instead of +0xe8 (index 4). So [rsp-0x2c] held 0 instead of 4 when it was read, and entry 4 was never written. The header and entries 0–3 were written correctly, and no other code wrote to the table. This is exactly what a clobbered red zone produces. (I didn't confirm which exception clobbered it. The likely candidate is the buffer-tracking page-protection faults, which appear once DMA is enabled.)
Workaround (confirmed): user/custom_configs/CUSA02320.json
{
"WindowsGuestRedZoneProtection": {
"windows_guest_red_zone_protection_mode": "StaticPatching"
}
}
With this, the main menu works reliably. The loader reports the patching as partial: 0 stack-dependent, 0 control-flow, and 2 unrelocatable memory instructions were not protected. Since DMA is required for issue 1, and DMA makes this crash appear, it may be worth enabling red-zone protection by default on Windows, at least when DMA is on.
3. m_gfxEopTick assertion when starting Uncharted 1
*** ASSERTION: GetRenderFrameParams(frameToFreeInstancesFor)->m_gfxEopTick
*** Function: FrameBegin
*** File: ndlib\render\fg-draw-mgr.cpp
*** Line: 234
Unhandled Exception code 0xc0000005 at eboot+0x4d27ba (int 0x41)
Check at eboot+0x4d2776: cmp qword [frameParams+0xa08], 0 / jne ok, then the assert block.
The EOP fence (PM4ItOpcode::EventWriteEop → SignalFence) is only written when the command processor reaches that packet. While loading a level (hundreds of shader compiles, pipeline cache off), the GPU thread falls several frames behind, and the game asserts that an older frame's EOP tick was never recorded.
Workaround patch (same idea as the TLOU "Remove 2-second frame time assert" patch):
<?xml version="1.0"?>
<Patch>
<TitleID>
<ID>CUSA02320</ID>
</TitleID>
<Metadata Title="Uncharted: The Nathan Drake Collection" Name="Remove GPU EOP tick assert (Uncharted 1)" Note="FrameBegin (ndlib\render\fg-draw-mgr.cpp:234) halts via int 0x41 when an older frame's GPU end-of-pipeline tick has not been recorded yet (GPU thread behind, e.g. shader compilation). Turns the jne over the assert block into a jmp." Author="" PatchVer="1.0" AppVer="01.00" AppElf="eboot.bin" isEnabled="false">
<PatchList>
<Line Type="bytes" Address="0x8d277f" Value="eb"/>
</PatchList>
</Metadata>
</Patch>
(The original bytes at eboot+0x4d277f are 75 3b.) With this, the first Uncharted 1 cutscene plays. Enabling the pipeline cache should also reduce how far the GPU thread falls behind.
Still broken (with all workarounds applied)
- Main menu 3D background renders black. Menu text and UI are fine, and the background loads from
menu-ingame.pakrather than a video. Readbacks 0/1 make no difference. Not investigated further. Device lostright after the first Uncharted 1 cutscene (gameplay start, dive-boat level) on the official v0.18.0 build.nvlddmkmevent 153 again. It did not reproduce on my local build with checkpoint markers enabled, which ran very slowly, so it may be timing-dependent.- Very low FPS in gameplay with DMA enabled, with assets streaming in slowly. Not profiled. One suspect from reading the code:
Rasterizer::BindResourcescallsSynchronizeBuffersInRangeover allmapped_rangeson every DMA-using bind.
Settings used to reach the menu and cutscene
config.json:
"GPU": { "direct_memory_access_enabled": true, "readbacks_mode": 0, "copy_gpu_buffers": false },
"Vulkan": { "pipeline_cache_enabled": true }
Plus the per-game red-zone config from section 2, and the patch from section 3.
Local diagnostics I added (can share as a PR if useful)
- Enable
VK_EXT_device_faultand log fault address/type/vendor info oneErrorDeviceLost(scheduler submit and presenter frame waits). - Enable
VK_NV_device_diagnostic_checkpointsand tag draws/dispatches with the shaderpgm_hash, logging the last checkpoints on device loss. - Windows
SignalHandler: on an unhandled access violation, log access type, fault address, the decoded instruction, all GPRs, and the top of the guest stack.
Source: shadps4-emu/shadPS4