Repository navigation
Fix IddCx watchdog crashes in swap-chain assign/unassign - #532
Draft
Tenebray84 wants to merge 2 commits into
Draft
Tenebray84 wants to merge 2 commits into
Tenebray84 wants to merge 2 commits into
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #531.
Problem
IddCx guards
EvtIddCxMonitorAssignSwapChainandEvtIddCxMonitorUnassignSwapChainwith a DDI watchdog (5000 ms by default). If a callback takes longer, IddCx reports aWUDFVerifierFailure(Category=Driver, Driver=MttVDD.dll, ErrorNumber...10ffor assign,...10efor unassign), WUDFHost is terminated and the virtual display goes offline. With Sunshine'sensure_only_displaythis leaves the physical monitor black. Details, WER signatures and dump analysis are in #531.On
masterboth patterns still exist: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 runsD3D11CreateDevice(and loads the GPU user-mode driver) under the watchdog.processorToStop/previousProcessorare destroyed at the end of the callback, which waitsINFINITEfor the processing thread. In the dumps that thread was blocked inWdfObjectDelete(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.dllwith public symbols (Windows 11 build 26300); this is not documented, so please double-check:UnassignSwapChainreturns, IddCx takes the monitor lock and callsWdfObjectDeleteon the swap-chain if it is still attached.IddSwapChain::Cleanupdetaches 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.AssignSwapChain.EVT_IDD_CX_MONITOR_ASSIGN_SWAPCHAINdocumentation suggests creating the D3D device on the processing thread.Changes
SwapChainProcessorgets 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.IddCxSwapChainFinishedProcessingFrame, so a stop is noticed while frames keep arriving.Testing
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.masteritself. The runtime results above are from the 24.12.24 backport (fix/swapchain-ddi-timeouts). Onmasterthe device comes fromGetOrCreateDevice()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)
GetDeviceRemovedReason(). After a TDR it keeps returning a removed device, which can cause an assign/fail loop.vddlogwrites 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.VirtualDisplayDriverMouseevent handle is created on every assignment and never closed (separate small fix available).🤖 Generated with Claude Code