diff --git a/README.md b/README.md
index 434bbbf..f070a94 100644
--- a/README.md
+++ b/README.md
@@ -1,142 +1,150 @@
-
+
GPU‑Switcher
-A lightweight GPU switching utility for Windows
+
+ A lightweight, near-zero overhead GPU switching utility for Windows
+
+
+
+
+
+
+
+**GPU-Switcher** is a tiny, battery-friendly Windows system tray utility that keeps your laptop's discrete GPU (dGPU) activated with **near-zero CPU usage** and no persistent background rendering loops.
-A tiny Windows tray utility that activates the discrete GPU (dGPU), with zero CPU usage and no background threads.
+Designed primarily for hybrid laptops equipped with **NVIDIA Advanced Optimus** (or MUX switch systems) where you want the dGPU ready for low-latency gaming, external displays, or 3D workloads without needing to keep a heavy game or benchmark running in the background.
-Designed for hybrid GPU laptops with **NVIDIA Advanced Optimus** where you want the dGPU ready without running a heavy application and without configuring every app manually through the driver control panel.
+---
-Please let me know if this also works for non-Nvidia display adapters.
+## Features
+
+- ⚡ **Near-Zero Overhead**: Creates a persistent Direct3D 11 device on the dGPU and sleeps in the Windows message pump. No heavy background worker loops, no telemetry, and virtually 0% CPU consumption.
+- 🎨 **Dynamic Vendor Tray Icons**: Instantly see which GPU is active with color-coded tray icons:
+ - 🟢 **Green**: Active NVIDIA discrete GPU
+ - 🔴 **Red**: Active AMD discrete GPU
+ - 🔵 **Blue**: Active Intel discrete GPU
+ - ⚪ **Grey**: Other / Unknown adapter
+ - 🟡 **Yellow**: Display reset in progress or dGPU acquisition warning
+- 🩺 **Live Health Monitoring & eGPU Auto-Recovery**: Tracks discrete adapters by **Locally Unique Identifier (LUID)** and provides event-driven hot-plug detection via `WM_DEVICECHANGE` (`DBT_DEVNODES_CHANGED`) with periodic retry fallback. If an eGPU is disconnected or a driver reset occurs (`GetDeviceRemovedReason()`), the app enters an explicit waiting state without trapping onto integrated graphics, and automatically re-acquires the preferred discrete GPU as soon as hardware re-initialization completes.
+- 💤 **Non-Blocking Power Management**: Flushes and releases the DirectX context cleanly before sleep (`PBT_APMSUSPEND`), and re-acquires automatically with a brief delay upon system resume without freezing the message loop.
+- 🔄 **Verified Display Adapter Restart**: Safely cycles PCI display adapters using the Windows Configuration Manager API (`CfgMgr32`), skips known virtual/software devices, deterministically verifies driver initialization via `CM_Get_DevNode_Status` (`DN_STARTED`), and surfaces partial or failed reset results.
+- 🖥️ **CLI Controls**: Supports `--exit` / `--quit` for graceful shutdown from scripts or terminals, and `--help` for usage information.
+- 🔔 **Single-Instance Aware**: Launching a duplicate instance highlights and displays the active GPU status balloon from the existing tray process.
+- 🔍 **High-DPI & Per-User Logging**: Full Per-Monitor V2 DPI awareness and rolling diagnostic logs stored safely in `%LOCALAPPDATA%\GPU-Switcher\gpu_switcher.log`.
---
-## Requirements
+## Download & Installation
-- Windows 10 or 11 (64‑bit)
-- Designed and tested for NVIDIA Advanced Optimus capable laptops, but this should work for any hybrid system with a MUX switch supporting on-the-fly dGPU display switching
-- D3D11‑capable discrete GPU
+GPU-Switcher is a standalone portable application:
+1. Download the latest `GPU-Switcher-*-windows-x64.zip` from the **[Releases](https://github.com/Tiflit/GPU-Switcher/releases)** page.
+2. Extract `GPU-Switcher.exe` to a permanent location (e.g., `C:\Program Files\GPU-Switcher` or your tools directory).
+3. Run `GPU-Switcher.exe`. It will appear in your notification area (system tray).
---
-## One‑time setup
-
-1. Run `GPU-Switcher.exe`
-2. Open **NVIDIA Control Panel**
-3. Go to **Manage 3D settings → Program Settings**
+## One-Time GPU Preference Setup
-4. Add `GPU-Switcher.exe`
-5. Set **Preferred graphics processor → High‑performance NVIDIA processor**
-6. Enable **Automatic display switching**
-7. Exit and relaunch `GPU-Switcher.exe`
+Starting with Windows 10 (version 20H1+) and Windows 11, the operating system's per-application **Graphics Settings** can override driver-level preferences. To ensure your discrete GPU is consistently selected:
-After this, GPU‑Switcher will consistently trigger a dGPU display switch on launch. Right-click on the tray icon to access extra options.
+### Step 1: Windows Graphics Settings (Recommended)
+1. Open Windows **Settings → System → Display → Graphics** (or search "Graphics Settings" in the Start menu).
+2. Under **Custom options for apps**, click **Browse** and select `GPU-Switcher.exe`.
+3. Click **Options** on the newly added entry and select **High performance** (identifying your discrete GPU, e.g. NVIDIA / AMD / Intel Arc).
+4. Click **Save**.
----
+### Step 2: NVIDIA Control Panel (for Advanced Optimus laptops)
+For NVIDIA Advanced Optimus laptops to trigger dynamic internal display switching on launch:
+1. Open **NVIDIA Control Panel**.
+2. Navigate to **Manage 3D settings → Program Settings**.
+3. Click **Add** and select `GPU-Switcher.exe`.
+4. Set **Preferred graphics processor** to **High-performance NVIDIA processor**.
+5. Ensure **Automatic display switching** is enabled in your global display mode settings.
+6. Right-click the tray icon and select **Exit**, then relaunch `GPU-Switcher.exe`.
-## Tray icon
+*After this one-time configuration, launching GPU-Switcher will consistently trigger discrete GPU pinning and Advanced Optimus display switching.*
-- **Dynamic status icon**:
- - 🟢 **Green**: Active NVIDIA discrete GPU
- - 🔴 **Red**: Active AMD discrete GPU
- - 🔵 **Blue**: Active Intel discrete GPU
- - ⚪ **Grey**: Other / Unknown adapter
- - 🟡 **Yellow**: Reset in progress or dGPU acquisition warning
+---
-- **Left‑click**: Displays active GPU model and health status balloon.
-- **Hover**: Shows current adapter name and status in the tooltip.
+## Tray Controls
-- **Right‑click for options**:
- - **Start with Windows** — toggle a `HKCU\...\Run` registry entry
- - **Restart Display Adapters** — safely cycles all physical display adapters and automatically re-launches GPU-Switcher → requires UAC
- - **Exit** — cleanly releases the GPU context and removes the tray icon
+- **Hover**: Displays the active GPU description in the tooltip (e.g. `GPU-Switcher: NVIDIA GeForce RTX 4060 Laptop GPU`).
+- **Left‑click**: Displays a status balloon with the active GPU name and health state.
+- **Right‑click Context Menu**:
+ - **Start with Windows**: Toggles a startup entry in `HKCU\Software\Microsoft\Windows\CurrentVersion\Run`.
+ - **Restart Display Adapters**: Safely cycles physical display adapters via elevated helper to fix rendering stutters, then automatically restarts the utility (requires UAC confirmation).
+ - **Exit**: Cleanly releases the DirectX context and terminates the program.
---
-## Command‑line options
+## Command‑Line Options
| Option | Description |
|---|---|
-| *(none)* | Launches the tray utility. If an instance is already running, highlights the active instance in the tray. |
+| *(none)* | Launches the tray utility. If already running, activates and notifies the existing instance. |
| `--exit` / `--quit` | Gracefully closes the running GPU-Switcher instance. |
-| `--help` / `-h` | Shows usage instructions and available flags. |
+| `--help` / `-h` / `/?` | Displays the help dialog with available options. |
+| `--reset-gpu` | Helper flag used internally to perform the elevated display adapter reset. |
---
-## How it works
+## How It Works
-On startup, GPU‑Switcher:
-
-1. **Exports GPU driver hints** (`NvOptimusEnablement`, `AmdPowerXpressRequestHighPerformance`)
-2. **Creates a persistent D3D11 device** on the adapter with the most dedicated VRAM
-3. **Sits in the system tray** with zero CPU usage.
-
-Exiting releases the D3D device and the driver returns to normal routing.
+1. **Driver Enablement Hints**: Exports documented vendor activation symbols:
+ - `NvOptimusEnablement = 1` (NVIDIA Optimus rendering hint)
+ - `AmdPowerXpressRequestHighPerformance = 1` (AMD PowerXpress hint)
+2. **Adapter Classification & LUID Tracking**: Enumerates all DXGI adapters, removes software renderers (`DXGI_ADAPTER_FLAG_SOFTWARE`), and classifies discrete candidates (NVIDIA, AMD dGPU, Intel Arc) separate from integrated APUs. Remembers the preferred adapter via its Windows **Locally Unique Identifier (LUID)** so transient disconnects do not trap the system into integrated graphics.
+3. **Direct3D 11 Pinning**: Creates a persistent `ID3D11Device` on the selected discrete adapter with `D3D11_CREATE_DEVICE_BGRA_SUPPORT`. This signals the graphics driver that a high-performance 3D process is active, preventing the GPU from entering deep sleep and keeping Advanced Optimus routed to the discrete GPU.
+4. **State-Driven, Lightweight Lifecycle**: An explicit state machine (`Active`, `WaitingForDiscreteGpu`, `DeviceLost`, `Resetting`) handles adapter loss, eGPU hot-plugging via `WM_DEVICECHANGE`, system sleep/resume (`WM_POWERBROADCAST`), and taskbar recreation with negligible resident CPU impact.
+5. **Deterministic Driver Reset**: When display restart is requested, an elevated helper cycles PCI display adapters via CfgMgr32, poll-verifies driver initialization with `CM_Get_DevNode_Status` (`DN_STARTED`, problem code 0), and propagates verified status back to the parent.
---
## Compatibility
-| System | Behaviour |
-|--------|-----------|
-| NVIDIA Advanced Optimus | Everything should work |
-| Standard Optimus (no MUX) | dGPU will activate but display switch is not available |
-| AMD hybrid | Driver hint is sent; behaviour may vary by OEM |
-
----
-
-## Building
-
-Requires CMake 3.20+ and Visual Studio / MSVC with Windows SDK.
-
-```
-cmake -B build
-cmake --build build --config Release
-```
+| System Type | Behavior |
+|---|---|
+| **NVIDIA Advanced Optimus** | Full dynamic display switching and dGPU activation. |
+| **Standard Optimus (Muxless)** | Activates dGPU; internal display routing remains on iGPU (by hardware design). |
+| **AMD Hybrid / SmartAccess** | Driver hints exported; display switching depends on OEM MUX architecture. |
---
-## Known behaviour and limitations
-
-**Display switching requires a one‑time NVCP profile**
-GPU‑Switcher does not switch the display on its own. It registers with the NVIDIA driver, which then triggers switching based on your NVCP profile. Without the profile configured, the app has no effect on display routing.
+## Building from Source
-**The display switch happens at process load, not at runtime**
-The driver hints (`NvOptimusEnablement`) are read once when the process starts. There is no way to trigger or cancel the switch after launch without restarting the app.
+Requires CMake 3.20+ and Visual Studio 2019/2022 (MSVC) with the Windows 10/11 SDK.
-**Standard Optimus (no MUX switch) cannot switch the display**
-On most non-Advanced-Optimus laptops, the iGPU is hardwired to the display panel. GPU‑Switcher will still register the dGPU with the driver and make it visible in NVCP, but no display switch will occur regardless of settings.
+```powershell
+# Clone the repository
+git clone https://github.com/Tiflit/GPU-Switcher.git
+cd GPU-Switcher
-**The tray icon reflects the acquired dGPU adapter and vendor**
-The tray icon reflects whether a discrete GPU is successfully acquired and running (with vendor color coding) and warns if the device is lost or removed. Note that detecting internal MUX display panel routing via DXGI is not supported by Optimus architecture, so the icon reflects the acquired DirectX device state rather than display panel muxing.
+# Configure with CMake
+cmake -B build -G "Visual Studio 17 2022" -A x64
-**Rendering stutters after a display switch**
-On some systems, switching display output between GPUs leaves the driver in a partially initialized state and causes rendering stutters. Use **Restart Display Adapters** from the tray menu to resolve this without rebooting.
-
-**"Restart Display Adapters" automatically re-launches the app**
-Re-triggering display switching and refreshing adapter state requires cycling the physical graphics devices and reloading. GPU-Switcher safely performs this cycle in an elevated helper, filters out virtual adapters, waits for driver initialization, and automatically re-launches the user tray instance when completed.
+# Build Release binary
+cmake --build build --config Release
+```
-**Sleep and wake**
-The app handles sleep/wake cycles by releasing and re‑acquiring the D3D device on resume. This re‑registers the process with the driver. Whether the display switch re‑triggers on resume depends on your system's driver behaviour and is not guaranteed.
+The compiled binary will be located at `build/Release/GPU-Switcher.exe` (or `build/GPU-Switcher.exe` if using Ninja).
-**AMD hybrid systems**
-The `AmdPowerXpressRequestHighPerformance` hint is exported and will be read by AMD drivers, but display switching behaviour on AMD hybrid systems has not been tested and may vary by OEM and driver versions.
+---
-**UAC prompt required**
-The app runs almost entirely in user space. The only exception is the Restart Display Adapters feature, which performs a full graphics driver reload. Restarting display adapters requires elevated privileges, so Windows will show a UAC prompt when this option is selected.
+## Troubleshooting & Diagnostics
-**Logging**
-A rolling log file (`%LOCALAPPDATA%\GPU-Switcher\gpu_switcher.log`, capped at 16 KB) records adapter detection, reset operations, and any errors.
+- **Diagnostic Logs**: A rolling log file is written to `%LOCALAPPDATA%\GPU-Switcher\gpu_switcher.log` (capped at 16 KB with automatic rotation). Check this file if a device fails to acquire or to inspect detected adapter scores.
+- **Display Stuttering**: If your laptop panel stutters after switching display modes, use **Restart Display Adapters** from the tray menu to reload the graphics driver stack without rebooting Windows.
+- **Display Not Switching**: Ensure you have configured the program profile in Windows Graphics Settings and NVIDIA Control Panel as described in the [One-Time GPU Preference Setup](#one-time-gpu-preference-setup).
---
## License
-MIT — do whatever you want, just link back to the original.
+This project is licensed under the [MIT License](LICENSE) — Copyright © 2026 Tiflit.
diff --git a/src/app.manifest b/src/app.manifest
index f0ba885..b86cf2b 100644
--- a/src/app.manifest
+++ b/src/app.manifest
@@ -1,7 +1,7 @@
#include
#include
+#include
#include "resource.h"
#include "util/logging.h"
@@ -13,7 +14,7 @@
#pragma comment(lib, "dxgi.lib")
#pragma comment(lib, "d3d11.lib")
-#define APP_VERSION L"1.1.0"
+#define APP_VERSION L"1.2.0"
extern "C" {
__declspec(dllexport) DWORD NvOptimusEnablement = 1;
@@ -33,6 +34,39 @@ extern "C" {
using Microsoft::WRL::ComPtr;
+enum class AppState
+{
+ NoDiscreteGpu, // No discrete GPU candidate detected
+ Acquiring, // Creating D3D11 device
+ Active, // Holding D3D11 device on discrete GPU
+ DeviceLost, // D3D11 device was removed or reset
+ WaitingForDiscreteGpu, // Preferred or discrete GPU is absent (e.g. eGPU disconnected)
+ Resetting // Elevated display adapter reset in progress
+};
+
+static const wchar_t* AppStateToString(AppState state)
+{
+ switch (state)
+ {
+ case AppState::NoDiscreteGpu: return L"NoDiscreteGpu";
+ case AppState::Acquiring: return L"Acquiring";
+ case AppState::Active: return L"Active";
+ case AppState::DeviceLost: return L"DeviceLost";
+ case AppState::WaitingForDiscreteGpu: return L"WaitingForDiscreteGpu";
+ case AppState::Resetting: return L"Resetting";
+ default: return L"Unknown";
+ }
+}
+
+struct GpuCandidate
+{
+ ComPtr adapter;
+ DXGI_ADAPTER_DESC1 desc = {};
+ LUID luid = {};
+ bool isDiscrete = false;
+ int score = 0;
+};
+
static HINSTANCE g_hInst = nullptr;
static HANDLE g_hMutex = nullptr;
static ComPtr g_device;
@@ -40,10 +74,113 @@ static ComPtr g_context;
static NOTIFYICONDATAW g_nid = {};
static UINT g_taskbarCreatedMsg = 0;
+static AppState g_appState = AppState::NoDiscreteGpu;
static UINT g_activeVendorId = 0;
static std::wstring g_activeGpuName;
-static bool g_dGpuActive = false;
-static bool g_resetInProgress = false;
+static LUID g_activeLuid = { 0, 0 };
+static LUID g_preferredLuid = { 0, 0 };
+static std::wstring g_preferredGpuName;
+
+static bool HasPreferredLuid()
+{
+ return (g_preferredLuid.LowPart != 0 || g_preferredLuid.HighPart != 0);
+}
+
+static void SetAppState(AppState newState)
+{
+ if (g_appState != newState)
+ {
+ wchar_t buf[128];
+ swprintf_s(buf, L"State transition: %s -> %s", AppStateToString(g_appState), AppStateToString(newState));
+ LogInfo(buf);
+ g_appState = newState;
+ }
+}
+
+static bool IsAdapterPresent(const LUID& targetLuid)
+{
+ ComPtr factory;
+ if (FAILED(CreateDXGIFactory1(__uuidof(IDXGIFactory1), reinterpret_cast(factory.GetAddressOf()))))
+ return false;
+
+ ComPtr adapter;
+ for (UINT i = 0; factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i)
+ {
+ DXGI_ADAPTER_DESC1 desc;
+ if (SUCCEEDED(adapter->GetDesc1(&desc)))
+ {
+ if (desc.AdapterLuid.LowPart == targetLuid.LowPart &&
+ desc.AdapterLuid.HighPart == targetLuid.HighPart)
+ {
+ return true;
+ }
+ }
+ adapter.Reset();
+ }
+ return false;
+}
+
+static bool EvaluateAdapter(IDXGIAdapter1* pAdapter, GpuCandidate& info)
+{
+ if (FAILED(pAdapter->GetDesc1(&info.desc)))
+ return false;
+
+ if (info.desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
+ return false;
+
+ info.adapter = pAdapter;
+ info.luid = info.desc.AdapterLuid;
+
+ // Distinguish discrete vs integrated:
+ // - 0x10DE (NVIDIA): Dedicated discrete GPU (Optimus dGPU or eGPU)
+ // - 0x1002 (AMD): Discrete if dedicated VRAM > 512 MB (filters out integrated APU carveouts)
+ // - 0x8086 (Intel): Discrete (Arc) if dedicated VRAM > 1024 MB (integrated Iris Xe/UHD typically 0-128 MB)
+ // - Other: Discrete if dedicated VRAM exceeds shared system memory
+ if (info.desc.VendorId == 0x10DE)
+ {
+ info.isDiscrete = true;
+ }
+ else if (info.desc.VendorId == 0x1002)
+ {
+ info.isDiscrete = (info.desc.DedicatedVideoMemory > 512 * 1024 * 1024);
+ }
+ else if (info.desc.VendorId == 0x8086)
+ {
+ info.isDiscrete = (info.desc.DedicatedVideoMemory > 1024 * 1024 * 1024);
+ }
+ else
+ {
+ info.isDiscrete = (info.desc.DedicatedVideoMemory > info.desc.SharedSystemMemory);
+ }
+
+ // Scoring:
+ // Base tier for discrete: NVIDIA: 300, AMD: 200, Intel: 100, Other: 50
+ // Base tier for integrated: AMD: 20, Intel: 10, Other: 1
+ if (info.isDiscrete)
+ {
+ if (info.desc.VendorId == 0x10DE)
+ info.score = 300;
+ else if (info.desc.VendorId == 0x1002)
+ info.score = 200;
+ else if (info.desc.VendorId == 0x8086)
+ info.score = 100;
+ else
+ info.score = 50;
+ }
+ else
+ {
+ if (info.desc.VendorId == 0x1002)
+ info.score = 20;
+ else if (info.desc.VendorId == 0x8086)
+ info.score = 10;
+ else
+ info.score = 1;
+ }
+
+ // Add VRAM in 64MB increments to score
+ info.score += static_cast(info.desc.DedicatedVideoMemory / (1024 * 1024 * 64));
+ return true;
+}
static void ReleaseDGpu()
{
@@ -54,7 +191,13 @@ static void ReleaseDGpu()
g_context.Reset();
}
g_device.Reset();
- g_dGpuActive = false;
+ g_activeVendorId = 0;
+ g_activeGpuName.clear();
+ g_activeLuid = { 0, 0 };
+ if (g_appState == AppState::Active)
+ {
+ SetAppState(AppState::WaitingForDiscreteGpu);
+ }
}
static bool AcquireDGpu()
@@ -68,57 +211,87 @@ static bool AcquireDGpu()
return false;
}
- ComPtr bestAdapter;
- DXGI_ADAPTER_DESC1 bestDesc = {};
- int bestScore = -1;
+ std::vector discreteCandidates;
ComPtr adapter;
for (UINT i = 0; factory->EnumAdapters1(i, &adapter) != DXGI_ERROR_NOT_FOUND; ++i)
{
- DXGI_ADAPTER_DESC1 desc;
- if (SUCCEEDED(adapter->GetDesc1(&desc)))
+ GpuCandidate info;
+ if (EvaluateAdapter(adapter.Get(), info))
{
- if (desc.Flags & DXGI_ADAPTER_FLAG_SOFTWARE)
- {
- adapter.Reset();
- continue;
- }
-
- int score = 0;
- // Prioritize dedicated discrete vendors targeting Optimus / PowerXpress
- if (desc.VendorId == 0x10DE) // NVIDIA
- score = 100;
- else if (desc.VendorId == 0x1002) // AMD
- score = 50;
- else if (desc.VendorId == 0x8086) // Intel
- score = 10;
-
- // Add score proportional to dedicated VRAM (64 MB increments)
- score += static_cast(desc.DedicatedVideoMemory / (1024 * 1024 * 64));
-
wchar_t logBuf[256];
- swprintf_s(logBuf, L"Detected GPU [%u]: %s (Vendor: 0x%04X, Dedicated VRAM: %llu MB)",
- i, desc.Description, desc.VendorId,
- static_cast(desc.DedicatedVideoMemory / (1024 * 1024)));
+ swprintf_s(logBuf, L"Detected GPU [%u]: %s (Vendor: 0x%04X, Dedicated VRAM: %llu MB, Discrete: %s, Score: %d)",
+ i, info.desc.Description, info.desc.VendorId,
+ static_cast(info.desc.DedicatedVideoMemory / (1024 * 1024)),
+ info.isDiscrete ? L"yes" : L"no", info.score);
LogInfo(logBuf);
- if (score > bestScore)
+ if (info.isDiscrete)
{
- bestScore = score;
- bestDesc = desc;
- bestAdapter = adapter;
+ discreteCandidates.push_back(info);
}
}
adapter.Reset();
}
- if (!bestAdapter)
+ if (discreteCandidates.empty())
{
- LogError(L"No suitable graphics adapter found");
+ if (HasPreferredLuid())
+ {
+ SetAppState(AppState::WaitingForDiscreteGpu);
+ LogInfo(L"Preferred discrete GPU is not present — waiting for reconnection");
+ }
+ else
+ {
+ SetAppState(AppState::NoDiscreteGpu);
+ LogInfo(L"No discrete GPU detected on the system");
+ }
return false;
}
- LogInfo(std::wstring(L"Selected target adapter: ") + bestDesc.Description);
+ // Select target adapter:
+ // If a preferred LUID was previously tracked, look for an exact match among discrete candidates
+ GpuCandidate best;
+ bool foundPreferred = false;
+ if (HasPreferredLuid())
+ {
+ for (const auto& cand : discreteCandidates)
+ {
+ if (cand.luid.LowPart == g_preferredLuid.LowPart &&
+ cand.luid.HighPart == g_preferredLuid.HighPart)
+ {
+ best = cand;
+ foundPreferred = true;
+ LogInfo(std::wstring(L"Matched preferred discrete GPU: ") + best.desc.Description);
+ break;
+ }
+ }
+ }
+
+ if (!foundPreferred)
+ {
+ // Pick the highest scoring discrete candidate (either initial selection, or temporary fallback)
+ int bestScore = -1;
+ for (const auto& cand : discreteCandidates)
+ {
+ if (cand.score > bestScore)
+ {
+ bestScore = cand.score;
+ best = cand;
+ }
+ }
+
+ if (HasPreferredLuid())
+ {
+ LogInfo(std::wstring(L"Preferred GPU not found — acquiring fallback discrete GPU: ") + best.desc.Description);
+ }
+ else
+ {
+ LogInfo(std::wstring(L"Selected initial preferred discrete GPU: ") + best.desc.Description);
+ }
+ }
+
+ SetAppState(AppState::Acquiring);
D3D_FEATURE_LEVEL level = {};
D3D_FEATURE_LEVEL featureLevels[] = {
@@ -129,7 +302,7 @@ static bool AcquireDGpu()
};
HRESULT hr = D3D11CreateDevice(
- bestAdapter.Get(), D3D_DRIVER_TYPE_UNKNOWN,
+ best.adapter.Get(), D3D_DRIVER_TYPE_UNKNOWN,
nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
featureLevels, static_cast(ARRAYSIZE(featureLevels)),
D3D11_SDK_VERSION,
@@ -140,17 +313,28 @@ static bool AcquireDGpu()
wchar_t buf[64];
swprintf_s(buf, L"D3D11CreateDevice failed: 0x%08X", hr);
LogError(buf);
+ SetAppState(AppState::DeviceLost);
return false;
}
if (level < D3D_FEATURE_LEVEL_11_0)
LogError(L"Warning: acquired adapter feature level is below D3D_FEATURE_LEVEL_11_0");
- g_activeVendorId = bestDesc.VendorId;
- g_activeGpuName = bestDesc.Description;
- g_dGpuActive = true;
+ g_activeVendorId = best.desc.VendorId;
+ g_activeGpuName = best.desc.Description;
+ g_activeLuid = best.luid;
+
+ // Establish preferred LUID only on initial selection to prevent preference drift
+ if (!HasPreferredLuid())
+ {
+ g_preferredLuid = best.luid;
+ g_preferredGpuName = best.desc.Description;
+ LogInfo(std::wstring(L"Established preferred discrete GPU: ") + best.desc.Description);
+ }
+
+ SetAppState(AppState::Active);
- LogInfo(std::wstring(L"Successfully initialized D3D11 device on: ") + bestDesc.Description);
+ LogInfo(std::wstring(L"Successfully initialized D3D11 device on: ") + best.desc.Description);
return true;
}
@@ -192,38 +376,63 @@ static void UpdateTrayStatus(const wchar_t* customTip = nullptr)
if (!g_nid.hWnd)
return;
- g_nid.hIcon = GetVendorIcon(g_dGpuActive && !g_resetInProgress, g_activeVendorId);
+ bool isTrayActive = (g_appState == AppState::Active);
+ g_nid.hIcon = GetVendorIcon(isTrayActive, g_activeVendorId);
if (customTip)
{
wcsncpy_s(g_nid.szTip, customTip, _TRUNCATE);
}
- else if (g_resetInProgress)
- {
- wcsncpy_s(g_nid.szTip, L"GPU-Switcher: Restarting display adapters…", _TRUNCATE);
- }
- else if (g_dGpuActive)
- {
- swprintf_s(g_nid.szTip, L"GPU-Switcher: %s", g_activeGpuName.c_str());
- }
else
{
- wcsncpy_s(g_nid.szTip, L"GPU-Switcher: dGPU inactive", _TRUNCATE);
+ switch (g_appState)
+ {
+ case AppState::Active:
+ if (HasPreferredLuid() && (g_activeLuid.LowPart != g_preferredLuid.LowPart || g_activeLuid.HighPart != g_preferredLuid.HighPart))
+ {
+ swprintf_s(g_nid.szTip, L"GPU-Switcher: %s (Fallback)", g_activeGpuName.c_str());
+ }
+ else
+ {
+ swprintf_s(g_nid.szTip, L"GPU-Switcher: %s (Active)", g_activeGpuName.c_str());
+ }
+ break;
+ case AppState::Resetting:
+ wcsncpy_s(g_nid.szTip, L"GPU-Switcher: Restarting display adapters…", _TRUNCATE);
+ break;
+ case AppState::WaitingForDiscreteGpu:
+ if (!g_preferredGpuName.empty())
+ swprintf_s(g_nid.szTip, L"GPU-Switcher: Waiting for %s", g_preferredGpuName.c_str());
+ else
+ wcsncpy_s(g_nid.szTip, L"GPU-Switcher: Waiting for discrete GPU", _TRUNCATE);
+ break;
+ case AppState::NoDiscreteGpu:
+ wcsncpy_s(g_nid.szTip, L"GPU-Switcher: No discrete GPU detected", _TRUNCATE);
+ break;
+ case AppState::DeviceLost:
+ wcsncpy_s(g_nid.szTip, L"GPU-Switcher: GPU device lost — recovering", _TRUNCATE);
+ break;
+ case AppState::Acquiring:
+ wcsncpy_s(g_nid.szTip, L"GPU-Switcher: Acquiring graphics adapter…", _TRUNCATE);
+ break;
+ }
}
g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP;
Shell_NotifyIconW(NIM_MODIFY, &g_nid);
}
-// Thread proc: waits for the elevated child to exit, then posts WM_RESET_DONE
+// Thread proc: waits for the elevated child to exit, then posts WM_RESET_DONE with child's exit code
struct WaitCtx { HANDLE hProcess; HWND hwnd; };
static DWORD WINAPI WaitForResetThread(LPVOID param)
{
auto* ctx = reinterpret_cast(param);
WaitForSingleObject(ctx->hProcess, INFINITE);
+ DWORD exitCode = static_cast(ResetResult::EnableFailed);
+ GetExitCodeProcess(ctx->hProcess, &exitCode);
CloseHandle(ctx->hProcess);
- PostMessageW(ctx->hwnd, WM_RESET_DONE, 0, 0);
+ PostMessageW(ctx->hwnd, WM_RESET_DONE, static_cast(exitCode), 0);
delete ctx;
return 0;
}
@@ -249,7 +458,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
if (event == WM_LBUTTONUP || event == WM_LBUTTONDBLCLK ||
event == NIN_SELECT || event == NIN_KEYSELECT)
{
- if (g_device)
+ if (g_appState == AppState::Active && g_device)
{
HRESULT hr = g_device->GetDeviceRemovedReason();
if (hr != S_OK)
@@ -257,6 +466,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
wchar_t buf[128];
swprintf_s(buf, L"D3D11 device lost (0x%08X) — re-acquiring", hr);
LogError(buf);
+ SetAppState(AppState::DeviceLost);
AcquireDGpu();
UpdateTrayStatus();
}
@@ -264,20 +474,42 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
NOTIFYICONDATAW balloon = g_nid;
balloon.uFlags |= NIF_INFO;
- balloon.dwInfoFlags = (g_dGpuActive ? NIIF_INFO : NIIF_WARNING) | NIIF_NOSOUND;
+ balloon.dwInfoFlags = (g_appState == AppState::Active ? NIIF_INFO : NIIF_WARNING) | NIIF_NOSOUND;
wcsncpy_s(balloon.szInfoTitle, L"GPU-Switcher", _TRUNCATE);
- if (g_resetInProgress)
+
+ switch (g_appState)
{
+ case AppState::Active:
+ if (HasPreferredLuid() && (g_activeLuid.LowPart != g_preferredLuid.LowPart || g_activeLuid.HighPart != g_preferredLuid.HighPart))
+ {
+ swprintf_s(balloon.szInfo, L"Active discrete GPU (Fallback):\n%s\n\nPreferred GPU:\n%s",
+ g_activeGpuName.c_str(), g_preferredGpuName.c_str());
+ }
+ else
+ {
+ swprintf_s(balloon.szInfo, L"Active discrete GPU:\n%s", g_activeGpuName.c_str());
+ }
+ break;
+ case AppState::Resetting:
wcsncpy_s(balloon.szInfo, L"Display adapter restart in progress…", _TRUNCATE);
+ break;
+ case AppState::WaitingForDiscreteGpu:
+ if (!g_preferredGpuName.empty())
+ swprintf_s(balloon.szInfo, L"Discrete GPU (%s) is disconnected or absent.\nWaiting for reconnection.", g_preferredGpuName.c_str());
+ else
+ wcsncpy_s(balloon.szInfo, L"Discrete GPU is absent.\nWaiting for reconnection.", _TRUNCATE);
+ break;
+ case AppState::NoDiscreteGpu:
+ wcsncpy_s(balloon.szInfo, L"No discrete GPU detected on this system.\nRunning on integrated graphics.", _TRUNCATE);
+ break;
+ case AppState::DeviceLost:
+ wcsncpy_s(balloon.szInfo, L"Discrete GPU device was lost or removed.\nAttempting recovery...", _TRUNCATE);
+ break;
+ case AppState::Acquiring:
+ wcsncpy_s(balloon.szInfo, L"Acquiring discrete GPU...", _TRUNCATE);
+ break;
}
- else if (g_dGpuActive)
- {
- swprintf_s(balloon.szInfo, L"Active discrete GPU:\n%s", g_activeGpuName.c_str());
- }
- else
- {
- wcsncpy_s(balloon.szInfo, L"dGPU acquisition failed — check log", _TRUNCATE);
- }
+
Shell_NotifyIconW(NIM_MODIFY, &balloon);
return 0;
}
@@ -291,7 +523,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
AppendMenuW(menu, MF_STRING | (startup ? MF_CHECKED : 0),
ID_START_WINDOWS, L"Start with Windows");
- AppendMenuW(menu, MF_STRING | (g_resetInProgress ? MF_GRAYED : 0),
+ AppendMenuW(menu, MF_STRING | (g_appState == AppState::Resetting ? MF_GRAYED : 0),
ID_RESET_DISPLAYS, L"Restart Display Adapters");
AppendMenuW(menu, MF_STRING,
@@ -315,10 +547,10 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
case ID_RESET_DISPLAYS:
{
- if (g_resetInProgress)
+ if (g_appState == AppState::Resetting)
break;
- g_resetInProgress = true;
+ SetAppState(AppState::Resetting);
UpdateTrayStatus(L"GPU-Switcher: Restarting display adapters…");
LogInfo(L"Restart Display Adapters requested");
@@ -339,7 +571,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
if (!ShellExecuteExW(&sei) || !sei.hProcess)
{
LogInfo(L"Elevated launch cancelled or failed — reacquiring dGPU");
- g_resetInProgress = false;
+ SetAppState(AppState::DeviceLost);
AcquireDGpu();
UpdateTrayStatus();
break;
@@ -377,30 +609,66 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
case WM_RESET_DONE:
{
- LogInfo(L"Elevated child finished — re-launching GPU-Switcher and exiting");
- Shell_NotifyIconW(NIM_DELETE, &g_nid);
+ DWORD resetExitCode = static_cast(wParam);
+ ResetResult res = static_cast(resetExitCode);
- if (g_hMutex)
+ if (res == ResetResult::Success)
{
- CloseHandle(g_hMutex);
- g_hMutex = nullptr;
- }
+ LogInfo(L"Elevated reset completed successfully — re-launching GPU-Switcher and exiting");
+ Shell_NotifyIconW(NIM_DELETE, &g_nid);
+
+ if (g_hMutex)
+ {
+ CloseHandle(g_hMutex);
+ g_hMutex = nullptr;
+ }
+
+ wchar_t exePath[MAX_PATH] = {};
+ if (GetModuleFileNameW(nullptr, exePath, MAX_PATH) > 0)
+ {
+ HINSTANCE hInstApp = ShellExecuteW(nullptr, L"open", exePath, nullptr, nullptr, SW_SHOWNORMAL);
+ if (reinterpret_cast(hInstApp) <= 32)
+ {
+ LogError(L"Failed to re-launch GPU-Switcher after reset");
+ }
+ else
+ {
+ LogInfo(L"Successfully re-launched GPU-Switcher instance");
+ }
+ }
- wchar_t exePath[MAX_PATH] = {};
- if (GetModuleFileNameW(nullptr, exePath, MAX_PATH) > 0)
+ PostQuitMessage(0);
+ }
+ else
{
- HINSTANCE hInstApp = ShellExecuteW(nullptr, L"open", exePath, nullptr, nullptr, SW_SHOWNORMAL);
- if (reinterpret_cast(hInstApp) <= 32)
+ wchar_t errBuf[256];
+ swprintf_s(errBuf, L"Display adapter reset finished with non-success status: %s (code %u)",
+ ResetResultToString(res), resetExitCode);
+ LogError(errBuf);
+
+ SetAppState(AppState::DeviceLost);
+ AcquireDGpu();
+ UpdateTrayStatus();
+
+ NOTIFYICONDATAW balloon = g_nid;
+ balloon.uFlags |= NIF_INFO;
+ balloon.dwInfoFlags = NIIF_WARNING | NIIF_NOSOUND;
+ wcsncpy_s(balloon.szInfoTitle, L"GPU-Switcher: Reset Incomplete", _TRUNCATE);
+
+ if (res == ResetResult::PartialDisable)
{
- LogError(L"Failed to re-launch GPU-Switcher after reset");
+ wcsncpy_s(balloon.szInfo,
+ L"One or more display adapters could not be disabled. The reset was only partially applied.",
+ _TRUNCATE);
}
else
{
- LogInfo(L"Successfully re-launched GPU-Switcher instance");
+ swprintf_s(balloon.szInfo,
+ L"Adapter reset failed with status: %s (code %u). Check log for details.",
+ ResetResultToString(res), resetExitCode);
}
+ Shell_NotifyIconW(NIM_MODIFY, &balloon);
}
-
- PostQuitMessage(0);
return 0;
}
@@ -409,7 +677,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
LogInfo(L"System entering suspend — releasing dGPU");
ReleaseDGpu();
- UpdateTrayStatus();
+ UpdateTrayStatus(L"GPU-Switcher: Suspended");
}
else if (wParam == PBT_APMRESUMEAUTOMATIC || wParam == PBT_APMRESUMESUSPEND)
{
@@ -420,6 +688,50 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
}
return TRUE;
+ case WM_DEVICECHANGE:
+ // 0x0007 = DBT_DEVNODES_CHANGED
+ if (wParam == 0x0007)
+ {
+ if (g_appState != AppState::Resetting)
+ {
+ LogInfo(L"Hardware devnodes changed — verifying GPU status");
+ if (g_appState == AppState::Active && g_device)
+ {
+ HRESULT hr = g_device->GetDeviceRemovedReason();
+ if (hr != S_OK)
+ {
+ wchar_t buf[128];
+ swprintf_s(buf, L"D3D11 device lost on devnodes change (0x%08X) — re-acquiring", hr);
+ LogError(buf);
+ SetAppState(AppState::DeviceLost);
+ AcquireDGpu();
+ UpdateTrayStatus();
+ }
+ else if (HasPreferredLuid() && (g_activeLuid.LowPart != g_preferredLuid.LowPart || g_activeLuid.HighPart != g_preferredLuid.HighPart))
+ {
+ // Only re-acquire if the preferred discrete GPU has actually reappeared in the topology
+ if (IsAdapterPresent(g_preferredLuid))
+ {
+ LogInfo(L"Preferred discrete GPU re-detected in topology — restoring preference");
+ if (AcquireDGpu())
+ {
+ UpdateTrayStatus();
+ }
+ }
+ }
+ }
+ else
+ {
+ if (AcquireDGpu())
+ {
+ LogInfo(L"Discrete GPU detected and acquired following devnodes notification");
+ UpdateTrayStatus();
+ }
+ }
+ }
+ }
+ return TRUE;
+
case WM_TIMER:
if (wParam == TIMER_RESUME_REACQUIRE)
{
@@ -433,7 +745,7 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
}
else if (wParam == TIMER_HEALTH_CHECK)
{
- if (g_device)
+ if (g_appState == AppState::Active && g_device)
{
HRESULT hr = g_device->GetDeviceRemovedReason();
if (hr != S_OK)
@@ -441,14 +753,30 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
wchar_t buf[128];
swprintf_s(buf, L"D3D11 device removed or lost (0x%08X) — re-acquiring", hr);
LogError(buf);
+ SetAppState(AppState::DeviceLost);
AcquireDGpu();
UpdateTrayStatus();
}
+ else if (HasPreferredLuid() && (g_activeLuid.LowPart != g_preferredLuid.LowPart || g_activeLuid.HighPart != g_preferredLuid.HighPart))
+ {
+ // Periodically test if preferred GPU has re-appeared in the topology
+ if (IsAdapterPresent(g_preferredLuid))
+ {
+ LogInfo(L"Preferred discrete GPU detected in topology during periodic scan — restoring preference");
+ if (AcquireDGpu())
+ {
+ UpdateTrayStatus();
+ }
+ }
+ }
}
- else if (!g_resetInProgress)
+ else if (g_appState == AppState::WaitingForDiscreteGpu ||
+ g_appState == AppState::NoDiscreteGpu ||
+ g_appState == AppState::DeviceLost)
{
if (AcquireDGpu())
{
+ LogInfo(L"Discrete GPU detected and acquired during periodic scan");
UpdateTrayStatus();
}
}
@@ -460,20 +788,42 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
UpdateTrayStatus();
NOTIFYICONDATAW balloon = g_nid;
balloon.uFlags |= NIF_INFO;
- balloon.dwInfoFlags = (g_dGpuActive ? NIIF_INFO : NIIF_WARNING) | NIIF_NOSOUND;
+ balloon.dwInfoFlags = (g_appState == AppState::Active ? NIIF_INFO : NIIF_WARNING) | NIIF_NOSOUND;
wcsncpy_s(balloon.szInfoTitle, L"GPU-Switcher", _TRUNCATE);
- if (g_resetInProgress)
+
+ switch (g_appState)
{
+ case AppState::Active:
+ if (HasPreferredLuid() && (g_activeLuid.LowPart != g_preferredLuid.LowPart || g_activeLuid.HighPart != g_preferredLuid.HighPart))
+ {
+ swprintf_s(balloon.szInfo, L"Already active in system tray.\nGPU (Fallback): %s\n(Preferred: %s)",
+ g_activeGpuName.c_str(), g_preferredGpuName.c_str());
+ }
+ else
+ {
+ swprintf_s(balloon.szInfo, L"Already active in system tray.\nGPU: %s", g_activeGpuName.c_str());
+ }
+ break;
+ case AppState::Resetting:
wcsncpy_s(balloon.szInfo, L"Already running (display reset in progress)", _TRUNCATE);
+ break;
+ case AppState::WaitingForDiscreteGpu:
+ if (!g_preferredGpuName.empty())
+ swprintf_s(balloon.szInfo, L"Already running (waiting for %s)", g_preferredGpuName.c_str());
+ else
+ wcsncpy_s(balloon.szInfo, L"Already running (waiting for discrete GPU)", _TRUNCATE);
+ break;
+ case AppState::NoDiscreteGpu:
+ wcsncpy_s(balloon.szInfo, L"Already running (no discrete GPU detected)", _TRUNCATE);
+ break;
+ case AppState::DeviceLost:
+ wcsncpy_s(balloon.szInfo, L"Already running (discrete GPU lost — recovering)", _TRUNCATE);
+ break;
+ case AppState::Acquiring:
+ wcsncpy_s(balloon.szInfo, L"Already running (acquiring discrete GPU)", _TRUNCATE);
+ break;
}
- else if (g_dGpuActive)
- {
- swprintf_s(balloon.szInfo, L"Already active in system tray.\nGPU: %s", g_activeGpuName.c_str());
- }
- else
- {
- wcsncpy_s(balloon.szInfo, L"Already running (dGPU inactive)", _TRUNCATE);
- }
+
Shell_NotifyIconW(NIM_MODIFY, &balloon);
return 0;
}
@@ -496,9 +846,12 @@ LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
static int RunElevatedReset()
{
LogInfo(L"GPU-Switcher elevated reset started");
- CycleAllDisplayAdapters();
- LogInfo(L"GPU-Switcher elevated reset complete");
- return 0;
+ ResetResult res = CycleAllDisplayAdapters();
+ wchar_t buf[128];
+ swprintf_s(buf, L"GPU-Switcher elevated reset completed with result: %s (%d)",
+ ResetResultToString(res), static_cast(res));
+ LogInfo(buf);
+ return static_cast(res);
}
// lpCmdLine is unused because we parse via GetCommandLineW().
@@ -599,21 +952,18 @@ int WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR, int)
Sleep(200);
if (!AcquireDGpu())
- LogError(L"Failed to acquire dGPU on startup");
+ LogError(L"Failed to acquire discrete GPU on startup");
g_nid.cbSize = sizeof(g_nid);
g_nid.hWnd = hwnd;
g_nid.uID = TRAY_ID;
g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP | NIF_SHOWTIP;
g_nid.uCallbackMessage = WM_TRAY;
- g_nid.hIcon = GetVendorIcon(g_dGpuActive, g_activeVendorId);
+ g_nid.hIcon = GetVendorIcon(g_appState == AppState::Active, g_activeVendorId);
if (!g_nid.hIcon)
g_nid.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
- if (g_dGpuActive)
- swprintf_s(g_nid.szTip, L"GPU-Switcher: %s", g_activeGpuName.c_str());
- else
- wcsncpy_s(g_nid.szTip, L"GPU-Switcher: dGPU inactive", _TRUNCATE);
+ UpdateTrayStatus();
Shell_NotifyIconW(NIM_ADD, &g_nid);
g_nid.uVersion = NOTIFYICON_VERSION_4;
diff --git a/src/resource.rc b/src/resource.rc
index 59c88ba..a9b9e42 100644
--- a/src/resource.rc
+++ b/src/resource.rc
@@ -10,8 +10,8 @@ IDI_ICON_UNKNOWN ICON "icons/ico/unknown-grey.ico"
IDI_ICON_WARNING ICON "icons/ico/warning-yellow.ico"
VS_VERSION_INFO VERSIONINFO
-FILEVERSION 1,1,0,0
-PRODUCTVERSION 1,1,0,0
+FILEVERSION 1,2,0,0
+PRODUCTVERSION 1,2,0,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
@@ -28,12 +28,12 @@ BEGIN
BEGIN
VALUE "CompanyName", "Tiflit"
VALUE "FileDescription", "GPU-Switcher - Discrete GPU Activator"
- VALUE "FileVersion", "1.1.0.0"
+ VALUE "FileVersion", "1.2.0.0"
VALUE "InternalName", "GPU-Switcher"
VALUE "LegalCopyright", "Copyright (C) 2026"
VALUE "OriginalFilename", "GPU-Switcher.exe"
VALUE "ProductName", "GPU-Switcher"
- VALUE "ProductVersion", "1.1.0.0"
+ VALUE "ProductVersion", "1.2.0.0"
END
END
BLOCK "VarFileInfo"
diff --git a/src/util/reset.cpp b/src/util/reset.cpp
index 2cef844..20da726 100644
--- a/src/util/reset.cpp
+++ b/src/util/reset.cpp
@@ -9,6 +9,10 @@
#pragma comment(lib, "cfgmgr32.lib")
+#ifndef DN_STARTED
+#define DN_STARTED 0x00000008
+#endif
+
struct AdapterTarget
{
DEVINST inst = 0;
@@ -20,6 +24,20 @@ struct AdapterTarget
: inst(i), name(n), instanceId(id), disabledSuccessfully(d) {}
};
+const wchar_t* ResetResultToString(ResetResult res)
+{
+ switch (res)
+ {
+ case ResetResult::Success: return L"Success";
+ case ResetResult::EnumerationFailed: return L"EnumerationFailed";
+ case ResetResult::NoAdaptersFound: return L"NoAdaptersFound";
+ case ResetResult::PartialDisable: return L"PartialDisable";
+ case ResetResult::EnableFailed: return L"EnableFailed";
+ case ResetResult::VerifyFailed: return L"VerifyFailed";
+ default: return L"Unknown";
+ }
+}
+
static std::wstring ConfigRetToString(CONFIGRET cr)
{
switch (cr)
@@ -38,7 +56,7 @@ static std::wstring ConfigRetToString(CONFIGRET cr)
}
}
-void CycleAllDisplayAdapters()
+ResetResult CycleAllDisplayAdapters()
{
std::vector adapters;
@@ -51,7 +69,7 @@ void CycleAllDisplayAdapters()
) != CR_SUCCESS || len == 0)
{
LogError(L"CM_Get_Device_ID_List_Size failed");
- return;
+ return ResetResult::EnumerationFailed;
}
// 2. Retrieve multi-string list of device instance IDs
@@ -64,7 +82,7 @@ void CycleAllDisplayAdapters()
) != CR_SUCCESS)
{
LogError(L"CM_Get_Device_ID_List failed");
- return;
+ return ResetResult::EnumerationFailed;
}
// 3. Filter for display class: GUID_DEVCLASS_DISPLAY {4d36e968-e325-11ce-bfc1-08002be10318}
@@ -102,7 +120,7 @@ void CycleAllDisplayAdapters()
}
}
- // Query hardware ID to distinguish physical PCI graphics cards from virtual/software adapters
+ // Query hardware ID to distinguish PCI graphics cards from virtual/software adapters
wchar_t hwId[512] = {};
ULONG hwIdLen = sizeof(hwId);
CM_Get_DevNode_Registry_PropertyW(inst, CM_DRP_HARDWAREID, nullptr, hwId, &hwIdLen, 0);
@@ -112,7 +130,7 @@ void CycleAllDisplayAdapters()
devName, p, hwId[0] ? hwId : L"(none)");
LogInfo(logBuf);
- // Only target physical PCI display devices (e.g. PCI\VEN_10DE, PCI\VEN_1002, PCI\VEN_8086)
+ // Target PCI display adapters (e.g. PCI\VEN_10DE, PCI\VEN_1002, PCI\VEN_8086)
// Skip non-PCI devices (ROOT\, SWD\, USB\) and Microsoft Basic Display Adapter (VEN_1414)
if (_wcsnicmp(p, L"PCI\\", 4) != 0 && _wcsnicmp(hwId, L"PCI\\", 4) != 0)
{
@@ -133,14 +151,15 @@ void CycleAllDisplayAdapters()
if (adapters.empty())
{
LogError(L"No physical PCI display adapters found for reset");
- return;
+ return ResetResult::NoAdaptersFound;
}
LogInfo(std::wstring(L"Beginning reset of ") + std::to_wstring(adapters.size()) + L" display adapter(s):");
for (const auto& a : adapters)
LogInfo(L" - Target: " + a.name + L" (" + a.instanceId + L")");
- // 4. Disable display adapters and verify status
+ // 4. Disable display adapters and track results
+ bool hadDisableFailure = false;
for (auto& a : adapters)
{
LogInfo(L"Disabling: " + a.name + L" (" + a.instanceId + L")");
@@ -152,6 +171,7 @@ void CycleAllDisplayAdapters()
}
else
{
+ hadDisableFailure = true;
LogError(L"Failed to disable " + a.name + L" — " + ConfigRetToString(cr));
}
}
@@ -159,6 +179,7 @@ void CycleAllDisplayAdapters()
Sleep(2000); // Allow driver unload
// 5. Re-enable display adapters with retry logic
+ bool hadEnableFailure = false;
LogInfo(L"Re-enabling display adapters...");
for (auto& a : adapters)
{
@@ -181,14 +202,75 @@ void CycleAllDisplayAdapters()
if (cr == CR_SUCCESS)
{
- LogInfo(L"Successfully re-enabled: " + a.name);
+ LogInfo(L"Successfully requested enable for: " + a.name);
}
else
{
+ hadEnableFailure = true;
LogError(L"CRITICAL: Failed to re-enable " + a.name + L" — " + ConfigRetToString(cr));
}
}
- Sleep(1500); // Allow driver re-initialization
- LogInfo(L"Adapter cycle completed");
+ // 6. Deterministic verification: poll CM_Get_DevNode_Status until DN_STARTED with problem == 0
+ bool hadVerifyFailure = false;
+ LogInfo(L"Verifying device startup status with CM_Get_DevNode_Status...");
+ for (auto& a : adapters)
+ {
+ if (!a.disabledSuccessfully)
+ continue;
+
+ bool started = false;
+ ULONG status = 0;
+ ULONG problem = 0;
+
+ // Poll for up to 3000ms (15 iterations x 200ms) for driver initialization
+ for (int attempt = 0; attempt < 15; ++attempt)
+ {
+ Sleep(200);
+ CONFIGRET cr = CM_Get_DevNode_Status(&status, &problem, a.inst, 0);
+ if (cr == CR_SUCCESS)
+ {
+ if ((status & DN_STARTED) != 0 && problem == 0)
+ {
+ started = true;
+ break;
+ }
+ }
+ }
+
+ if (started)
+ {
+ wchar_t logBuf[256];
+ swprintf_s(logBuf, L"Verified %s started successfully (Status: 0x%08X, Problem: %u)",
+ a.name.c_str(), status, problem);
+ LogInfo(logBuf);
+ }
+ else
+ {
+ hadVerifyFailure = true;
+ wchar_t errBuf[256];
+ swprintf_s(errBuf, L"CRITICAL: Verification failed for %s (Status: 0x%08X, Problem: %u)",
+ a.name.c_str(), status, problem);
+ LogError(errBuf);
+ }
+ }
+
+ if (hadEnableFailure)
+ {
+ LogError(L"Adapter cycle completed with errors: one or more adapters failed to re-enable");
+ return ResetResult::EnableFailed;
+ }
+ if (hadVerifyFailure)
+ {
+ LogError(L"Adapter cycle completed with errors: one or more adapters failed verification");
+ return ResetResult::VerifyFailed;
+ }
+ if (hadDisableFailure)
+ {
+ LogError(L"Adapter cycle completed with warning: one or more adapters could not be disabled; only successfully disabled adapters were cycled and verified");
+ return ResetResult::PartialDisable;
+ }
+
+ LogInfo(L"Adapter cycle completed successfully: all display adapters restarted and verified");
+ return ResetResult::Success;
}
diff --git a/src/util/reset.h b/src/util/reset.h
index cc7b129..76f2510 100644
--- a/src/util/reset.h
+++ b/src/util/reset.h
@@ -1,6 +1,21 @@
#pragma once
-// Performs a full disable/enable cycle on all display adapters.
+enum class ResetResult : int
+{
+ Success = 0,
+ EnumerationFailed = 1,
+ NoAdaptersFound = 2,
+ PartialDisable = 3,
+ EnableFailed = 4,
+ VerifyFailed = 5
+};
+
+// Returns a human-readable string representation of a ResetResult code.
+const wchar_t* ResetResultToString(ResetResult res);
+
+// Performs a full disable/enable cycle on targeted PCI display adapters,
+// skipping known virtual and software display devices.
// Must be called from an elevated process.
-// Blocks until all adapters are re-enabled.
-void CycleAllDisplayAdapters();
+// Verifies via CM_Get_DevNode_Status that successfully cycled adapters are started with no problem code.
+// Blocks until all target adapters are processed and verified.
+ResetResult CycleAllDisplayAdapters();