From ab96c7ea8109c277b96c71c68d3d6d8357d995a8 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Sat, 18 Jul 2026 22:20:36 +1000 Subject: [PATCH] [idd] ensure the only active monitor is the IDD --- idd/LGIddHelper/CNotifyWindow.cpp | 36 +++++ idd/LGIddHelper/CNotifyWindow.h | 5 +- idd/LGIddHelper/CPipeClient.cpp | 210 +++++++++++++++++++++++++++++- idd/LGIddHelper/CPipeClient.h | 6 +- idd/LGIddHelper/main.cpp | 6 +- 5 files changed, 256 insertions(+), 7 deletions(-) diff --git a/idd/LGIddHelper/CNotifyWindow.cpp b/idd/LGIddHelper/CNotifyWindow.cpp index 1bed0529..8677f24e 100644 --- a/idd/LGIddHelper/CNotifyWindow.cpp +++ b/idd/LGIddHelper/CNotifyWindow.cpp @@ -32,6 +32,11 @@ #define ID_MENU_SHOW_LOG 3000 #define ID_MENU_SHOW_CONFIG 3001 +#define ID_DISPLAY_CHECK_TIMER 1 +#define DISPLAY_SETTLE_DELAY 250 +#define DISPLAY_RETRY_DELAY 1000 +#define DISPLAY_CHECK_INTERVAL 5000 + ATOM CNotifyWindow::s_atom = 0; UINT CNotifyWindow::s_taskbarCreated = 0; @@ -86,6 +91,20 @@ LRESULT CNotifyWindow::handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) handleGPUNotification((bool)wParam); return 0; + case WM_DISPLAYCHANGE: + scheduleDisplayCheck(DISPLAY_SETTLE_DELAY); + return 0; + + case WM_TIMER: + if (wParam == ID_DISPLAY_CHECK_TIMER) + { + KillTimer(m_hwnd, ID_DISPLAY_CHECK_TIMER); + const bool success = m_onDisplayChange && m_onDisplayChange(); + scheduleDisplayCheck(success ? DISPLAY_CHECK_INTERVAL : DISPLAY_RETRY_DELAY); + return 0; + } + return CWindow::handleMessage(uMsg, wParam, lParam); + default: if (s_taskbarCreated && uMsg == s_taskbarCreated) { @@ -114,6 +133,7 @@ LRESULT CNotifyWindow::onClose() LRESULT CNotifyWindow::onDestroy() { + KillTimer(m_hwnd, ID_DISPLAY_CHECK_TIMER); Shell_NotifyIcon(NIM_DELETE, &m_iconData); return 0; } @@ -239,3 +259,19 @@ void CNotifyWindow::close() closeRequested = true; PostMessage(m_hwnd, WM_CLOSE, 0, 0); } + +void CNotifyWindow::scheduleDisplayCheck(UINT delay) +{ + if (!m_onDisplayChange) + return; + + KillTimer(m_hwnd, ID_DISPLAY_CHECK_TIMER); + if (!SetTimer(m_hwnd, ID_DISPLAY_CHECK_TIMER, delay, NULL)) + DEBUG_ERROR_HR(GetLastError(), "Failed to schedule primary display check"); +} + +void CNotifyWindow::onDisplayChange(std::function func) +{ + m_onDisplayChange = std::move(func); + scheduleDisplayCheck(DISPLAY_SETTLE_DELAY); +} diff --git a/idd/LGIddHelper/CNotifyWindow.h b/idd/LGIddHelper/CNotifyWindow.h index 3b39254e..8ac03dc7 100644 --- a/idd/LGIddHelper/CNotifyWindow.h +++ b/idd/LGIddHelper/CNotifyWindow.h @@ -39,10 +39,12 @@ class CNotifyWindow : public CWindow std::unique_ptr m_config; std::function m_onSettingChange; + std::function m_onDisplayChange; LRESULT onNotifyIcon(UINT uEvent, WORD wIconId, int x, int y); void registerIcon(); void handleGPUNotification(bool hasGPU); + void scheduleDisplayCheck(UINT delay); virtual LRESULT handleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam) override; virtual LRESULT onCreate() override; @@ -74,4 +76,5 @@ public: void close(); void onSettingChange(std::function func) { m_onSettingChange = std::move(func); } -}; \ No newline at end of file + void onDisplayChange(std::function func); +}; diff --git a/idd/LGIddHelper/CPipeClient.cpp b/idd/LGIddHelper/CPipeClient.cpp index 768eebac..834366e8 100644 --- a/idd/LGIddHelper/CPipeClient.cpp +++ b/idd/LGIddHelper/CPipeClient.cpp @@ -24,6 +24,83 @@ #include #include +#include + +namespace +{ + struct DisplayState + { + DISPLAY_DEVICE device; + DEVMODE mode; + bool isLG; + }; + + bool IsLGDisplay(const DISPLAY_DEVICE& device) + { + static const TCHAR deviceId[] = _T("ROOT\\LGIDD"); + const size_t deviceIdLength = _countof(deviceId) - 1; + + if (_tcsnicmp(device.DeviceID, deviceId, deviceIdLength) == 0 && + (device.DeviceID[deviceIdLength] == _T('\\') || + device.DeviceID[deviceIdLength] == _T('\0'))) + return true; + + if (_tcsicmp(device.DeviceString, + _T("Looking Glass Indirect Display Device")) == 0) + return true; + + DISPLAY_DEVICE monitor = {}; + monitor.cb = sizeof(monitor); + for (DWORD i = 0; EnumDisplayDevices(device.DeviceName, i, &monitor, 0); ++i) + { + if (_tcsnicmp(monitor.DeviceID, _T("MONITOR\\LGD1DDD"), 15) == 0 || + _tcsicmp(monitor.DeviceString, _T("Looking Glass")) == 0) + return true; + + monitor = {}; + monitor.cb = sizeof(monitor); + } + + return false; + } + + bool GetDisplayStates(std::vector& displays, size_t& lgIndex) + { + lgIndex = SIZE_MAX; + + 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)) + { + DisplayState state = {}; + state.device = device; + state.mode.dmSize = sizeof(state.mode); + state.isLG = IsLGDisplay(device); + + if (!EnumDisplaySettingsEx(device.DeviceName, ENUM_CURRENT_SETTINGS, + &state.mode, 0)) + { + DEBUG_WARN_HR(GetLastError(), + "Failed to query the current mode for %ls", device.DeviceName); + return false; + } + + if (state.isLG && lgIndex == SIZE_MAX) + lgIndex = displays.size(); + + displays.emplace_back(state); + } + + device = {}; + device.cb = sizeof(device); + } + + return lgIndex != SIZE_MAX; + } +} CPipeClient g_pipe; @@ -181,6 +258,115 @@ void CPipeClient::ReloadSettings() WriteMsg(msg); } +bool CPipeClient::EnsureOnlyDisplayLocked() +{ + std::vector displays; + size_t lgIndex; + if (!GetDisplayStates(displays, lgIndex)) + return false; + + // The only active display is necessarily the primary display. + if (displays.size() == 1) + return true; + + for (unsigned int attempt = 0; attempt < 3; ++attempt) + { + UINT32 pathCount = 0; + UINT32 modeCount = 0; + LONG result = GetDisplayConfigBufferSizes( + QDC_ONLY_ACTIVE_PATHS, &pathCount, &modeCount); + if (result != ERROR_SUCCESS) + { + DEBUG_ERROR("GetDisplayConfigBufferSizes failed (%ld)", result); + return false; + } + + std::vector paths(pathCount); + std::vector modes(modeCount); + result = QueryDisplayConfig(QDC_ONLY_ACTIVE_PATHS, + &pathCount, paths.data(), &modeCount, modes.data(), NULL); + if (result == ERROR_INSUFFICIENT_BUFFER) + continue; + + if (result != ERROR_SUCCESS) + { + DEBUG_ERROR("QueryDisplayConfig failed (%ld)", result); + return false; + } + + paths.resize(pathCount); + modes.resize(modeCount); + + for (size_t i = 0; i < paths.size(); ++i) + { + DISPLAYCONFIG_SOURCE_DEVICE_NAME sourceName = {}; + sourceName.header.type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME; + sourceName.header.size = sizeof(sourceName); + sourceName.header.adapterId = paths[i].sourceInfo.adapterId; + sourceName.header.id = paths[i].sourceInfo.id; + + result = DisplayConfigGetDeviceInfo(&sourceName.header); + if (result != ERROR_SUCCESS) + continue; + + if (_tcsicmp(sourceName.viewGdiDeviceName, + displays[lgIndex].device.DeviceName) != 0) + continue; + + const UINT32 sourceModeIndex = paths[i].sourceInfo.modeInfoIdx; + const UINT32 targetModeIndex = paths[i].targetInfo.modeInfoIdx; + if (sourceModeIndex == DISPLAYCONFIG_PATH_MODE_IDX_INVALID || + sourceModeIndex >= modes.size() || + targetModeIndex == DISPLAYCONFIG_PATH_MODE_IDX_INVALID || + targetModeIndex >= modes.size() || + modes[sourceModeIndex].infoType != DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE || + modes[targetModeIndex].infoType != DISPLAYCONFIG_MODE_INFO_TYPE_TARGET) + { + DEBUG_ERROR("Looking Glass display path has invalid mode indices"); + return false; + } + + DISPLAYCONFIG_PATH_INFO path = paths[i]; + DISPLAYCONFIG_MODE_INFO selectedModes[2] = { + modes[sourceModeIndex], modes[targetModeIndex] + }; + + selectedModes[0].sourceMode.position.x = 0; + selectedModes[0].sourceMode.position.y = 0; + path.sourceInfo.modeInfoIdx = 0; + path.targetInfo.modeInfoIdx = 1; + path.flags |= DISPLAYCONFIG_PATH_ACTIVE; + + result = SetDisplayConfig(1, &path, 2, selectedModes, + SDC_APPLY | SDC_USE_SUPPLIED_DISPLAY_CONFIG | + SDC_ALLOW_CHANGES | SDC_SAVE_TO_DATABASE); + if (result != ERROR_SUCCESS) + { + DEBUG_ERROR("Failed to apply the LG-only display topology (%ld)", + result); + return false; + } + + DEBUG_INFO("Looking Glass display set as the only active display"); + return true; + } + + DEBUG_ERROR("Looking Glass display configuration path not found"); + return false; + } + + DEBUG_WARN("Display topology kept changing while selecting Looking Glass"); + return false; +} + +bool CPipeClient::EnsureOnlyDisplay() +{ + AcquireSRWLockExclusive(&m_displayLock); + const bool result = EnsureOnlyDisplayLocked(); + ReleaseSRWLockExclusive(&m_displayLock); + return result; +} + void CPipeClient::Thread() { DEBUG_INFO("Pipe thread started"); @@ -305,16 +491,32 @@ void CPipeClient::HandleSetCursorPos(const LGPipeMsg& msg) void CPipeClient::HandleSetDisplayMode(const LGPipeMsg& msg) { - DEVMODE dm = {}; - dm.dmSize = sizeof(dm); + AcquireSRWLockExclusive(&m_displayLock); + + std::vector displays; + size_t lgIndex; + if (!GetDisplayStates(displays, lgIndex)) + { + ReleaseSRWLockExclusive(&m_displayLock); + DEBUG_ERROR("Looking Glass display not found while setting its mode"); + return; + } + + DEVMODE dm = displays[lgIndex].mode; dm.dmPelsWidth = msg.displayMode.width; dm.dmPelsHeight = msg.displayMode.height; dm.dmDisplayFrequency = msg.displayMode.refresh; - dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; + dm.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFREQUENCY; - LONG result = ChangeDisplaySettingsEx(NULL, &dm, NULL, CDS_UPDATEREGISTRY, NULL); + LONG result = ChangeDisplaySettingsEx(displays[lgIndex].device.DeviceName, + &dm, NULL, CDS_UPDATEREGISTRY, NULL); if (result != DISP_CHANGE_SUCCESSFUL) DEBUG_ERROR("ChangeDisplaySettingsEx Failed (0x%08x)", result); + + ReleaseSRWLockExclusive(&m_displayLock); + + if (result == DISP_CHANGE_SUCCESSFUL) + EnsureOnlyDisplay(); } void CPipeClient::HandleGPUStatus(const LGPipeMsg& msg) diff --git a/idd/LGIddHelper/CPipeClient.h b/idd/LGIddHelper/CPipeClient.h index 00ba0613..0cca3b7c 100644 --- a/idd/LGIddHelper/CPipeClient.h +++ b/idd/LGIddHelper/CPipeClient.h @@ -39,6 +39,7 @@ private: bool m_running = false; bool m_connected = false; + SRWLOCK m_displayLock = SRWLOCK_INIT; static DWORD WINAPI _pipeThread(LPVOID lpParam) { ((CPipeClient*)lpParam)->Thread(); return 0; } void Thread(); @@ -46,6 +47,8 @@ private: void WriteMsg(const LGPipeMsg& msg); void SetActiveDesktop(); + + bool EnsureOnlyDisplayLocked(); void HandleSetCursorPos(const LGPipeMsg& msg); void HandleSetDisplayMode(const LGPipeMsg& msg); @@ -61,6 +64,7 @@ public: bool IsRunning() { return m_running; } void ReloadSettings(); + bool EnsureOnlyDisplay(); }; -extern CPipeClient g_pipe; \ No newline at end of file +extern CPipeClient g_pipe; diff --git a/idd/LGIddHelper/main.cpp b/idd/LGIddHelper/main.cpp index e8999810..223461f4 100644 --- a/idd/LGIddHelper/main.cpp +++ b/idd/LGIddHelper/main.cpp @@ -124,6 +124,10 @@ int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _ g_pipe.ReloadSettings(); }); + window.onDisplayChange([]() { + return g_pipe.EnsureOnlyDisplay(); + }); + HANDLE hWait; if (!RegisterWaitForSingleObject(&hWait, hParent.Get(), DestroyNotifyWindow, &window, INFINITE, WT_EXECUTEONLYONCE)) DEBUG_ERROR_HR(GetLastError(), "Failed to RegisterWaitForSingleObject"); @@ -445,4 +449,4 @@ static void Launch() l_process.Attach(pi.hProcess); CloseHandle(pi.hThread); -} \ No newline at end of file +}