Skip to content

[Bug]: IddCx watchdog kills the driver in Assign/UnassignSwapChain (WUDFVerifierFailure 0x10f / 0x10e): virtual display goes offline, physical monitor stays black #531

Open

Description

@Tenebray84

Summary

When Sunshine switches the desktop between the virtual display and a physical monitor (dd_configuration_option = ensure_only_display), MttVDD can be terminated by the IddCx DDI watchdog. WUDFHost is torn down, the virtual display goes offline (Code 43) and the topology is left in a state where the physical monitor stays black ("second screen only") until a manual fix or reset. It happened reliably after longer streaming sessions; switching in either direction can trigger it.

The root cause is in the driver: heavy and unbounded work inside EvtIddCxMonitorAssignSwapChain / EvtIddCxMonitorUnassignSwapChain, which IddCx guards with a ~5 s watchdog. A fix is in #532.

Environment

  • Virtual Display Driver 24.12.24 (driver 11.30.4.434, SignPath-signed). The 25.7.23 driver-only package contains the identical MttVDD.dll.
  • Windows 11 build 26300, RTX 4080 SUPER, NVIDIA 621.01
  • Sunshine v2026.914.233613, ensure_only_display, client resolution 3456x2160 @ 120 Hz
  • One virtual monitor

Evidence

Three WER reports, all with the same signature:

EventType=WUDFVerifierFailure
FoundBy=Framework
Category=Driver
Location=minkernel\wdf\framework\shared\object\fxverifierbugcheck.cpp:188 (FxVerifierDriverReportedBugcheck)
Driver=MttVDD.dll
ErrorNumber=0x50100040000010f  (twice)  /  0x50100040000010e  (once)
UMDFVersion=2.35.0

Followed by UMDF event 10111 ("device offline because of a user-mode driver failure"), a Kernel_193 (VIDEO_DXGKRNL_LIVEDUMP) live dump and Sunshine failing to revert ("Device … does not exist in the available path source data").

Analysis of the three WUDFHost dumps (C:\ProgramData\Microsoft\WDF\WudfHost_ext__*.mdmp) with public symbols:

  • The reporting thread is IddCx!IddWatchdog::WatchdogThread → IddAdapter::ReportBugcheck → WdfCxVerifierKeBugCheck. In IddCx, ReportBugcheckForDdiWatchdogDriverTookTooLongInDdiCall builds the code as (reason << 8) | ddi, so 0x10f = driver took too long in AssignSwapChain (DDI 0x0F) and 0x10e = … in UnassignSwapChain (DDI 0x0E). IddMonitor::HandleNewSwapChain arms the watchdog with 5000 ms before both callbacks. (These are not the kernel bugchecks 0x10E/0x10F.)
  • 0x10f: the driver thread is in IndirectDeviceContext::AssignSwapChain → Direct3DDevice::Init → D3D11CreateDevice → nvldumdx/nvwgf2umx OpenAdapter …. In 24.12.24, AssignSwapChain calls Device->Init() twice in a row (Driver.cpp lines 2446 and 2452), synchronously inside the callback; each call loads the GPU user-mode driver (in one dump the NVIDIA loader was busy verifying an Authenticode signature).
  • 0x10e: the callback is in UnassignSwapChain → m_ProcessingThread.reset() → ~SwapChainProcessor → WaitForSingleObject(m_hThread, INFINITE) (line 1848), while the processing thread sits in WdfObjectDelete(swap-chain) → IddSwapChain::Cleanup → DXGI → NtGdiDdDDIDestroyAllocation2 in the kernel.

IddCx behaviour relevant for a fix (from disassembly of IddMonitor::HandleNewSwapChain and IddSwapChain::Cleanup):

  • After UnassignSwapChain returns, IddCx takes the monitor lock and deletes the swap-chain itself (WdfObjectDelete) if the driver has not already done so. IddSwapChain::Cleanup clears the monitor's swap-chain pointer under that lock before the slow COM releases. So the driver does not have to delete the swap-chain inside UnassignSwapChain, but it must not use the handle anymore once the callback returns.
  • IddCx holds the monitor lock while calling AssignSwapChain, so waiting there for an old processing thread whose cleanup needs that lock can deadlock.
  • The swap-chain DDIs (ReleaseAndAcquireBuffer2, SetDevice) only look up the WDF context; there is no generation check, so calls after the swap-chain was freed are use-after-free.

The documentation of EVT_IDD_CX_MONITOR_ASSIGN_SWAPCHAIN suggests the same approach: "an IDD might create a new thread to initialize D3D devices and then loop on processing frames".

Other issues found in the same code

  • RunCore only checks the terminate event in the E_PENDING branch, so while frames keep coming the thread does not notice a stop request (same as described by basicfu in Displays disconnect after some time and become CPU-intensive #100).
  • A new VirtualDisplayDriverMouse event handle is created on every swap-chain assignment and never closed.
  • 24.12.24 only: one processor for all monitors, and the global s_KnownMonitorModes2 grows on every mode query. Both are already different on master.

Test of the fix

Built from 24.12.24 with the fix (branch fix/swapchain-ddi-timeouts in my fork), signed locally and run daily:

  • one session of almost 4 hours at 3456x2160 @ 120 Hz, plus several short sessions with switches in both directions and overnight monitor standby;
  • every unassign returned immediately (0 ms wait), the swap-chain was released by IddCx or, after DXGI_ERROR_ACCESS_LOST, by the thread itself;
  • no WUDFVerifierFailure, UMDF restart, live dump or black screen since.

Before the fix, the same setup failed several times within one evening.

Possibly related

#343 (no response after monitor sleep, primary display moves to VDD), #474 (black screen, RTX 4080 over DP), #277 (only VDD active after Moonlight ends), #100 (terminate event not seen while frames keep coming), #490 (DWM/DXGI instability with several virtual monitors). I could not verify that they share this root cause; the WER signature above is what distinguishes it.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions