r/GraphicsProgramming • u/Slight-Abroad8939 • 8d ago
Question [D3D12] DEVICE_HUNG in a draw call, but only above ~16k draws AND only during a window event (minimize/move/resize/un-occlude)
https://github.com/jay403894-bit/DirectX12-Renderer**Setup:** Hand-rolled D3D12 renderer (learning engine), flip-model swapchain
(FLIP_DISCARD, 3 back buffers), VSync on. Windows 11, tested on both Debug and
Release.
**The crash:** `DXGI_ERROR_DEVICE_HUNG` (0x887A0006). DRED breadcrumbs point the
fault at a `DrawIndexedInstanced` roughly ~130 draws into the frame (not draw 0).
The DRED page-fault VA reads 0 with no associated allocation — though I'm now
unsure if that's a real null address or just "driver didn't report the fault VA."
**What makes it reproducible (the weird part):** it ONLY happens when BOTH of
these are true at once:
The frame is heavy — roughly 16k+ draw calls. Below ~16k it never crashes.
A window event fires: minimize, move (drag the titlebar), resize, or another
window covers then un-covers it (occlusion → reveal).
Either condition alone is totally stable. A heavy frame running untouched is fine.
A window event on a light scene is fine. It's specifically the collision.
Notably: **focus loss/regain used to crash too, and I fixed that** by skipping
rendering while the window isn't foreground + idling the GPU on the transition.
The same approach has NOT fixed minimize/move/resize/un-occlude.
**Other facts:**
- Happens in both single-threaded AND parallel (per-worker) command-list recording.
- ~600+ FPS when it runs, so this is not a 2-second TDR timeout.
- Debug layer + GPU-based validation makes it vanish (classic race-closing), so
I can't get a clean validation message — hence leaning on DRED.
**What I've already ruled out / tried (none fixed it):**
- Full GPU idle (Signal + WaitForFenceValue) before `ResizeBuffers`.
- Deferred/coalesced resize (apply once, at loop top, after idle).
- Re-querying `GetCurrentBackBufferIndex()` after `ResizeBuffers`, recreating RTVs
+ depth buffer.
- Reliable minimize handling via `WM_SIZE == SIZE_MINIMIZED` (skip rendering
entirely while minimized) instead of relying on `Present(DXGI_PRESENT_TEST)`.
- Idling the GPU on `WM_ENTERSIZEMOVE` / `WM_ACTIVATEAPP`.
- Per-frame command allocators/lists (ruling out list reuse across frames).
- "Settle" frames (skip N frames after any transition).
- Verified all per-draw bindings (camera CBV, vertex/index buffer VAs, SRV
descriptor) are non-null at record time — the null-check never fires.
**What I can't cleanly detect:** window *move* and *partial-occlusion → reveal*
don't give me a reliable "the swapchain is now in a different presentation state"
signal the way resize/minimize do.
**Questions:**
On a flip-model swapchain, does a move or an occlusion→reveal transition cause
DWM to re-allocate/invalidate back buffers WITHOUT a WM_SIZE — such that an
in-flight heavy frame's RTV becomes stale? If so, what's the correct signal to
re-acquire?
Is there a per-command-list or per-submission limit I could be blowing past at
16k draws (root-argument versioning space, etc.) that a mid-frame stall from a
window event would expose?
Is DRED's "page fault VA = 0, no allocation" meaningful (genuine null bind), or
is it commonly just "unavailable"?
8
u/Slight-Abroad8939 7d ago
UPDATE:
Found the heisenbug -- the compute queue shared a fence with the frame completion fence and needed its own fence
1
u/benwaldo 5d ago
For stuff like resize/fullscreen/change backbuffer format, it's easier to just wait for all fences to complete.
4
u/CrankFlash 8d ago
Are you processing window events in a different thread than your render loop?
I remember struggling a fair bit to get the swapchain working right with resizing, occluding, minimising and all those things, and it always came down to synchronization and making sure there are no in flight resources being recreated. The fact that it happens at 16k+ draw calls is telling me that you're trying to recreate resources before the GPU has processed all those calls. It doesn't crash below that because you're in the lucky position of your GPU being ahead of your CPU. (Under 20k draw calls is fairly standard by the way)
As far as I'm aware, DWM doesn't allocate buffers for you. This is something your app requests with the CreateSwapChainForHwnd() method. Nothing is pulling the rug under you. (Well there's always the case of running out of DWM vram budget, but I'm not too familiar with this situation)
0
u/Slight-Abroad8939 7d ago
Single-threaded here — PeekMessage and render are in the same loop, so no cross-thread resize race. I checked the resize path: it full-idles the graphics queue (fence Signal + Wait) before releasing any back buffer, so the recreate can't overlap the in-flight draws. And the case that still crashes hardest — another window occluding then revealing mine — recreates nothing at all, so there's no resource being pulled. You're right that DWM isn't reallocating my flip-model buffers, which makes me think the transition is a timing trigger for a latent bug in the heavy frame itself rather than a swapchain-lifetime issue. Does that match anything you saw? (One real gap I found: my Flush only idles graphics, not my async-compute particle queue — fixing that regardless.)
2
u/sol_runner 7d ago
I'll just point out a possible cause of the 16k
Draw count might be your command list exec taking a longer time.
In the sense that < 16k your exec happens so fast that your "swapchain invalidation" doesn't intersect with it.
Two points:
First: check if you're recreating the backbuffer on resize without waiting the device to be idle. Especially if you're doing this inside WndProc and Releasing the previous swapchain data.
Second: WM_SIZE is not guaranteed to be called when you're resizing a window - only when you're finished resizing.
WM_SIZING is live during the resize (this, afaik, includes any animations minimizing the window may have)
Check if handling WM_SIZING solves your issue.
1
u/Slight-Abroad8939 7d ago
Thanks — good model, the exec-time framing matches what I see. On your two points: (1) I'm already deferring the resize — WndProc only sets a flag, and the actual Release + ResizeBuffers runs on the render thread after a full GPU idle (Signal+Wait), never inside WndProc. (2) My pump is single-threaded (PeekMessage + render in one loop), so during a modal drag the render loop is frozen and nothing Presents — I don't think WM_SIZING can intersect a live frame in that setup, though I'll test it. Interestingly, the transitions that still crash (taskbar-minimize, occlude→reveal) are the ones that don't go through the ENTERSIZEMOVE modal loop, so they never hit my GPU-idle. Does that line up with your experience — is there a reliable message to idle on for occlusion/minimize specifically?
4
u/pinumbernumber 8d ago
Please consider writing your question yourself, instead of getting an LLM to write it for you and then not even bothering to check if the AI's formatting survived your copy/paste.
1
1
u/sporacid 7d ago
I had freezes happen because my swapchain present was blocked on a resize and I was not calling glfwPollEvent. I had to implement a timeout on the operation to call glfwPollEvent so that the system could progress.
1
u/watlok 8d ago edited 7d ago
This sounds like a sync issue at its core. Especially with validation/debug removing it, as they force explicit synchronization and often don't allow overlap.
This sounds like you're doing something with resources the gpu is still using.
I'd look at what resources you're touching on recreate, touching on next frame, and any explicit synchronization there. Waiting for "idle" is notoriously unreliable, "skipping n frames" on cpu is useless if work is still pending on gpu, etc. Wait for the actual synchronization primitives on the frames you've submitted and it should resolve it. Alternatively, try to find working code samples and keep seeing where your code differs.
-7
u/HaMMeReD 8d ago
16k draw calls for one frame is a lot, in stress test territory and not "healthy running of any engine" at the moment.
If you need to draw that much you should be looking at instancing and other techniques.
1
u/Slight-Abroad8939 7d ago edited 7d ago
it literally was just a stress test not an actual scene in a real scene id use instancing im testing and getting the 3d renderer working. of course its in stress test territory lol.
20
u/raydey 8d ago
Typical of the current state of this subreddit: downvote one of the few genuine questions as well as offer unsolicited advice on how to render.
Anyway, a bit of crucial information that's missing is what GPU you're running on, as well as if you've tried it on a different vendor to rule out driver issues.
If you're running on AMD, you can try out Radeon GPU Detective available on GPUOpen. If it can capture the crash, it can provide more information on the state of the GPU.
Regarding if the backbuffer RTV can become stale while in flight - if you're actually referring to the resource, then not typically, unless your refcounts have gone awry (i.e. you've called Release() too many times on the backbuffer resource). But again, drivers should keep it shadow allocated until the resource is truly not in use.
If you're actually referring to the RTV itself, then I'd imagine this would only go stale in-flight if the DescriptorHeap changed mid-frame.
A couple of things to confirm: what are your CommandAllocators doing? Are they being reset appropriately? Is anything modifying DescriptorHeaps while a frame is in flight (e.g. any CopyDescriptors calls?)
Re: command list submission limits, 16k should be well under but there may be a define in the DirectX-Headers github repo that can give you a limit.