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;