protonscr

Warhammer: Vermintide 2 Crashes after alt+tab induced black screen

vkd3dclosed appid 552500
HansKristian-Work/vkd3d-proton#3156 · opened 2026-07-10 by zzhiyi · updated 2026-07-12 · 3 comments · github · game page · search this game
Zzzhiyi 2026-07-10 github

TLDR; I investigated the crash and I think it's an application bug that has d3d12 fence value rewind, which causes a rendering freeze that triggers a crash. However, I suspect there might be some d3d12 fence peculiarities so I would like to get your opinion.

To reproduce the bug

  1. Launch the game on the latest proton experimental 11 (example experimental-11.0-20260609)
  2. Click through intro logos/safety warnings.
  3. At main menu, go to Settings and Video, and make sure game is set to fullscreen and DX12.
  4. Alt+tab out and back into game. Letting the game fully animate into the taskbar and back out is ideal for watching the black screen manifest eternally. Try Alt+Tabbing multiples times if you can reproduce the bug. Sometimes it takes more than 10 attempts. You might need to unset VKD3D_DEBUG and WINE_DEBUG because printing logs could make it difficult to reproduce. I did reproduce the bug with VKD3D_DEBUG=trace, but it takes like 50 attempts.
  5. Eventually witness the game return to fullscreen in black and never regain visuals. Game will crash shortly after.

My analysis

The bug happens after proton wine experimental 11 commit "6ffaa8ea Revert "win32u: Don't forcefully activate windows before restoring them.", which now makes the game enter fullscreen correctly. Actually, I can reproduce the bug with Proton 10, 9, and 8 if I try multiple times. So it's not really a Wine regression.

The bug is from a race condition inside the application. It goes something like this.

115680.710:091c:0a00:trace:vkd3d-proton:d3d12_command_queue_Signal: iface 00000000414d2410, fence 00000000414d28b0, value 0x1b.
115680.710:091c:0a00:trace:vkd3d-proton:d3d12_fence_SetEventOnCompletion: iface 00000000414d28b0, value 0x1b, event 00000000000002f4.
...
115680.710:091c:0a18:trace:vkd3d-proton:vkd3d_waiting_fence_signal_fence: Signaling fence 00000000414d28b0 to virtual value 27 (0x1b).
      ^
      |
      At this point, fence->event_count is 0, so the event is not signaled.
115680.710:091c:0a20:trace:vkd3d-proton:vkd3d_waiting_fence_signal_fence: Signaling fence 00000000414d28b0 to virtual value 26.
...
115680.710:091c:0a00:trace:vkd3d-proton:d3d12_fence_set_native_sync_handle_on_completion_explicit: --- line 1404 fence->event_count 1.
             ^
             |
             Now the event is queued, but it's too late. The value for the fence has been set to 26 (0x1a), which is less than 27 (0x1b).
115680.710:091c:0a00:trace:wine:WaitForSingleObjectEx: event 0x2f4 with INFINITE.
Now the application freezes and hangs forever.

Notice that the fence->event_count gets incremented to 1 after the fence value has been set to 27 and then 26 in two other threads. The rewind from the fence value 27 to 26 triggers the deadlock. This is a typical d3d12 fence deadlock. Quoting MSDN

A fence may be rewound. This means that the fence value does not need to solely increment. If a Signal operation is enqueued on two different command queues, or if two CPU threads are both calling Signal on a fence, there may be a race to determine which Signal completes last, and therefore which fence value is the one which will remain. If a fence is rewound, any new waits (including SetEventOnCompletion requests) will be compared against the new lower fence value, and therefore may not be satisfied, even if the fence value had previously been high enough to satisfy them.

So, vkd3d-proton is functioning correctly. After the freeze, the game also has a deadlock detector running on a separate thread. The thread checks that it's waiting for the event for too long, and eventually executes *(char *)NULL = 0 (At vermintide2_dx12+0x305826) to deliberately crash the process if IsDebuggerPresent() returns FALSE, which is the default. If IsDebuggerPresent() returns TRUE, then it doesn't execute the deadlock detector and lets the deadlock continue, and shows a black screen instead. I usually print the rendering thread ID in DXVK swapchain Present() and make IsDebuggerPresent() return TRUE, then check that's the last calls in the thread. This way we can focus on the rendering freeze, which is what actually causing the crash.

If I add something like the following to vkd3d-proton. Then the bug will be difficult to reproduce.

diff --git a/libs/vkd3d/command.c b/libs/vkd3d/command.c
index b288794b..345f516b 100644
--- a/libs/vkd3d/command.c
+++ b/libs/vkd3d/command.c
@@ -491,6 +491,8 @@ static void vkd3d_waiting_fence_signal_fence(struct vkd3d_fence_worker *worker,
     {
         TRACE("Signaling fence %p to virtual value %"PRIu64".\n", info->fence, info->virtual_value);
 
+        Sleep(1);
+
         if (FAILED(hr = d3d12_fence_signal(info->fence, worker, info->update_count)))
             ERR("Failed to signal D3D12 fence, hr %#x.\n", hr);
     }

Obviously, that's an ugly hack we are not going to use. The race condition is very sensitive to timing. A slight delay, for example, 1 microsecond, or adding one line of trace, could make the bug disappear. That explains why the bug can't be reproduced on vkd3d or Windows because their code paths are different. It's totally possible that one code path is slightly longer, and it just so happens to avoid the deadlock.

A strange behavior worth noting though is that the d3d12_fence_SetEventOnCompletion() trace does seem always appear before the later two vkd3d_waiting_fence_signal_fence(). So it's possible that it might be related to some d3d12 fence peculiarity. But I am not sure. The fence does get used in two command queues.

The full log is at steam-552500-3.zip. The log snippet shown here starts around line 567185. Notice that in the log, at the end of thread 0xa00, there is no WaitForSingleObjectEx() call. That's because adding a trace in WaitForSingleObjectEx() make the bug difficult to reproduce so I can't add any traces. But the thread is indeed waiting for event 0x2f4 with INFINITE timeout.

Zzzhiyi 2026-07-10 github

@HansKristian-Work Hi, what do you think of this?

HHansKristian-Work maintainer 2026-07-10 github

I'm not sure what you expect me to do here. A fence rewind by game is a game bug.

What is causing the signal to 26 and 27? I only see one signal in your log.

115680.710:091c:0a00:trace:vkd3d-proton:d3d12_command_queue_Signal: iface 00000000414d2410, fence 00000000414d28b0, value 0x1b.

Is there an equivalent command queue signal to 26? If that happens on a different queue, it's a racing signal where either 26 or 27 complete first.

Zzzhiyi 2026-07-12 github
line 567168: 115680.710:091c:0a00:trace:vkd3d-proton:d3d12_command_queue_Signal: iface 00000000414d2660, fence 00000000414d28b0, value 0x1a.
...
line 567178: 115680.710:091c:0a00:trace:vkd3d-proton:d3d12_command_queue_Signal: iface 00000000414d2410, fence 00000000414d28b0, value 0x1b.

At line 567168 and 567178 in the full log, you can see the fence is used on two different queues. I do think this is a game bug. Paul Gofman said that I should talk to you about this. He said that maybe Microsoft has implemented some undocumented tricks that make the event signaled in this case. Anyway, now I can say that we both agree that it's a game bug and the game developers should fix this. Thanks for taking a look.

Proton versions

Launch options