diff --git a/idd/LGCommon/PipeMsg.h b/idd/LGCommon/PipeMsg.h index 3a70e00d..1c6473c1 100644 --- a/idd/LGCommon/PipeMsg.h +++ b/idd/LGCommon/PipeMsg.h @@ -24,18 +24,30 @@ static constexpr wchar_t LG_PIPE_NAME[] = L"\\\\.\\pipe\\LookingGlassIDD"; +#pragma pack(push, 4) struct LGPipeMsg { unsigned size; - enum + enum Type : uint32_t { SETCURSORPOS, SETDISPLAYMODE, GPUSTATUS, RELOADSETTINGS, - RESOLUTIONREJECTED + RESOLUTIONREJECTED, + SET_RECOVERY, + RECOVERY_OFF, + RECOVERY_ON, + RECOVERY_FAILED, + RECOVERY_NO_DISPLAY } type; + + enum : uint32_t + { + RECOVERY_ACTIVE = 0x1U + }; + union { struct @@ -66,7 +78,15 @@ struct LGPipeMsg uint32_t requiredSizeMiB; } resolutionRejected; + + struct + { + uint64_t session; + uint32_t request; + } + recovery; }; }; +#pragma pack(pop) static_assert(sizeof(LGPipeMsg) == 20, "LGPipeMsg wire layout changed"); diff --git a/idd/LGIdd/LGIdd.vcxproj b/idd/LGIdd/LGIdd.vcxproj index 38165c86..c93a4492 100644 --- a/idd/LGIdd/LGIdd.vcxproj +++ b/idd/LGIdd/LGIdd.vcxproj @@ -62,6 +62,7 @@ + @@ -114,6 +115,7 @@ + diff --git a/idd/LGIdd/LGIdd.vcxproj.filters b/idd/LGIdd/LGIdd.vcxproj.filters index 6eff672f..698960dd 100644 --- a/idd/LGIdd/LGIdd.vcxproj.filters +++ b/idd/LGIdd/LGIdd.vcxproj.filters @@ -181,6 +181,9 @@ Transport\LGMP + + Transport\LGMP + Transport\LGMP @@ -294,6 +297,9 @@ Transport\LGMP + + Transport\LGMP + Transport\LGMP diff --git a/idd/LGIdd/display/CDeviceContext.cpp b/idd/LGIdd/display/CDeviceContext.cpp index 49abb765..7cb0d6ee 100644 --- a/idd/LGIdd/display/CDeviceContext.cpp +++ b/idd/LGIdd/display/CDeviceContext.cpp @@ -44,8 +44,14 @@ CDeviceContext::CDeviceContext(WDFDEVICE wdfDevice) : CDeviceContext::~CDeviceContext() { - // Both callbacks dereference this context. Drain them before the subsystem + // These callbacks dereference this context. Drain them before the subsystem // members are destroyed in frame, control, host order. + if (m_recoveryHandlerSet) + { + g_pipe.ClearRecoveryHandler(this); + m_recoveryHandlerSet = false; + } + if (m_initTimer) { WdfTimerStop(m_initTimer, TRUE); @@ -379,8 +385,9 @@ void CDeviceContext::FinishInit(UINT connectorIndex) { CDisplayConfiguration::Description description = m_displayConfiguration.GetDescription(); - m_monitorManager.Create( - connectorIndex, m_adapter, std::move(description.edid), this); + if (m_monitorManager.Create( + connectorIndex, m_adapter, std::move(description.edid), this)) + m_transport->SyncRecovery(); } void CDeviceContext::ReplugMonitor() @@ -460,7 +467,90 @@ void CDeviceContext::SetResolution(uint32_t width, uint32_t height) bool CDeviceContext::InitializeTransport() { - return m_transport && m_transport->Initialize(); + if (!m_transport) + return false; + + if (m_transportTimer) + return true; + + g_pipe.SetRecoveryHandler( + [](void * opaque, uint64_t session, uint32_t serial, bool active, + LGPipeMsg::Type result) + { + CDeviceContext * context = + static_cast(opaque); + + ITransport::Recovery state = ITransport::Recovery::FAILED; + uint32_t error = ERROR_SUCCESS; + switch (result) + { + case LGPipeMsg::RECOVERY_OFF: + state = ITransport::Recovery::NORMAL; + break; + + case LGPipeMsg::RECOVERY_ON: + state = ITransport::Recovery::ACTIVE; + break; + + case LGPipeMsg::RECOVERY_FAILED: + error = ERROR_GEN_FAILURE; + break; + + case LGPipeMsg::RECOVERY_NO_DISPLAY: + error = ERROR_NOT_FOUND; + break; + + default: + return; + } + + context->m_transport->RecoveryStatus( + session, serial, active, state, error); + }, + this); + m_recoveryHandlerSet = true; + + // Claim the pipe recovery channel before initializing the producer session + // so no request cached by a prior device context can cross the handoff. + if (!m_transport->Initialize()) + { + g_pipe.ClearRecoveryHandler(this); + m_recoveryHandlerSet = false; + return false; + } + + WDF_TIMER_CONFIG config; + WDF_TIMER_CONFIG_INIT_PERIODIC(&config, + [](WDFTIMER timer) -> void + { + WDFOBJECT parent = WdfTimerGetParentObject(timer); + auto wrapper = WdfObjectGet_CDeviceContextWrapper(parent); + wrapper->context->TransportTimer(); + }, + 10); + config.AutomaticSerialization = FALSE; + + /** + * Documentation states that Dispatch is not available under UMDF, + * however using Passive returns a not-supported error and Dispatch works. + */ + WDF_OBJECT_ATTRIBUTES attribs; + WDF_OBJECT_ATTRIBUTES_INIT(&attribs); + attribs.ParentObject = m_wdfDevice; + attribs.ExecutionLevel = WdfExecutionLevelDispatch; + + NTSTATUS status = WdfTimerCreate( + &config, &attribs, &m_transportTimer); + if (!NT_SUCCESS(status)) + { + g_pipe.ClearRecoveryHandler(this); + m_recoveryHandlerSet = false; + DEBUG_ERROR_HR(status, "Transport timer creation failed"); + return false; + } + + WdfTimerStart(m_transportTimer, WDF_REL_TIMEOUT_IN_MS(10)); + return true; } bool CDeviceContext::SetupTransport(size_t alignSize) @@ -471,36 +561,6 @@ bool CDeviceContext::SetupTransport(size_t alignSize) { if (!InitializeTransport() || !m_transport->Setup(alignSize)) return false; - - WDF_TIMER_CONFIG config; - WDF_TIMER_CONFIG_INIT_PERIODIC(&config, - [](WDFTIMER timer) -> void - { - WDFOBJECT parent = WdfTimerGetParentObject(timer); - auto wrapper = WdfObjectGet_CDeviceContextWrapper(parent); - wrapper->context->TransportTimer(); - }, - 10); - config.AutomaticSerialization = FALSE; - - /** - * Documentation states that Dispatch is not available under UMDF, - * however using Passive returns a not-supported error and Dispatch works. - */ - WDF_OBJECT_ATTRIBUTES attribs; - WDF_OBJECT_ATTRIBUTES_INIT(&attribs); - attribs.ParentObject = m_wdfDevice; - attribs.ExecutionLevel = WdfExecutionLevelDispatch; - - NTSTATUS status = WdfTimerCreate( - &config, &attribs, &m_transportTimer); - if (!NT_SUCCESS(status)) - { - DEBUG_ERROR_HR(status, "Timer creation failed"); - return false; - } - - WdfTimerStart(m_transportTimer, WDF_REL_TIMEOUT_IN_MS(10)); } IInputTransport * input = m_transport->Input(); @@ -542,3 +602,9 @@ void CDeviceContext::OnSetResolution(uint32_t width, uint32_t height) { SetResolution(width, height); } + +void CDeviceContext::OnRecoveryRequest( + uint64_t session, uint32_t serial, bool active) +{ + g_pipe.SetRecovery(this, session, serial, active); +} diff --git a/idd/LGIdd/display/CDeviceContext.h b/idd/LGIdd/display/CDeviceContext.h index eadef141..c3ed9677 100644 --- a/idd/LGIdd/display/CDeviceContext.h +++ b/idd/LGIdd/display/CDeviceContext.h @@ -51,7 +51,8 @@ private: CDisplayConfiguration m_displayConfiguration; CMonitorManager m_monitorManager; - WDFTIMER m_transportTimer = nullptr; + WDFTIMER m_transportTimer = nullptr; + bool m_recoveryHandlerSet = false; UINT m_iddCxVersion = 0; bool m_hasIddCx110DDIs = false; @@ -67,6 +68,8 @@ private: void TransportTimer(); void OnSetCursorPos(int32_t x, int32_t y) override; void OnSetResolution(uint32_t width, uint32_t height) override; + void OnRecoveryRequest( + uint64_t session, uint32_t serial, bool active) override; void SetResolution(uint32_t width, uint32_t height); public: diff --git a/idd/LGIdd/display/CMonitorManager.cpp b/idd/LGIdd/display/CMonitorManager.cpp index 9409d146..3229b337 100644 --- a/idd/LGIdd/display/CMonitorManager.cpp +++ b/idd/LGIdd/display/CMonitorManager.cpp @@ -23,7 +23,7 @@ #include "display/CMonitorContext.h" #include "CDebug.h" -void CMonitorManager::Create(UINT connectorIndex, IDDCX_ADAPTER adapter, +bool CMonitorManager::Create(UINT connectorIndex, IDDCX_ADAPTER adapter, std::vector edid, CDeviceContext * owner) { DEBUG_INFO("Creating monitor on connector %u", connectorIndex); @@ -38,7 +38,7 @@ void CMonitorManager::Create(UINT connectorIndex, IDDCX_ADAPTER adapter, if (haveMonitor) { DEBUG_WARN("FinishInit skipped: a monitor already exists"); - return; + return false; } WDF_OBJECT_ATTRIBUTES attr; @@ -61,7 +61,7 @@ void CMonitorManager::Create(UINT connectorIndex, IDDCX_ADAPTER adapter, if (FAILED(hr)) { DEBUG_ERROR_HR(hr, "Failed to create the monitor container ID"); - return; + return false; } IDARG_IN_MONITORCREATE create = {}; @@ -73,7 +73,7 @@ void CMonitorManager::Create(UINT connectorIndex, IDDCX_ADAPTER adapter, if (!NT_SUCCESS(status)) { DEBUG_ERROR_HR(status, "IddCxMonitorCreate Failed"); - return; + return false; } DEBUG_INFO("Monitor object created (%p)", createOut.MonitorObject); @@ -91,10 +91,11 @@ void CMonitorManager::Create(UINT connectorIndex, IDDCX_ADAPTER adapter, if (FAILED(status)) { DEBUG_ERROR_HR(status, "IddCxMonitorArrival Failed"); - return; + return false; } DEBUG_INFO("Monitor arrival reported successfully"); + return true; } CMonitorManager::ReplugAction CMonitorManager::Replug() diff --git a/idd/LGIdd/display/CMonitorManager.h b/idd/LGIdd/display/CMonitorManager.h index af03a967..9e50bb5a 100644 --- a/idd/LGIdd/display/CMonitorManager.h +++ b/idd/LGIdd/display/CMonitorManager.h @@ -75,7 +75,7 @@ private: std::atomic m_replugQueued = 0; public: - void Create(UINT connectorIndex, IDDCX_ADAPTER adapter, + bool Create(UINT connectorIndex, IDDCX_ADAPTER adapter, std::vector edid, CDeviceContext * owner); ReplugAction Replug(); void RequestMode(const CSettings::DisplayMode& mode); diff --git a/idd/LGIdd/ipc/CPipeServer.cpp b/idd/LGIdd/ipc/CPipeServer.cpp index 53902c48..d01b0ff2 100644 --- a/idd/LGIdd/ipc/CPipeServer.cpp +++ b/idd/LGIdd/ipc/CPipeServer.cpp @@ -52,6 +52,12 @@ void CPipeServer::OnPipeConnected() QueueMsgLocked(queued[i]); break; } + + // Recovery is latched state rather than a one-shot command. Reapply the + // latest request whenever the helper reconnects so a helper restart cannot + // silently restore the IDD-only topology while recovery is active. + if (m_recoveryValid) + m_endpoint.Send(&m_recoveryRequest, sizeof(m_recoveryRequest)); } bool CPipeServer::OnPipeMessage(const void * message, size_t size) @@ -69,6 +75,13 @@ bool CPipeServer::OnPipeMessage(const void * message, size_t size) HandleReloadSettings(); return true; + case LGPipeMsg::RECOVERY_OFF: + case LGPipeMsg::RECOVERY_ON: + case LGPipeMsg::RECOVERY_FAILED: + case LGPipeMsg::RECOVERY_NO_DISPLAY: + HandleRecovery(msg); + return true; + default: DEBUG_ERROR("Unknown message type %d", msg.type); return true; @@ -103,12 +116,59 @@ void CPipeServer::HandleReloadSettings() m_deviceContext->ReloadSettings(); } +void CPipeServer::HandleRecovery(const LGPipeMsg & msg) +{ + CSRWSharedLock queueLock(m_queueLock); + if (!m_recoveryValid || + msg.recovery.session != m_recoveryRequest.recovery.session || + msg.recovery.request != m_recoveryRequest.recovery.request) + { + DEBUG_WARN("Ignoring stale recovery status"); + return; + } + + const uint32_t serial = + msg.recovery.request & ~LGPipeMsg::RECOVERY_ACTIVE; + const bool active = + (msg.recovery.request & LGPipeMsg::RECOVERY_ACTIVE) != 0; + + CSRWSharedLock recoveryLock(m_recoveryLock); + queueLock.Unlock(); + if (m_recoveryHandler) + m_recoveryHandler(m_recoveryOpaque, + msg.recovery.session, serial, active, msg.type); +} + void CPipeServer::SetDeviceContext(CDeviceContext * context) { CSRWExclusiveLock lock(m_deviceContextLock); m_deviceContext = context; } +void CPipeServer::SetRecoveryHandler( + RecoveryHandler handler, void * opaque) +{ + CSRWExclusiveLock queueLock(m_queueLock); + CSRWExclusiveLock recoveryLock(m_recoveryLock); + m_recoveryValid = false; + m_recoveryRequest = {}; + m_recoveryHandler = handler; + m_recoveryOpaque = opaque; +} + +void CPipeServer::ClearRecoveryHandler(void * opaque) +{ + CSRWExclusiveLock queueLock(m_queueLock); + CSRWExclusiveLock recoveryLock(m_recoveryLock); + if (m_recoveryOpaque != opaque) + return; + + m_recoveryValid = false; + m_recoveryRequest = {}; + m_recoveryHandler = nullptr; + m_recoveryOpaque = nullptr; +} + void CPipeServer::SetCursorPos(uint32_t x, uint32_t y) { // do not send cursor messages if we are not connected or they will end up queued @@ -129,8 +189,8 @@ void CPipeServer::SetDisplayMode( uint32_t width, uint32_t height, uint32_t refreshMilliHz) { LGPipeMsg msg = {}; - msg.size = sizeof(msg); - msg.type = LGPipeMsg::SETDISPLAYMODE; + msg.size = sizeof(msg); + msg.type = LGPipeMsg::SETDISPLAYMODE; msg.displayMode.width = width; msg.displayMode.height = height; msg.displayMode.refreshMilliHz = refreshMilliHz; @@ -150,10 +210,37 @@ void CPipeServer::ResolutionRejected(uint32_t width, uint32_t height, uint32_t requiredSizeMiB) { LGPipeMsg msg = {}; - msg.size = sizeof(msg); - msg.type = LGPipeMsg::RESOLUTIONREJECTED; - msg.resolutionRejected.width = width; - msg.resolutionRejected.height = height; + msg.size = sizeof(msg); + msg.type = LGPipeMsg::RESOLUTIONREJECTED; + msg.resolutionRejected.width = width; + msg.resolutionRejected.height = height; msg.resolutionRejected.requiredSizeMiB = requiredSizeMiB; WriteMsg(msg); } + +void CPipeServer::SetRecovery( + void * owner, uint64_t session, uint32_t serial, bool active) +{ + if (!session || !serial || + (serial & LGPipeMsg::RECOVERY_ACTIVE)) + { + DEBUG_ERROR("Invalid recovery request correlation"); + return; + } + + LGPipeMsg msg = {}; + msg.size = sizeof(msg); + msg.type = LGPipeMsg::SET_RECOVERY; + msg.recovery.session = session; + msg.recovery.request = serial | + (active ? LGPipeMsg::RECOVERY_ACTIVE : 0U); + + CSRWExclusiveLock queueLock(m_queueLock); + CSRWSharedLock recoveryLock(m_recoveryLock); + if (!m_recoveryHandler || m_recoveryOpaque != owner) + return; + + m_recoveryValid = true; + m_recoveryRequest = msg; + m_endpoint.Send(&msg, sizeof(msg)); +} diff --git a/idd/LGIdd/ipc/CPipeServer.h b/idd/LGIdd/ipc/CPipeServer.h index e3e8044a..16d7e7a7 100644 --- a/idd/LGIdd/ipc/CPipeServer.h +++ b/idd/LGIdd/ipc/CPipeServer.h @@ -33,18 +33,30 @@ class CDeviceContext; class CPipeServer : private IPipeEndpointHandler { + public: + using RecoveryHandler = void (*)(void * opaque, + uint64_t session, uint32_t serial, bool active, + LGPipeMsg::Type result); + private: CPipeEndpoint m_endpoint; CSRWLock m_queueLock; std::vector m_queue; + bool m_recoveryValid = false; + LGPipeMsg m_recoveryRequest = {}; CSRWLock m_deviceContextLock; - CDeviceContext * m_deviceContext = nullptr; + CDeviceContext * m_deviceContext = nullptr; + + CSRWLock m_recoveryLock; + RecoveryHandler m_recoveryHandler = nullptr; + void * m_recoveryOpaque = nullptr; void WriteMsg(const LGPipeMsg & msg); void QueueMsgLocked(const LGPipeMsg & msg); void HandleReloadSettings(); + void HandleRecovery(const LGPipeMsg & msg); void OnPipeConnected() override; bool OnPipeMessage(const void * message, size_t size) override; @@ -56,6 +68,8 @@ class CPipeServer : private IPipeEndpointHandler void DeInit(); void SetDeviceContext(CDeviceContext * context); + void SetRecoveryHandler(RecoveryHandler handler, void * opaque); + void ClearRecoveryHandler(void * opaque); void SetCursorPos(uint32_t x, uint32_t y); void SetDisplayMode( @@ -63,6 +77,8 @@ class CPipeServer : private IPipeEndpointHandler void SetGPUStatus(bool software); void ResolutionRejected(uint32_t width, uint32_t height, uint32_t requiredSizeMiB); + void SetRecovery( + void * owner, uint64_t session, uint32_t serial, bool active); }; extern CPipeServer g_pipe; diff --git a/idd/LGIdd/transport/ITransport.h b/idd/LGIdd/transport/ITransport.h index b093789c..e38b0536 100644 --- a/idd/LGIdd/transport/ITransport.h +++ b/idd/LGIdd/transport/ITransport.h @@ -37,6 +37,8 @@ public: virtual void OnSetCursorPos(int32_t x, int32_t y) = 0; virtual void OnSetResolution(uint32_t width, uint32_t height) = 0; + virtual void OnRecoveryRequest( + uint64_t session, uint32_t serial, bool active) = 0; }; class ITransport @@ -49,12 +51,22 @@ public: FAILURE, }; + enum class Recovery + { + NORMAL, + ACTIVE, + FAILED, + }; + virtual ~ITransport() = default; virtual OpenResult Open() = 0; virtual bool Initialize() = 0; virtual bool Setup(size_t alignment) = 0; virtual void Process(ITransportEvents& events) = 0; + virtual void SyncRecovery() {} + virtual void RecoveryStatus( + uint64_t, uint32_t, bool, Recovery, uint32_t) {} virtual FrameMemoryLimits GetMemoryLimits() const = 0; virtual DirectFrameBufferMemory GetDirectMemory() const = 0; diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp index 6d3ef3f3..a6756633 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp +++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.cpp @@ -22,6 +22,7 @@ #include "CDebug.h" #include "common/KVMFR.h" +#include "common/KVMFRRecovery.h" static bool TranslateFrameScheduleFlags( uint32_t source, uint32_t& destination) @@ -67,6 +68,9 @@ ITransport::OpenResult CLGMPTransport::Open() bool CLGMPTransport::Initialize() { + if (!m_recovery.Initialize(m_ivshmem)) + return false; + if (!m_host.Initialize(m_ivshmem)) return false; @@ -82,11 +86,26 @@ bool CLGMPTransport::Initialize() bool CLGMPTransport::Setup(size_t alignment) { - return m_frames.Setup(alignment); + if (!m_frames.Setup(alignment)) + return false; + + m_ready.store(true, std::memory_order_release); + return true; } void CLGMPTransport::Process(ITransportEvents& events) { + const CRecovery::Request recovery = m_recovery.Process(); + if (recovery.valid) + events.OnRecoveryRequest( + recovery.session, recovery.serial, recovery.active); + + // Before the swap chain establishes the frame-buffer alignment, service + // only the protocol-independent recovery channel. This preserves the old + // transport startup boundary while keeping recovery available immediately. + if (!m_ready.load(std::memory_order_acquire)) + return; + const LGMP_STATUS processStatus = m_host.Process(); if (processStatus != LGMP_OK) { @@ -174,6 +193,39 @@ void CLGMPTransport::Process(ITransportEvents& events) m_control.ResendState(); } +void CLGMPTransport::SyncRecovery() +{ + m_recovery.Sync(); +} + +void CLGMPTransport::RecoveryStatus( + uint64_t session, uint32_t serial, bool active, + Recovery state, uint32_t error) +{ + uint32_t wireState = KVMFR_R_STATE_FAILED; + uint32_t wireError = KVMFR_R_ERR_NONE; + switch (state) + { + case Recovery::NORMAL: + wireState = KVMFR_R_STATE_NORMAL; + break; + + case Recovery::ACTIVE: + wireState = KVMFR_R_STATE_ACTIVE; + break; + + case Recovery::FAILED: + wireState = KVMFR_R_STATE_FAILED; + wireError = error == ERROR_NOT_FOUND ? + KVMFR_R_ERR_NO_FALLBACK_DISPLAY : + KVMFR_R_ERR_TOPOLOGY_FAILED; + break; + } + + m_recovery.SetStatus( + session, serial, active, wireState, wireError); +} + FrameMemoryLimits CLGMPTransport::GetMemoryLimits() const { return m_frames.GetMemoryLimits(); diff --git a/idd/LGIdd/transport/lgmp/CLGMPTransport.h b/idd/LGIdd/transport/lgmp/CLGMPTransport.h index 204f2e28..5d97f361 100644 --- a/idd/LGIdd/transport/lgmp/CLGMPTransport.h +++ b/idd/LGIdd/transport/lgmp/CLGMPTransport.h @@ -26,6 +26,9 @@ #include "transport/lgmp/CLGMPFrameTransport.h" #include "transport/lgmp/CLGMPHost.h" #include "transport/lgmp/CLGMPInputTransport.h" +#include "transport/lgmp/CRecovery.h" + +#include class CLGMPTransport final : public ITransport { @@ -37,6 +40,8 @@ private: CLGMPControl m_control; CLGMPFrameTransport m_frames; CLGMPInputTransport m_input; + CRecovery m_recovery; + std::atomic m_ready = false; public: CLGMPTransport(); @@ -49,6 +54,10 @@ public: bool Initialize() override; bool Setup(size_t alignment) override; void Process(ITransportEvents& events) override; + void SyncRecovery() override; + void RecoveryStatus( + uint64_t session, uint32_t serial, bool active, + Recovery state, uint32_t error) override; FrameMemoryLimits GetMemoryLimits() const override; DirectFrameBufferMemory GetDirectMemory() const override; diff --git a/idd/LGIdd/transport/lgmp/CRecovery.cpp b/idd/LGIdd/transport/lgmp/CRecovery.cpp new file mode 100644 index 00000000..d060fb80 --- /dev/null +++ b/idd/LGIdd/transport/lgmp/CRecovery.cpp @@ -0,0 +1,408 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "transport/lgmp/CRecovery.h" + +#include "transport/lgmp/CIVSHMEM.h" +#include "platform/CPlatformInfo.h" +#include "CDebug.h" +#include "VersionInfo.h" + +#include "common/KVMFR.h" +#include "common/KVMFRRecovery.h" + +#include + +#include +#include + +namespace +{ + static const uint64_t HELPER_TIMEOUT_MS = 30000; + CSRWLock l_wireLock; + + uint32_t AtomicRead(uint32_t& value) + { + return static_cast(InterlockedCompareExchange( + (volatile LONG *)&value, 0, 0)); + } + + void AtomicWrite(uint32_t& value, uint32_t data) + { + InterlockedExchange((volatile LONG *)&value, static_cast(data)); + } + + void AtomicIncrement(uint32_t& value) + { + InterlockedIncrement((volatile LONG *)&value); + } + + uint32_t AtomicAdd(uint32_t& value, uint32_t data) + { + return static_cast(InterlockedExchangeAdd( + (volatile LONG *)&value, static_cast(data))) + data; + } + + bool AtomicCompareExchange( + uint32_t& value, uint32_t expected, uint32_t data) + { + return static_cast(InterlockedCompareExchange( + (volatile LONG *)&value, static_cast(data), + static_cast(expected))) == expected; + } + + uint64_t CreateSession(const void * memory, uint64_t previous) + { + LARGE_INTEGER counter; + QueryPerformanceCounter(&counter); + + uint64_t session = static_cast(counter.QuadPart) ^ + (GetTickCount64() << 24) ^ + static_cast(reinterpret_cast(memory)) ^ + (static_cast(GetCurrentProcessId()) << 32) ^ + GetCurrentThreadId(); + + if (!session || session == previous) + ++session; + if (!session) + ++session; + return session; + } +} + +bool CRecovery::OwnsSession() +{ + if (AtomicRead(m_data->header.ready) != KVMFR_R_READY) + return false; + + const uint64_t session = m_data->header.session; + MemoryBarrier(); + return session == m_session && + AtomicRead(m_data->header.ready) == KVMFR_R_READY; +} + +bool CRecovery::ReadRequest( + KVMFRRRequest& source, KVMFRRRequest& result) +{ + for (unsigned i = 0; i < 4; ++i) + { + const uint32_t serial = AtomicRead(source.serial); + if (!serial || (serial & 1U)) + return false; + + const uint32_t type = source.request; + const uint64_t session = source.session; + MemoryBarrier(); + if (AtomicRead(source.serial) == serial) + { + result.serial = serial; + result.request = type; + result.session = session; + return true; + } + } + + return false; +} + +bool CRecovery::ReadStatus(KVMFRRStatus& source, KVMFRRStatus& result) +{ + for (unsigned i = 0; i < 4; ++i) + { + const uint32_t serial = AtomicRead(source.serial); + if (!serial || (serial & 1U)) + continue; + + result.ackSerial = source.ackSerial; + result.ackRequest = source.ackRequest; + result.state = source.state; + result.error = source.error; + result.session = source.session; + MemoryBarrier(); + if (AtomicRead(source.serial) == serial) + { + result.serial = serial; + return true; + } + } + + return false; +} + +bool CRecovery::SerialNewer(uint32_t serial, uint32_t reference) +{ + const uint32_t difference = serial - reference; + return difference && difference < 0x80000000U; +} + +uint32_t CRecovery::NextTicket() +{ + uint32_t ticket = AtomicAdd(m_data->req.ticket, 2U); + if (!ticket) + ticket = AtomicAdd(m_data->req.ticket, 2U); + return ticket; +} + +void CRecovery::Publish(uint32_t serial, uint32_t request, + uint32_t state, uint32_t error) +{ + const uint32_t writing = m_statusSerial | 1U; + uint32_t published = writing + 1U; + if (!published) + published = KVMFR_R_REQ_FIRST; + + AtomicWrite(m_data->status.serial, writing); + m_data->status.ackRequest = request; + m_data->status.state = state; + m_data->status.error = error; + m_data->status.session = m_session; + m_data->status.ackSerial = serial; + AtomicWrite(m_data->status.serial, published); + m_statusSerial = published; +} + +bool CRecovery::Initialize(CIVSHMEM& ivshmem) +{ + CSRWExclusiveLock wireLock(l_wireLock); + CSRWExclusiveLock lock(m_lock); + if (m_data) + return OwnsSession(); + + m_data = static_cast(ivshmem.GetRecoveryMem()); + if (!m_data) + { + DEBUG_ERROR("IVSHMEM is too small for the recovery region"); + return false; + } + + const uint32_t oldReady = AtomicRead(m_data->header.ready); + const bool oldValid = oldReady == KVMFR_R_READY && + memcmp(m_data->header.magic, KVMFR_R_MAGIC, + sizeof(m_data->header.magic)) == 0 && + m_data->header.abiVersion == KVMFR_R_VERSION && + m_data->header.structSize >= sizeof(KVMFRR) && + m_data->header.session != 0; + const uint64_t oldSession = oldValid ? m_data->header.session : 0; + + uint32_t retainedRequest = KVMFR_R_REQ_NONE; + if (oldValid) + { + KVMFRRStatus status = {}; + if (ReadStatus(m_data->status, status) && + status.session == oldSession) + { + if (status.ackRequest == KVMFR_R_REQ_RECOVERY && + (status.state == KVMFR_R_STATE_SWITCHING || + status.state == KVMFR_R_STATE_ACTIVE || + status.state == KVMFR_R_STATE_FAILED)) + retainedRequest = KVMFR_R_REQ_RECOVERY; + else if (status.ackRequest == KVMFR_R_REQ_NORMAL && + (status.state == KVMFR_R_STATE_SWITCHING || + status.state == KVMFR_R_STATE_FAILED)) + retainedRequest = KVMFR_R_REQ_NORMAL; + } + } + + AtomicWrite(m_data->header.ready, 0); + if (oldValid) + { + ZeroMemory(&m_data->header, sizeof(m_data->header)); + ZeroMemory(&m_data->info, sizeof(m_data->info)); + ZeroMemory(&m_data->status, sizeof(m_data->status)); + + // Preserve the client-owned ticket and interrupted odd slots. Completed + // requests belong to the old producer session and can now be reclaimed. + for (unsigned i = 0; i < KVMFR_R_REQ_SLOTS; ++i) + { + KVMFRRRequest request = {}; + if (ReadRequest(m_data->requests[i], request)) + AtomicCompareExchange( + m_data->requests[i].serial, request.serial, 0); + } + } + else + ZeroMemory(m_data, sizeof(*m_data)); + + m_session = CreateSession(m_data, oldSession); + + memcpy(m_data->header.magic, KVMFR_R_MAGIC, + sizeof(m_data->header.magic)); + m_data->header.abiVersion = KVMFR_R_VERSION; + m_data->header.structSize = static_cast(sizeof(*m_data)); + m_data->header.capabilities = KVMFR_R_CAP_DISPLAY; + m_data->header.lgmpVersion = LGMP_PROTOCOL_VERSION; + m_data->header.kvmfrVersion = KVMFR_VERSION; + m_data->header.session = m_session; + memcpy(m_data->header.uuid, CPlatformInfo::GetUUID(), + sizeof(m_data->header.uuid)); + m_data->header.heartbeat = 1; + strncpy_s(m_data->info.version, sizeof(m_data->info.version), + LG_VERSION_STR, _TRUNCATE); + + m_request = retainedRequest == KVMFR_R_REQ_NONE ? + KVMFR_R_REQ_NORMAL : retainedRequest; + m_lastSerial = NextTicket(); + m_replay = true; + Publish(m_lastSerial, m_request, + KVMFR_R_STATE_SWITCHING, KVMFR_R_ERR_NONE); + + m_nextHeartbeat = GetTickCount64() + KVMFR_R_HEARTBEAT_MS; + AtomicWrite(m_data->header.ready, KVMFR_R_READY); + + DEBUG_INFO("Recovery channel initialized (session %llu%s)", + (unsigned long long)m_session, + retainedRequest != KVMFR_R_REQ_NONE ? ", request retained" : ""); + return true; +} + +void CRecovery::Sync() +{ + CSRWExclusiveLock lock(m_lock); + m_syncReady = true; +} + +CRecovery::Request CRecovery::Process() +{ + Request result; + CSRWExclusiveLock wireLock(l_wireLock); + CSRWExclusiveLock lock(m_lock); + if (!m_data || !OwnsSession()) + return result; + + const uint64_t now = GetTickCount64(); + if (now >= m_nextHeartbeat) + { + AtomicIncrement(m_data->header.heartbeat); + m_nextHeartbeat = now + KVMFR_R_HEARTBEAT_MS; + } + + KVMFRRRequest requests[KVMFR_R_REQ_SLOTS] = {}; + bool stable[KVMFR_R_REQ_SLOTS] = {}; + KVMFRRRequest request = {}; + bool haveRequest = false; + for (unsigned i = 0; i < KVMFR_R_REQ_SLOTS; ++i) + { + stable[i] = ReadRequest(m_data->requests[i], requests[i]); + if (!stable[i] || requests[i].session != m_session || + !SerialNewer(requests[i].serial, m_lastSerial)) + continue; + + if (!haveRequest || SerialNewer(requests[i].serial, request.serial)) + { + request = requests[i]; + haveRequest = true; + } + } + + if (haveRequest) + { + m_lastSerial = request.serial; + + if (request.session == m_session && + (request.request == KVMFR_R_REQ_NORMAL || + request.request == KVMFR_R_REQ_RECOVERY)) + { + m_request = request.request; + m_replay = m_request == KVMFR_R_REQ_NORMAL && !m_syncReady; + m_waiting = false; + Publish(m_lastSerial, m_request, + KVMFR_R_STATE_SWITCHING, KVMFR_R_ERR_NONE); + + if (m_replay) + DEBUG_INFO("Deferring recovery request %u until monitor arrival", + m_lastSerial); + else + { + m_waiting = true; + m_deadline = now + HELPER_TIMEOUT_MS; + + result.session = m_session; + result.serial = m_lastSerial; + result.valid = true; + result.active = m_request == KVMFR_R_REQ_RECOVERY; + DEBUG_INFO("Recovery mode request %u: %s", m_lastSerial, + result.active ? "active" : "normal"); + } + } + else if (request.session == m_session) + { + m_request = request.request; + m_replay = false; + m_waiting = false; + Publish(m_lastSerial, m_request, + KVMFR_R_STATE_FAILED, KVMFR_R_ERR_UNSUPPORTED); + DEBUG_WARN("Ignoring invalid recovery request %u", m_lastSerial); + } + } + else if (m_replay && + (m_request != KVMFR_R_REQ_NORMAL || m_syncReady)) + { + m_replay = false; + m_waiting = true; + m_deadline = now + HELPER_TIMEOUT_MS; + + result.session = m_session; + result.serial = m_lastSerial; + result.valid = true; + result.active = m_request == KVMFR_R_REQ_RECOVERY; + DEBUG_INFO("Synchronizing recovery helper mode: %s", + result.active ? "active" : "normal"); + } + + // A stable slot cannot be reused until the producer clears it. Publish the + // selected request's state first so its client cannot observe a reclaimed + // slot without a corresponding acknowledgement. + for (unsigned i = 0; i < KVMFR_R_REQ_SLOTS; ++i) + if (stable[i]) + AtomicCompareExchange( + m_data->requests[i].serial, requests[i].serial, 0); + + if (m_waiting && now >= m_deadline) + { + m_waiting = false; + Publish(m_lastSerial, m_request, + KVMFR_R_STATE_FAILED, KVMFR_R_ERR_HELPER_UNAVAILABLE); + DEBUG_WARN("Recovery helper did not respond to request %u", + m_lastSerial); + } + + return result; +} + +void CRecovery::SetStatus(uint64_t session, uint32_t serial, bool active, + uint32_t state, uint32_t error) +{ + CSRWExclusiveLock wireLock(l_wireLock); + CSRWExclusiveLock lock(m_lock); + const bool expectedActive = m_request == KVMFR_R_REQ_RECOVERY; + if (!m_data || !OwnsSession() || session != m_session || + serial != m_lastSerial || + active != expectedActive || + (state == KVMFR_R_STATE_ACTIVE && !active) || + (state == KVMFR_R_STATE_NORMAL && active)) + { + DEBUG_WARN("Ignoring stale recovery helper status"); + return; + } + + m_waiting = false; + Publish(serial, m_request, state, error); + DEBUG_INFO("Recovery request %u completed with state %u", serial, state); +} diff --git a/idd/LGIdd/transport/lgmp/CRecovery.h b/idd/LGIdd/transport/lgmp/CRecovery.h new file mode 100644 index 00000000..bd6331f9 --- /dev/null +++ b/idd/LGIdd/transport/lgmp/CRecovery.h @@ -0,0 +1,72 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#pragma once + +#include "CSRWLock.h" + +#include + +struct KVMFRR; +struct KVMFRRRequest; +struct KVMFRRStatus; + +class CIVSHMEM; + +class CRecovery +{ +public: + struct Request + { + uint64_t session = 0; + uint32_t serial = 0; + bool valid = false; + bool active = false; + }; + +private: + CSRWLock m_lock; + KVMFRR * m_data = nullptr; + + uint64_t m_session = 0; + uint64_t m_nextHeartbeat = 0; + uint64_t m_deadline = 0; + uint32_t m_lastSerial = 0; + uint32_t m_statusSerial = 0; + uint32_t m_request = 0; + bool m_syncReady = false; + bool m_replay = false; + bool m_waiting = false; + + static bool ReadRequest(KVMFRRRequest& source, KVMFRRRequest& result); + static bool ReadStatus(KVMFRRStatus& source, KVMFRRStatus& result); + static bool SerialNewer(uint32_t serial, uint32_t reference); + bool OwnsSession(); + uint32_t NextTicket(); + void Publish(uint32_t serial, uint32_t request, + uint32_t state, uint32_t error); + +public: + bool Initialize(CIVSHMEM& ivshmem); + void Sync(); + Request Process(); + void SetStatus(uint64_t session, uint32_t serial, bool active, + uint32_t state, uint32_t error); +}; diff --git a/idd/LGIddHelper/CNotifyWindow.cpp b/idd/LGIddHelper/CNotifyWindow.cpp index 8a99ca5e..45f76d36 100644 --- a/idd/LGIddHelper/CNotifyWindow.cpp +++ b/idd/LGIddHelper/CNotifyWindow.cpp @@ -30,6 +30,7 @@ #define WM_CLEAN_UP_CONFIG (WM_USER+1) #define WM_NO_GPU (WM_USER+2) #define WM_RESOLUTION_REJECTED (WM_USER+3) +#define WM_RECOVERY_STATE (WM_USER+4) #define ID_MENU_SHOW_LOG 3000 #define ID_MENU_SHOW_CONFIG 3001 @@ -66,7 +67,7 @@ bool CNotifyWindow::registerClass() } CNotifyWindow::CNotifyWindow() : m_iconData({ 0 }), m_iconRegistered(false), - m_menu(CreatePopupMenu()), closeRequested(false) + m_menu(CreatePopupMenu()), closeRequested(false), m_recoveryActive(false) { CreateWindowEx(0, MAKEINTATOM(s_atom), NULL, 0, 0, 0, 0, 0, NULL, NULL, hInstance, this); @@ -112,8 +113,16 @@ LRESULT CNotifyWindow::handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) return 0; } + case WM_RECOVERY_STATE: + m_recoveryActive = wParam; + KillTimer(m_hwnd, ID_DISPLAY_CHECK_TIMER); + if (!m_recoveryActive) + scheduleDisplayCheck(DISPLAY_SETTLE_DELAY); + return 0; + case WM_DISPLAYCHANGE: - scheduleDisplayCheck(DISPLAY_SETTLE_DELAY); + if (!m_recoveryActive) + scheduleDisplayCheck(DISPLAY_SETTLE_DELAY); return 0; case WM_TIMER: @@ -121,6 +130,8 @@ LRESULT CNotifyWindow::handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) { case ID_DISPLAY_CHECK_TIMER: KillTimer(m_hwnd, ID_DISPLAY_CHECK_TIMER); + if (m_recoveryActive) + break; if (m_onEnsureOnlyDisplay && m_onEnsureOnlyDisplay()) DEBUG_INFO("Enforced Looking Glass as the only display"); else @@ -293,6 +304,12 @@ void CNotifyWindow::notifyResolutionRejected(uint32_t width, uint32_t height, } } +void CNotifyWindow::setRecoveryMode(bool active) +{ + if (!PostMessage(m_hwnd, WM_RECOVERY_STATE, active, 0)) + DEBUG_ERROR_HR(GetLastError(), "Failed to update recovery state"); +} + void CNotifyWindow::handleResolutionRejected(uint32_t width, uint32_t height, uint32_t requiredSizeMiB) { @@ -326,7 +343,7 @@ void CNotifyWindow::close() void CNotifyWindow::scheduleDisplayCheck(UINT delay) { - if (!m_onEnsureOnlyDisplay) + if (m_recoveryActive || !m_onEnsureOnlyDisplay) return; CRegistrySettings settings; diff --git a/idd/LGIddHelper/CNotifyWindow.h b/idd/LGIddHelper/CNotifyWindow.h index 77a85f34..6e7d9620 100644 --- a/idd/LGIddHelper/CNotifyWindow.h +++ b/idd/LGIddHelper/CNotifyWindow.h @@ -37,6 +37,7 @@ class CNotifyWindow : public CWindow std::optional m_gpuQueue; HMENU m_menu; bool closeRequested; + bool m_recoveryActive; std::unique_ptr m_config; std::function m_onSettingChange; @@ -76,6 +77,7 @@ public: void setGPU(bool hasGPU); void notifyResolutionRejected(uint32_t width, uint32_t height, uint32_t requiredSizeMiB); + void setRecoveryMode(bool active); HWND hwndDialog(); void close(); diff --git a/idd/LGIddHelper/CPipeClient.cpp b/idd/LGIddHelper/CPipeClient.cpp index 7cfa32f8..b798160c 100644 --- a/idd/LGIddHelper/CPipeClient.cpp +++ b/idd/LGIddHelper/CPipeClient.cpp @@ -22,6 +22,7 @@ #include "CDebug.h" #include "CSRWLock.h" #include "CNotifyWindow.h" +#include "CRegistrySettings.h" #include #include @@ -29,6 +30,11 @@ namespace { + static const unsigned RECOVERY_VERIFY_ATTEMPTS = 20; + static const unsigned RECOVERY_PATH_ATTEMPTS = 20; + static const size_t RECOVERY_MAX_PATHS = 4; + static const DWORD RECOVERY_VERIFY_DELAY_MS = 100; + struct DisplayState { DISPLAY_DEVICE device; @@ -65,6 +71,90 @@ namespace return false; } + bool ContainsNoCase(LPCTSTR text, LPCTSTR value) + { + const size_t length = _tcslen(value); + for (; *text; ++text) + if (_tcsnicmp(text, value, length) == 0) + return true; + + return false; + } + + bool IsLGPath(const DISPLAYCONFIG_PATH_INFO& path) + { + DISPLAYCONFIG_TARGET_DEVICE_NAME target = {}; + target.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME; + target.header.size = sizeof(target); + target.header.adapterId = path.targetInfo.adapterId; + target.header.id = path.targetInfo.id; + if (DisplayConfigGetDeviceInfo(&target.header) == ERROR_SUCCESS && + (_tcsicmp(target.monitorFriendlyDeviceName, + _T("Looking Glass")) == 0 || + ContainsNoCase(target.monitorDevicePath, _T("LGD1DDD")) || + ContainsNoCase(target.monitorDevicePath, _T("ROOT#LGIDD")))) + return true; + + DISPLAYCONFIG_SOURCE_DEVICE_NAME source = {}; + source.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + source.header.size = sizeof(source); + source.header.adapterId = path.sourceInfo.adapterId; + source.header.id = path.sourceInfo.id; + if (DisplayConfigGetDeviceInfo(&source.header) != ERROR_SUCCESS || + !source.viewGdiDeviceName[0]) + return false; + + DISPLAY_DEVICE device = {}; + device.cb = sizeof(device); + for (DWORD i = 0; EnumDisplayDevices(NULL, i, &device, 0); ++i) + { + if (_tcsicmp(device.DeviceName, source.viewGdiDeviceName) == 0) + return IsLGDisplay(device); + + device = {}; + device.cb = sizeof(device); + } + + return false; + } + + bool SameTarget(const DISPLAYCONFIG_PATH_INFO& a, + const DISPLAYCONFIG_PATH_INFO& b) + { + return a.targetInfo.adapterId.HighPart == + b.targetInfo.adapterId.HighPart && + a.targetInfo.adapterId.LowPart == + b.targetInfo.adapterId.LowPart && + a.targetInfo.id == b.targetInfo.id; + } + + uint32_t QueryAllPaths(std::vector& paths) + { + for (unsigned int attempt = 0; attempt < 3; ++attempt) + { + UINT32 pathCount = 0; + UINT32 modeCount = 0; + LONG result = GetDisplayConfigBufferSizes( + QDC_ALL_PATHS, &pathCount, &modeCount); + if (result != ERROR_SUCCESS) + return static_cast(result); + + paths.resize(pathCount); + std::vector modes(modeCount); + result = QueryDisplayConfig(QDC_ALL_PATHS, + &pathCount, paths.data(), &modeCount, modes.data(), NULL); + if (result == ERROR_INSUFFICIENT_BUFFER) + continue; + if (result != ERROR_SUCCESS) + return static_cast(result); + + paths.resize(pathCount); + return ERROR_SUCCESS; + } + + return ERROR_INSUFFICIENT_BUFFER; + } + bool GetDisplayStates(std::vector& displays, size_t& lgIndex) { lgIndex = SIZE_MAX; @@ -77,9 +167,9 @@ namespace !(device.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER)) { DisplayState state = {}; - state.device = device; + state.device = device; state.mode.dmSize = sizeof(state.mode); - state.isLG = IsLGDisplay(device); + state.isLG = IsLGDisplay(device); if (!EnumDisplaySettingsEx(device.DeviceName, ENUM_CURRENT_SETTINGS, &state.mode, 0)) @@ -101,6 +191,191 @@ namespace return lgIndex != SIZE_MAX; } + + bool HasActiveDisplay(bool lg) + { + DISPLAY_DEVICE device = {}; + device.cb = sizeof(device); + for (DWORD i = 0; EnumDisplayDevices(NULL, i, &device, 0); ++i) + { + if ((device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) && + !(device.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER) && + IsLGDisplay(device) == lg) + return true; + + device = {}; + device.cb = sizeof(device); + } + + return false; + } + + bool WaitForDisplay(bool lg, + unsigned int attempts = RECOVERY_VERIFY_ATTEMPTS) + { + for (unsigned int attempt = 0; + attempt < attempts; + ++attempt) + { + if (HasActiveDisplay(lg)) + return true; + + if (attempt + 1 < attempts) + Sleep(RECOVERY_VERIFY_DELAY_MS); + } + + return false; + } + + bool HasOnlyLGDisplay() + { + bool found = false; + DISPLAY_DEVICE device = {}; + device.cb = sizeof(device); + for (DWORD i = 0; EnumDisplayDevices(NULL, i, &device, 0); ++i) + { + if ((device.StateFlags & DISPLAY_DEVICE_ATTACHED_TO_DESKTOP) && + !(device.StateFlags & DISPLAY_DEVICE_MIRRORING_DRIVER)) + { + if (!IsLGDisplay(device)) + return false; + found = true; + } + + device = {}; + device.cb = sizeof(device); + } + + return found; + } + + bool WaitForOnlyLGDisplay() + { + for (unsigned int attempt = 0; + attempt < RECOVERY_VERIFY_ATTEMPTS; + ++attempt) + { + if (HasOnlyLGDisplay()) + return true; + + if (attempt + 1 < RECOVERY_VERIFY_ATTEMPTS) + Sleep(RECOVERY_VERIFY_DELAY_MS); + } + + return false; + } + + uint32_t ActivateDisplay(bool lg) + { + std::vector paths; + const uint32_t queryError = QueryAllPaths(paths); + if (queryError != ERROR_SUCCESS) + { + DEBUG_ERROR("Failed to enumerate display paths (%u)", queryError); + return queryError; + } + + std::vector attempted; + uint32_t lastError = ERROR_NOT_FOUND; + for (const DISPLAYCONFIG_PATH_INFO& path : paths) + { + if (!path.targetInfo.targetAvailable || IsLGPath(path) != lg) + continue; + + bool duplicate = false; + for (const DISPLAYCONFIG_PATH_INFO& previous : attempted) + if (SameTarget(path, previous)) + { + duplicate = true; + break; + } + if (duplicate) + continue; + + if (attempted.size() >= RECOVERY_MAX_PATHS) + break; + attempted.emplace_back(path); + + DISPLAYCONFIG_PATH_INFO candidate = path; + candidate.flags |= DISPLAYCONFIG_PATH_ACTIVE; + candidate.sourceInfo.modeInfoIdx = + DISPLAYCONFIG_PATH_MODE_IDX_INVALID; + candidate.targetInfo.modeInfoIdx = + DISPLAYCONFIG_PATH_MODE_IDX_INVALID; + + LONG result = SetDisplayConfig(1, &candidate, 0, NULL, + SDC_APPLY | SDC_TOPOLOGY_SUPPLIED | SDC_ALLOW_CHANGES); + if (result == ERROR_SUCCESS && + WaitForDisplay(lg, RECOVERY_PATH_ATTEMPTS)) + { + DEBUG_INFO("Activated a saved %s topology", + lg ? "Looking Glass" : "non-Looking Glass"); + return ERROR_SUCCESS; + } + + // The connected display may not yet have a database entry. Ask CCD's + // best-mode logic for a temporary configuration without saving it. + result = SetDisplayConfig(1, &candidate, 0, NULL, + SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | SDC_ALLOW_CHANGES); + if (result == ERROR_SUCCESS && + WaitForDisplay(lg, RECOVERY_PATH_ATTEMPTS)) + { + DEBUG_INFO("Activated a fallback %s display", + lg ? "Looking Glass" : "non-Looking Glass"); + return ERROR_SUCCESS; + } + + lastError = result == ERROR_SUCCESS ? + ERROR_NOT_FOUND : static_cast(result); + } + + if (attempted.empty()) + DEBUG_ERROR("No connected %s display path was found", + lg ? "Looking Glass" : "non-Looking Glass"); + else + DEBUG_ERROR("No %s display path could be activated", + lg ? "Looking Glass" : "non-Looking Glass"); + return lastError; + } + + uint32_t ActivateFallbackDisplay() + { + return ActivateDisplay(false); + } + + uint32_t RestoreNonExclusiveTopology() + { + if (HasActiveDisplay(true)) + return ERROR_SUCCESS; + + LONG result = SetDisplayConfig(0, NULL, 0, NULL, + SDC_APPLY | SDC_USE_DATABASE_CURRENT | SDC_ALLOW_CHANGES); + if (result == ERROR_SUCCESS && WaitForDisplay(true)) + { + DEBUG_INFO("Saved non-exclusive display topology restored"); + return ERROR_SUCCESS; + } + + if (result == ERROR_SUCCESS) + DEBUG_WARN("The saved display topology does not activate Looking Glass"); + else + DEBUG_WARN("Failed to restore the saved display topology (%ld)", result); + + // If the current database topology omits LG, use Windows' most recently + // saved clone or extended topology. No persistence flag is supplied, so + // this does not replace the user's saved display configuration. + result = SetDisplayConfig(0, NULL, 0, NULL, + SDC_APPLY | SDC_TOPOLOGY_CLONE | SDC_TOPOLOGY_EXTEND | + SDC_ALLOW_CHANGES); + if (result == ERROR_SUCCESS && WaitForDisplay(true)) + { + DEBUG_INFO("Non-exclusive Looking Glass topology activated"); + return ERROR_SUCCESS; + } + + return result == ERROR_SUCCESS ? + ERROR_NOT_FOUND : static_cast(result); + } } CPipeClient g_pipe; @@ -197,6 +472,20 @@ void CPipeClient::WriteMsg(const LGPipeMsg& msg) m_endpoint.Send(&msg, sizeof(msg)); } +void CPipeClient::OnPipeConnected() +{ + bool hasStatus; + LGPipeMsg status; + { + CSRWSharedLock lock(m_displayLock); + hasStatus = m_hasRecoveryStatus; + status = m_recoveryStatus; + } + + if (hasStatus) + WriteMsg(status); +} + void CPipeClient::ReloadSettings() { if (!m_endpoint.IsConnected()) @@ -218,6 +507,9 @@ bool CPipeClient::ShouldReconnect() bool CPipeClient::EnsureOnlyDisplayLocked() { + if (m_recoveryActive) + return true; + std::vector displays; size_t lgIndex; if (!GetDisplayStates(displays, lgIndex)) @@ -322,6 +614,95 @@ bool CPipeClient::EnsureOnlyDisplayLocked() return false; } +uint32_t CPipeClient::RestoreSavedTopologyLocked() const +{ + const LONG result = SetDisplayConfig(0, NULL, 0, NULL, + SDC_APPLY | SDC_USE_DATABASE_CURRENT | SDC_ALLOW_CHANGES); + if (result == ERROR_SUCCESS) + { + if (WaitForDisplay(false)) + { + DEBUG_INFO("Recovery display topology activated"); + return ERROR_SUCCESS; + } + + DEBUG_WARN("The saved topology has no active non-Looking Glass display"); + } + else + { + DEBUG_WARN("Failed to restore the saved display topology (%ld)", result); + } + + return ActivateFallbackDisplay(); +} + +uint32_t CPipeClient::RestoreLGTopologyLocked() +{ + uint32_t error = ERROR_SUCCESS; + bool exclusive = false; + + CRegistrySettings settings; + const LSTATUS settingsError = settings.open(); + if (settingsError != ERROR_SUCCESS) + { + DEBUG_ERROR_HR(settingsError, "Failed to load settings"); + error = static_cast(settingsError); + } + else + { + const std::optional value = settings.getExclusiveMonitor(); + if (!value.has_value()) + error = ERROR_INVALID_DATA; + else + exclusive = value.value(); + } + + if (error == ERROR_SUCCESS) + { + if (!exclusive) + { + error = RestoreNonExclusiveTopology(); + if (error == ERROR_SUCCESS) + { + m_recoveryActive = false; + DEBUG_INFO("Looking Glass display topology restored"); + return ERROR_SUCCESS; + } + } + else + { + if (!HasActiveDisplay(true)) + error = ActivateDisplay(true); + + if (error == ERROR_SUCCESS) + { + // Keep notification-driven display enforcement suppressed until the + // LG path is active. This explicit call owns the transition back to + // the temporary LG-only topology. + m_recoveryActive = false; + if (EnsureOnlyDisplayLocked() && WaitForOnlyLGDisplay()) + { + DEBUG_INFO("Looking Glass display topology restored"); + return ERROR_SUCCESS; + } + + error = ERROR_GEN_FAILURE; + } + } + } + + // A failed exit must leave a usable recovery display rather than a blank + // desktop if a partial CCD transition disabled the physical path. + m_recoveryActive = true; + if (!HasActiveDisplay(false)) + { + const uint32_t fallbackError = ActivateFallbackDisplay(); + if (fallbackError != ERROR_SUCCESS) + DEBUG_ERROR("Failed to restore a recovery display (%u)", fallbackError); + } + return error; +} + bool CPipeClient::EnsureOnlyDisplay() { CSRWExclusiveLock lock(m_displayLock); @@ -355,6 +736,10 @@ bool CPipeClient::OnPipeMessage(const void * message, size_t size) HandleResolutionRejected(msg); return true; + case LGPipeMsg::SET_RECOVERY: + HandleSetRecovery(msg); + return true; + default: DEBUG_ERROR("Unknown message type %d", msg.type); return true; @@ -411,3 +796,70 @@ void CPipeClient::HandleResolutionRejected(const LGPipeMsg& msg) msg.resolutionRejected.height, msg.resolutionRejected.requiredSizeMiB); } + +void CPipeClient::HandleSetRecovery(const LGPipeMsg& msg) +{ + const bool active = + (msg.recovery.request & LGPipeMsg::RECOVERY_ACTIVE) != 0; + bool cached = false; + LGPipeMsg status = {}; + status.size = sizeof(status); + status.type = LGPipeMsg::RECOVERY_FAILED; + status.recovery = msg.recovery; + + { + CSRWExclusiveLock lock(m_displayLock); + cached = m_hasRecoveryStatus && + m_recoveryStatus.recovery.session == msg.recovery.session && + m_recoveryStatus.recovery.request == msg.recovery.request; + if (cached) + status = m_recoveryStatus; + else if (active) + { + m_recoveryActive = true; + CNotifyWindow::instance().setRecoveryMode(true); + + const uint32_t error = RestoreSavedTopologyLocked(); + if (error == ERROR_SUCCESS) + status.type = LGPipeMsg::RECOVERY_ON; + else if (error == ERROR_NOT_FOUND) + status.type = LGPipeMsg::RECOVERY_NO_DISPLAY; + else + status.type = LGPipeMsg::RECOVERY_FAILED; + } + else + { + m_recoveryActive = true; + CNotifyWindow::instance().setRecoveryMode(true); + + if (RestoreLGTopologyLocked() == ERROR_SUCCESS) + { + CNotifyWindow::instance().setRecoveryMode(false); + status.type = LGPipeMsg::RECOVERY_OFF; + } + } + + if (!cached) + { + m_hasRecoveryStatus = true; + m_recoveryStatus = status; + } + } + + if (cached) + { + DEBUG_TRACE("Replaying cached recovery status"); + WriteMsg(status); + return; + } + + if (active) + DEBUG_INFO("Recovery mode %s", status.type == LGPipeMsg::RECOVERY_ON ? + "active" : "failed"); + else if (status.type == LGPipeMsg::RECOVERY_OFF) + DEBUG_INFO("Recovery mode disabled"); + else + DEBUG_INFO("Recovery mode exit failed"); + + WriteMsg(status); +} diff --git a/idd/LGIddHelper/CPipeClient.h b/idd/LGIddHelper/CPipeClient.h index a3581e0b..5f3236c4 100644 --- a/idd/LGIddHelper/CPipeClient.h +++ b/idd/LGIddHelper/CPipeClient.h @@ -33,17 +33,25 @@ private: CPipeEndpoint m_endpoint; CSRWLock m_displayLock; + bool m_recoveryActive = false; + bool m_hasRecoveryStatus = false; + LGPipeMsg m_recoveryStatus = {}; + void WriteMsg(const LGPipeMsg& msg); void SetActiveDesktop(); bool EnsureOnlyDisplayLocked(); - + uint32_t RestoreSavedTopologyLocked() const; + uint32_t RestoreLGTopologyLocked(); + void HandleSetCursorPos(const LGPipeMsg& msg); void HandleSetDisplayMode(const LGPipeMsg& msg); void HandleGPUStatus(const LGPipeMsg& msg); void HandleResolutionRejected(const LGPipeMsg& msg); + void HandleSetRecovery(const LGPipeMsg& msg); + void OnPipeConnected() override; bool ShouldReconnect() override; bool OnPipeMessage(const void * message, size_t size) override;