Skip to content

Fix IddCx watchdog crashes in swap-chain assign/unassign - #532

Draft
Tenebray84 wants to merge 2 commits into
VirtualDrivers:masterfrom
Tenebray84:fix/swapchain-ddi-timeouts-master
Draft

Tenebray84 wants to merge 2 commits into
VirtualDrivers:masterfrom
Tenebray84:fix/swapchain-ddi-timeouts-master

Conversation

@Tenebray84

Copy link
Copy Markdown

Fixes #531.

Problem

IddCx guards EvtIddCxMonitorAssignSwapChain and EvtIddCxMonitorUnassignSwapChain with a DDI watchdog (5000 ms by default). If a callback takes longer, IddCx reports a WUDFVerifierFailure (Category=Driver, Driver=MttVDD.dll, ErrorNumber ...10f for assign, ...10e for unassign), WUDFHost is terminated and the virtual display goes offline. With Sunshine's ensure_only_display this leaves the physical monitor black. Details, WER signatures and dump analysis are in #531.

On master both patterns still exist:

  • Assign: GetOrCreateDevice() is called inside the callback. The LUID cache only helps from the second assignment on; the first assignment per adapter, and every retry after a failed creation, still runs D3D11CreateDevice (and loads the GPU user-mode driver) under the watchdog.
  • Unassign / replacement: processorToStop / previousProcessor are destroyed at the end of the callback, which waits INFINITE for the processing thread. In the dumps that thread was blocked in WdfObjectDelete(swap-chain) → IddSwapChain::Cleanup → NtGdiDdDDIDestroyAllocation2. During assign IddCx also holds the monitor lock, which the old thread's swap-chain cleanup needs.

IddCx behaviour this relies on

Derived from disassembly of IddCx.dll with public symbols (Windows 11 build 26300); this is not documented, so please double-check:

  • After UnassignSwapChain returns, IddCx takes the monitor lock and calls WdfObjectDelete on the swap-chain if it is still attached. IddSwapChain::Cleanup detaches it from the monitor under that lock before the slow COM releases. So the driver may leave the deletion to IddCx, but must not use the handle once the callback has returned.
  • IddCx holds the monitor lock while calling AssignSwapChain.
  • The EVT_IDD_CX_MONITOR_ASSIGN_SWAPCHAIN documentation suggests creating the D3D device on the processing thread.

Changes

  • SwapChainProcessor gets the render adapter LUID and obtains the device (GetOrCreateDevice) on its own thread. If no device can be created, it pauses for 250 ms (interruptible) before releasing the swap-chain, to avoid a tight assign/fail loop.
  • StopForUnassign(): unassign revokes the driver's ownership and waits only until the thread can no longer touch the swap-chain (bounded, at most 3000 ms). IddCx then deletes the swap-chain after the callback. A thread that stops on its own (error, DXGI_ERROR_ACCESS_LOST) still deletes the swap-chain itself, as documented. Ownership is decided under a lock, so the two paths cannot both delete it.
  • Assign never waits for the previous processor: it only gets a stop request and is kept in a retired list until its thread has exited. The device context joins retired processors on teardown.
  • A WDF reference is held on the swap-chain for the processor's lifetime.
  • The terminate event is manual-reset and is checked on every loop iteration and before IddCxSwapChainFinishedProcessingFrame, so a stop is noticed while frames keep arriving.
  • If the processing thread cannot be started, the swap-chain is deleted instead of being left unprocessed.

Testing

  • Builds on this branch: Release x64, WDK 10.0.26100, no new warnings.
  • Runtime-tested on a backport to 24.12.24, which has the same protocol and is in daily use: RTX 4080 SUPER, NVIDIA 621.01, Windows 11 26300, Sunshine ensure_only_display. One session of almost 4 h at 3456x2160 @ 120 Hz, several short sessions, overnight monitor standby. Every unassign returned within 0 ms. No WUDFVerifierFailure, UMDF restart or live dump since; before the fix the same setup failed several times per evening.
  • Not yet runtime-tested on master itself. The runtime results above are from the 24.12.24 backport (fix/swapchain-ddi-timeouts). On master the device comes from GetOrCreateDevice() on the processing thread, a path the backport does not exercise. Opened as a draft for that reason; a smoke test of assign/unassign, mode changes and monitor standby on this build would be needed before merging.

Not in this PR (possible follow-ups)

  • The device cache never checks GetDeviceRemovedReason(). After a TDR it keeps returning a removed device, which can cause an assign/fail loop.
  • vddlog writes synchronously to the named pipe, also inside DDI callbacks and under locks. A connected client that does not read could block a callback.
  • RebuildKnownMonitorModesCache() modifies the global mode list without a lock while mode callbacks may read it.
  • A VirtualDisplayDriverMouse event handle is created on every assignment and never closed (separate small fix available).

🤖 Generated with Claude Code

Tenebray84 and others added 2 commits October 30, 2026 01:00
IddCx guards EvtIddCxMonitorAssignSwapChain and EvtIddCxMonitorUnassignSwapChain
with a DDI watchdog (5 s by default). When it fires, IddCx reports a
WUDFVerifierFailure (Category=Driver, Driver=MttVDD.dll, ErrorNumber 0x10f for
assign, 0x10e for unassign), terminates WUDFHost and the virtual display goes
offline (Code 43). In practice this happened when Sunshine switched between the
virtual and a physical display after a long streaming session, leaving the
physical monitor black.

Three WUDFHost dumps (RTX 4080 SUPER, NVIDIA 621.01, Windows 11 26300, driver
24.12.24) show:
- 0x10f: AssignSwapChain created the D3D11 device synchronously inside the
  callback; loading the GPU user-mode driver took the time.
- 0x10e: UnassignSwapChain joined the processing thread with an INFINITE wait
  (by destroying the SwapChainProcessor) while that thread was blocked in
  WdfObjectDelete(swap-chain), releasing GPU allocations in the kernel.

IddCx disassembly also shows that, after UnassignSwapChain returns, IddCx
deletes the swap-chain itself unless the driver already did, and that it holds
the monitor lock while calling AssignSwapChain.

The current code still has both patterns: the per-adapter device cache avoids
repeated device creation, but the first device for an adapter (and every
retry after a failed creation) is still created inside AssignSwapChain, and
both AssignSwapChain (replaced processor) and UnassignSwapChain destroy the
old SwapChainProcessor synchronously, which joins its thread with an INFINITE
wait. In AssignSwapChain that join can also deadlock, because the old thread's
swap-chain cleanup needs the monitor lock IddCx is holding.

Changes:
- Obtain the D3D device (GetOrCreateDevice, unchanged) on the processing
  thread, as the IddCx documentation suggests; pause briefly before releasing
  the swap-chain if no device could be obtained.
- Unassign hands the swap-chain back to IddCx and only waits (bounded, 3 s)
  until the thread can no longer touch it; IddCx deletes it after the
  callback. A thread that stops on its own (e.g. DXGI_ERROR_ACCESS_LOST) still
  deletes it, as the documentation requires.
- Assign never waits for a previous processing thread. Stopped processors are
  kept until their threads exit, reaped on later assign/unassign calls and
  joined on device teardown.
- Hold a WDF reference on the swap-chain for the processor's lifetime.
- Use a manual-reset terminate event and check it on every loop iteration and
  before FinishedProcessingFrame, so a stop is noticed while frames keep coming.
- Delete the swap-chain if the processing thread cannot be started.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant