From 6f719ab450fee481b460c0b0f00cec02ab013451 Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 30 Oct 2026 01:00:25 +0100 Subject: [PATCH 1/2] Fix IddCx watchdog crashes in swap-chain assign/unassign 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 --- .../MttVDD/Driver.cpp | 239 +++++++++++++++--- Virtual Display Driver (HDR)/MttVDD/Driver.h | 39 ++- 2 files changed, 237 insertions(+), 41 deletions(-) diff --git a/Virtual Display Driver (HDR)/MttVDD/Driver.cpp b/Virtual Display Driver (HDR)/MttVDD/Driver.cpp index 3145b04a..773c1e91 100644 --- a/Virtual Display Driver (HDR)/MttVDD/Driver.cpp +++ b/Virtual Display Driver (HDR)/MttVDD/Driver.cpp @@ -3083,22 +3083,29 @@ HRESULT Direct3DDevice::Init() #pragma region SwapChainProcessor -SwapChainProcessor::SwapChainProcessor(IDDCX_SWAPCHAIN hSwapChain, shared_ptr Device, HANDLE NewFrameEvent) - : m_hSwapChain(hSwapChain), m_Device(Device), m_hAvailableBufferEvent(NewFrameEvent) +SwapChainProcessor::SwapChainProcessor(IDDCX_SWAPCHAIN hSwapChain, LUID RenderAdapter, HANDLE NewFrameEvent) + : m_hSwapChain(hSwapChain), m_RenderAdapter(RenderAdapter), m_hAvailableBufferEvent(NewFrameEvent) { stringstream logStream; logStream << "Constructing SwapChainProcessor:" << "\n SwapChain Handle: " << static_cast(hSwapChain) - << "\n Device Pointer: " << static_cast(Device.get()) + << "\n RenderAdapterLuid: " << RenderAdapter.LowPart << "-" << RenderAdapter.HighPart << "\n NewFrameEvent Handle: " << NewFrameEvent; vddlog("d", logStream.str().c_str()); - m_hTerminateEvent.Attach(CreateEvent(nullptr, FALSE, FALSE, nullptr)); - if (!m_hTerminateEvent.Get()) + // The handle is guaranteed valid during EvtIddCxMonitorAssignSwapChain. Hold a reference for the lifetime of the + // processor so that a late call after an unassign cannot touch freed memory; released in ReleaseSwapChain(). + WdfObjectReferenceWithTag((WDFOBJECT)m_hSwapChain, (PVOID)this); + + // Manual-reset events: a stop request and the loop-exit/release notifications must stay visible to every later wait. + m_hTerminateEvent.Attach(CreateEvent(nullptr, TRUE, FALSE, nullptr)); + m_hLoopExitedEvent.Attach(CreateEvent(nullptr, TRUE, FALSE, nullptr)); + m_hReleasedEvent.Attach(CreateEvent(nullptr, TRUE, FALSE, nullptr)); + if (!m_hTerminateEvent.Get() || !m_hLoopExitedEvent.Get() || !m_hReleasedEvent.Get()) { logStream.str(""); - logStream << "Failed to create terminate event. GetLastError: " << GetLastError(); + logStream << "Failed to create swap-chain processor events. GetLastError: " << GetLastError(); vddlog("e", logStream.str().c_str()); } else @@ -3106,15 +3113,22 @@ SwapChainProcessor::SwapChainProcessor(IDDCX_SWAPCHAIN hSwapChain, shared_ptr lock(m_StateLock); + if (m_SelfReleasing) + { + // The thread already left its loop on its own (e.g. DXGI_ERROR_ACCESS_LOST) and is deleting the + // swap-chain. IddCx will skip its own delete once the cleanup has started. + waitForRelease = true; + } + else + { + m_Revoked = true; + waitForLoopExit = m_InLoop; + } + } + RequestStop(); + + // Loop exit normally takes at most one swap-chain call plus one 16 ms wait. Releasing a swap-chain was observed + // to block for more than 5 s inside the GPU driver, so both waits are bounded below the IddCx DDI watchdog. + const ULONGLONG start = GetTickCount64(); + bool inTime = true; + if (waitForLoopExit) + { + inTime = WaitForSingleObject(m_hLoopExitedEvent.Get(), LoopExitBudgetMs) == WAIT_OBJECT_0; + } + else if (waitForRelease) + { + inTime = WaitForSingleObject(m_hReleasedEvent.Get(), ReleaseBudgetMs) == WAIT_OBJECT_0; + } + + stringstream logStream; + logStream << "Unassign: waited " << (GetTickCount64() - start) << " ms for " + << (waitForLoopExit ? "loop exit" : waitForRelease ? "swap-chain release" : "nothing") + << (inTime ? "." : " - budget exceeded."); + vddlog(inTime ? "d" : "e", logStream.str().c_str()); +} + +void SwapChainProcessor::ReleaseSwapChain() +{ + bool releaseHere; + { + lock_guard lock(m_StateLock); + m_InLoop = false; + releaseHere = !m_Revoked; + m_SelfReleasing = releaseHere; + } + SetEvent(m_hLoopExitedEvent.Get()); + + if (releaseHere) + { + // The driver still owns the swap-chain: delete it to make the OS provide a new one if necessary. + WdfObjectDelete((WDFOBJECT)m_hSwapChain); + vddlog("d", "Swap-chain object deleted."); + } + else + { + vddlog("d", "Swap-chain was unassigned; IddCx releases it."); + } + // Drop the reference taken in the constructor. Until here it kept the object's memory valid even if IddCx + // deleted the swap-chain concurrently. + WdfObjectDereferenceWithTag((WDFOBJECT)m_hSwapChain, (PVOID)this); + m_hSwapChain = nullptr; + SetEvent(m_hReleasedEvent.Get()); +} + +bool SwapChainProcessor::HasExited() const +{ + return !m_hThread.Get() || WaitForSingleObject(m_hThread.Get(), 0) == WAIT_OBJECT_0; +} + DWORD CALLBACK SwapChainProcessor::RunThread(LPVOID Argument) { stringstream logStream; @@ -3219,32 +3319,51 @@ void SwapChainProcessor::Run() vddlog("e", logStream.str().c_str()); } - RunCore(); - - logStream.str(""); - logStream << "Core processing function RunCore() completed."; - vddlog("d", logStream.str().c_str()); - - // Always delete the swap-chain object when swap-chain processing loop terminates in order to kick the system to - // provide a new swap-chain if necessary. - /* - WdfObjectDelete((WDFOBJECT)m_hSwapChain); - m_hSwapChain = nullptr; added error handling in so its not called to delete swap chain if its not needed. - */ - if (m_hSwapChain) + // Obtain the render device here rather than in EvtIddCxMonitorAssignSwapChain: creating it (loading the GPU + // user-mode driver) can take seconds, and IddCx reports a driver failure and terminates the host process if a DDI + // callback does not return within its watchdog timeout. + m_Device = IndirectDeviceContext::GetOrCreateDevice(m_RenderAdapter); + if (!m_Device) { - WdfObjectDelete((WDFOBJECT)m_hSwapChain); - logStream.str(""); - logStream << "Swap-chain object deleted."; - vddlog("d", logStream.str().c_str()); - m_hSwapChain = nullptr; + vddlog("e", "Failed to get or create Direct3DDevice, abandoning swap-chain."); + // Releasing the swap-chain makes the OS assign a new one right away. Pause briefly (interruptible) so a GPU that + // is temporarily unavailable, e.g. right after resume, does not cause a tight assign/fail loop. + WaitForSingleObject(m_hTerminateEvent.Get(), 250); } else { - logStream.str(""); - logStream << "No valid swap-chain object to delete."; - vddlog("w", logStream.str().c_str()); + // Enter the loop, i.e. start using the swap-chain, only if it was neither unassigned nor replaced while the + // device was being obtained. StopForUnassign() relies on m_InLoop to know whether it has to wait. + bool enterLoop = false; + { + lock_guard lock(m_StateLock); + if (!m_Revoked && WaitForSingleObject(m_hTerminateEvent.Get(), 0) != WAIT_OBJECT_0) + { + m_InLoop = true; + enterLoop = true; + } + } + + if (enterLoop) + { + RunCore(); + + logStream.str(""); + logStream << "Core processing function RunCore() completed."; + vddlog("d", logStream.str().c_str()); + } + else + { + vddlog("d", "Stop requested while obtaining the D3D device; device not attached."); + } } + + // Always release the swap-chain when processing ends: either delete it ourselves to kick the system to provide a + // new one, or leave it to IddCx if it was unassigned. + ReleaseSwapChain(); + // Drop this processor's device reference after the swap-chain, as soon as processing ends, instead of keeping it + // until the retired processor is reaped. The device itself stays in the per-adapter cache. + m_Device.reset(); /* AvRevertMmThreadCharacteristics(AvTaskHandle); */ //error handling when reversing multimedia thread characteristics @@ -3310,6 +3429,13 @@ void SwapChainProcessor::RunCore() // Acquire and release buffers in a loop for (;;) { + // Check for termination on every iteration: while frames keep arriving the E_PENDING wait below is never + // reached, so the terminate event would otherwise go unnoticed. + if (WaitForSingleObject(m_hTerminateEvent.Get(), 0) == WAIT_OBJECT_0) + { + break; + } + ComPtr AcquiredBuffer; // Ask for the next buffer from the producer @@ -3398,6 +3524,11 @@ void SwapChainProcessor::RunCore() AcquiredBuffer.Reset(); //vddlog("d", "Reset buffer"); + // If the swap-chain was unassigned meanwhile, make no further calls on it. + if (WaitForSingleObject(m_hTerminateEvent.Get(), 0) == WAIT_OBJECT_0) + { + break; + } hr = IddCxSwapChainFinishedProcessingFrame(m_hSwapChain); if (FAILED(hr)) { @@ -3667,17 +3798,25 @@ IndirectDeviceContext::~IndirectDeviceContext() { stringstream logStream; std::map> processingThreads; + std::vector> retiredProcessors; - logStream << "Destroying IndirectDeviceContext. Releasing per-monitor processing threads."; + logStream << "Destroying IndirectDeviceContext. Stopping current and retired per-monitor processing threads."; vddlog("d", logStream.str().c_str()); { std::lock_guard lock(m_ProcessingThreadsMutex); processingThreads.swap(m_ProcessingThreads); + retiredProcessors.swap(m_RetiredProcessors); } + // Device teardown, not a DDI callback: joining the remaining threads here is safe and required before unload. + const size_t currentCount = processingThreads.size(); + const size_t retiredCount = retiredProcessors.size(); + processingThreads.clear(); + retiredProcessors.clear(); + logStream.str(""); - logStream << "Released " << processingThreads.size() << " monitor processing thread(s)."; + logStream << "Released " << currentCount << " monitor processing thread(s) and " << retiredCount << " retired one(s)."; vddlog("d", logStream.str().c_str()); } @@ -3881,26 +4020,26 @@ void IndirectDeviceContext::AssignSwapChain(IDDCX_MONITOR Monitor, IDDCX_SWAPCHA CleanupExpiredDevices(); } - auto Device = GetOrCreateDevice(RenderAdapter); - if (!Device) - { - vddlog("e", "Failed to get or create Direct3DDevice, deleting existing swap-chain."); - WdfObjectDelete(SwapChain); - return; - } - else + // This runs inside an IddCx DDI callback that is guarded by a watchdog, and IddCx holds the monitor lock while + // calling it: never wait for an old processing thread here (its swap-chain cleanup needs that lock) and never do + // heavy work. A still-running previous processor keeps ownership of its swap-chain and releases it on its own; + // the D3D device for the new swap-chain is obtained (and created if necessary) on the new processing thread. { std::unique_ptr previousProcessor; - auto newProcessor = std::make_unique(SwapChain, Device, NewFrameEvent); + auto newProcessor = std::make_unique(SwapChain, RenderAdapter, NewFrameEvent); { std::lock_guard lock(m_ProcessingThreadsMutex); + ReapRetiredProcessors(); auto& processorSlot = m_ProcessingThreads[Monitor]; previousProcessor = std::move(processorSlot); processorSlot = std::move(newProcessor); } if (previousProcessor) { + // Destroying it here would join its thread while IddCx holds the monitor lock. + previousProcessor->RequestStop(); + RetireProcessor(std::move(previousProcessor)); vddlog("d", "Replaced existing processing thread for this monitor only."); } else { @@ -3964,6 +4103,7 @@ void IndirectDeviceContext::UnassignSwapChain(IDDCX_MONITOR Monitor) { std::lock_guard lock(m_ProcessingThreadsMutex); + ReapRetiredProcessors(); auto it = m_ProcessingThreads.find(Monitor); if (it != m_ProcessingThreads.end()) { @@ -3975,6 +4115,10 @@ void IndirectDeviceContext::UnassignSwapChain(IDDCX_MONITOR Monitor) if (processorToStop) { vddlog("i", "Unassigning swapchain for one monitor. Its processing thread will be stopped."); + // Returns once the thread no longer touches the swap-chain; IddCx deletes it after this callback. Destroying + // the processor here would join the thread, which can block inside the GPU driver beyond the DDI watchdog. + processorToStop->StopForUnassign(); + RetireProcessor(std::move(processorToStop)); } else { @@ -3982,6 +4126,21 @@ void IndirectDeviceContext::UnassignSwapChain(IDDCX_MONITOR Monitor) } } +void IndirectDeviceContext::RetireProcessor(std::unique_ptr Processor) +{ + // Keep the processor until its thread has exited; joining here could block the DDI callback. + std::lock_guard lock(m_ProcessingThreadsMutex); + m_RetiredProcessors.push_back(std::move(Processor)); +} + +void IndirectDeviceContext::ReapRetiredProcessors() +{ + m_RetiredProcessors.erase( + std::remove_if(m_RetiredProcessors.begin(), m_RetiredProcessors.end(), + [](const std::unique_ptr& Processor) { return Processor->HasExited(); }), + m_RetiredProcessors.end()); +} + #pragma endregion #pragma region DDI Callbacks diff --git a/Virtual Display Driver (HDR)/MttVDD/Driver.h b/Virtual Display Driver (HDR)/MttVDD/Driver.h index b48b97ff..b7a0c53c 100644 --- a/Virtual Display Driver (HDR)/MttVDD/Driver.h +++ b/Virtual Display Driver (HDR)/MttVDD/Driver.h @@ -67,25 +67,53 @@ namespace Microsoft /// /// Manages a thread that consumes buffers from an indirect display swap-chain object. + /// The Direct3D device is obtained on that thread, not in the IddCx DDI callback, because IddCx reports a + /// driver failure and terminates the host process if AssignSwapChain/UnassignSwapChain exceed its watchdog + /// timeout (5 s by default). /// class SwapChainProcessor { public: - SwapChainProcessor(IDDCX_SWAPCHAIN hSwapChain, std::shared_ptr Device, HANDLE NewFrameEvent); + SwapChainProcessor(IDDCX_SWAPCHAIN hSwapChain, LUID RenderAdapter, HANDLE NewFrameEvent); ~SwapChainProcessor(); + // Signals the processing thread to stop without waiting for it. The thread keeps ownership of the + // swap-chain and deletes it when it exits. + void RequestStop(); + // For EvtIddCxMonitorUnassignSwapChain: hands the swap-chain back to IddCx (which deletes it after the + // callback) and returns once the thread no longer uses it, waiting at most LoopExitBudgetMs. + void StopForUnassign(); + // True once the processing thread has exited (or was never started). + bool HasExited() const; + private: static DWORD CALLBACK RunThread(LPVOID Argument); void Run(); void RunCore(); + // Deletes the swap-chain unless it was handed back to IddCx, and signals loop exit and release. + void ReleaseSwapChain(); + + // StopForUnassign() waits for at most one of these; both are well below the IddCx DDI watchdog + // (5000 ms by default). + static constexpr DWORD LoopExitBudgetMs = 3000; + static constexpr DWORD ReleaseBudgetMs = 3000; public: IDDCX_SWAPCHAIN m_hSwapChain; + LUID m_RenderAdapter; std::shared_ptr m_Device; HANDLE m_hAvailableBufferEvent; Microsoft::WRL::Wrappers::Thread m_hThread; Microsoft::WRL::Wrappers::Event m_hTerminateEvent; + + private: + std::mutex m_StateLock; + bool m_Revoked = false; // swap-chain handed back to IddCx by StopForUnassign() + bool m_InLoop = false; // thread may be calling IddCx swap-chain functions + bool m_SelfReleasing = false; // thread decided to delete the swap-chain itself + Microsoft::WRL::Wrappers::Event m_hLoopExitedEvent; + Microsoft::WRL::Wrappers::Event m_hReleasedEvent; }; /// @@ -119,6 +147,10 @@ namespace Microsoft void UnassignSwapChain(IDDCX_MONITOR Monitor); protected: + // Keeps a stopped processor until its thread has exited, without joining it. + void RetireProcessor(std::unique_ptr Processor); + // Destroys retired processors whose threads have exited. Caller holds m_ProcessingThreadsMutex. + void ReapRetiredProcessors(); WDFDEVICE m_WdfDevice; IDDCX_ADAPTER m_Adapter; @@ -126,6 +158,8 @@ namespace Microsoft IDDCX_MONITOR m_Monitor2; std::map> m_ProcessingThreads; + // Stopped processors whose threads may still be running; reaped once they have exited, joined on teardown. + std::vector> m_RetiredProcessors; std::mutex m_ProcessingThreadsMutex; public: @@ -133,6 +167,9 @@ namespace Microsoft static std::vector s_KnownMonitorEdid; private: + // The processing thread obtains its render device from the cache (see SwapChainProcessor::Run()). + friend class SwapChainProcessor; + static std::map, LuidComparator> s_DeviceCache; static std::mutex s_DeviceCacheMutex; static std::shared_ptr GetOrCreateDevice(LUID RenderAdapter); From 22a007702af3aedf751dcc7a716c0b4a8576a58b Mon Sep 17 00:00:00 2001 From: Marc Date: Fri, 30 Oct 2026 01:05:14 +0100 Subject: [PATCH 2/2] Include for std::remove_if Co-Authored-By: Claude Opus 5.5 --- Virtual Display Driver (HDR)/MttVDD/Driver.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/Virtual Display Driver (HDR)/MttVDD/Driver.cpp b/Virtual Display Driver (HDR)/MttVDD/Driver.cpp index 773c1e91..de5ea351 100644 --- a/Virtual Display Driver (HDR)/MttVDD/Driver.cpp +++ b/Virtual Display Driver (HDR)/MttVDD/Driver.cpp @@ -32,6 +32,7 @@ Copyright (c) Microsoft Corporation #include #include #include +#include #include #include #include