Repository navigation
Description
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,ReportBugcheckForDdiWatchdogDriverTookTooLongInDdiCallbuilds the code as(reason << 8) | ddi, so 0x10f = driver took too long in AssignSwapChain (DDI 0x0F) and 0x10e = … in UnassignSwapChain (DDI 0x0E).IddMonitor::HandleNewSwapChainarms 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,AssignSwapChaincallsDevice->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 inWdfObjectDelete(swap-chain) → IddSwapChain::Cleanup → DXGI → NtGdiDdDDIDestroyAllocation2in the kernel.
IddCx behaviour relevant for a fix (from disassembly of IddMonitor::HandleNewSwapChain and IddSwapChain::Cleanup):
- After
UnassignSwapChainreturns, IddCx takes the monitor lock and deletes the swap-chain itself (WdfObjectDelete) if the driver has not already done so.IddSwapChain::Cleanupclears 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 insideUnassignSwapChain, 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
RunCoreonly checks the terminate event in theE_PENDINGbranch, 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
VirtualDisplayDriverMouseevent handle is created on every swap-chain assignment and never closed. - 24.12.24 only: one processor for all monitors, and the global
s_KnownMonitorModes2grows on every mode query. Both are already different onmaster.
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