Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ jobs:
- name: Build
run: cmake --build build --config Release

- name: Test acquisition and reset recovery
run: ctest --test-dir build -C Release --output-on-failure

- name: Upload artifact
uses: actions/upload-artifact@v4.6.2
with:
Expand Down
30 changes: 29 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,21 @@ add_executable(GPU-Switcher WIN32
src/util/logging.cpp
src/util/startup.cpp
src/util/reset.cpp
src/reset/reset_lock.cpp
)

set_target_properties(GPU-Switcher PROPERTIES OUTPUT_NAME "GPU-Switcher")

target_include_directories(GPU-Switcher PRIVATE src)
execute_process(COMMAND git rev-parse --short=12 HEAD
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
OUTPUT_VARIABLE GPU_SWITCHER_BUILD_REVISION
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET)
if(NOT GPU_SWITCHER_BUILD_REVISION)
set(GPU_SWITCHER_BUILD_REVISION "unknown")
endif()
configure_file(src/build_info.h.in generated/build_info.h @ONLY)
target_include_directories(GPU-Switcher PRIVATE src "${CMAKE_CURRENT_BINARY_DIR}/generated")

target_compile_definitions(GPU-Switcher PRIVATE
UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX
Expand All @@ -47,3 +57,21 @@ target_link_libraries(GPU-Switcher PRIVATE
dxgi
cfgmgr32
)

include(CTest)
if(BUILD_TESTING)
add_executable(acquisition_tests tests/acquisition_tests.cpp)
target_include_directories(acquisition_tests PRIVATE src)
add_test(NAME acquisition_regressions COMMAND acquisition_tests)
add_executable(power_gate_tests tests/power_gate_tests.cpp)
target_include_directories(power_gate_tests PRIVATE src)
add_test(NAME power_transition_regressions COMMAND power_gate_tests)
add_executable(reset_policy_tests tests/reset_policy_tests.cpp)
target_include_directories(reset_policy_tests PRIVATE src)
add_test(NAME reset_policy_regressions COMMAND reset_policy_tests)
add_executable(reset_lock_tests tests/reset_lock_tests.cpp src/reset/reset_lock.cpp)
target_include_directories(reset_lock_tests PRIVATE src)
target_compile_definitions(reset_lock_tests PRIVATE UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
add_test(NAME reset_lock_process_exclusion COMMAND reset_lock_tests)
set_tests_properties(reset_lock_process_exclusion PROPERTIES TIMEOUT 15)
endif()
182 changes: 100 additions & 82 deletions README.md

Large diffs are not rendered by default.

132 changes: 132 additions & 0 deletions docs/LEGION-VALIDATION-2026-10-22.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Legion validation — 2026-10-22

## Tested configuration and evidence

- Lenovo Legion Slim 7 16IRH8, Intel Iris Xe integrated graphics and NVIDIA
GeForce RTX 4060 Laptop GPU.
- Executable identified by the supplied logs as version 1.2.0, build
`e1470674e237` (full commit `e1470674e2375df3bc483c99c8b5d813f4ca1c7e`).
- Windows build, graphics driver versions, power source, and external display
connections were not recorded in the report; do not infer them.
- Evidence: user reports and successive rolling-log snapshots named
`gpu_switcher.log`, `gpu_switcher (2).log`, `gpu_switcher (3).log`, and
`gpu_switcher(1).log` through `gpu_switcher(6).log` supplied in the development
conversation. Raw logs are not included here. Times below are copied from the
logs without timezone conversion.

The logs establish D3D11 acquisition, lifecycle transitions, and Configuration
Manager outcomes. Panel routing and observed GPU activity are user observations;
the application does not independently measure the panel MUX route.

## Results

| Scenario | Evidence and result |
| --- | --- |
| Adapter selection | NVIDIA selected; Intel 128 MB adapter excluded from discrete candidates. |
| Duplicate launch | Existing tray instance notified; user saw its status notification. |
| Ordinary exit | Clean exit logged; user observed NVIDIA become inactive. |
| Sleep/resume | Device released and reacquired; tray survived. |
| Cancel UAC | Acquisition recovered after cancellation; panel routing stayed unchanged. |
| Accepted reset, 12:19 | Both adapters enabled on first requests and verified started with problem 0; helper elapsed 7171 ms, then tray relaunched. |
| Accepted reset, 12:33 | Both adapters enabled on first requests and verified started with problem 0; helper elapsed 6875 ms, then tray relaunched. User observed NVIDIA panel routing after relaunch and Intel routing after Exit. |
| Final launch/resume/exit | 12:38:17 process start; 12:38:25 device active; 12:38:31 suspend release; 12:38:42 device active after resume; 12:38:59 clean exit. User reported the complete sequence working. |

## NVIDIA profile clarification

Initially, the new executable activated the NVIDIA GPU without switching the
panel. The user removed and recreated its NVIDIA App program configuration and
set **Automatic Display Switching** to **On**.

The user subsequently clarified that the first ordinary launch after this change
already switched the panel successfully, before the accepted reset test. The
adapter reset must therefore **not** be documented as a prerequisite for applying
the profile. The evidence points to profile configuration as the explanation for
the initially absent switching; it does not establish the driver's internal cause
or a universal remedy for other systems.

## Additional issue found in the logs

In the earlier resume at 12:32:57–12:33:00, a periodic scan acquired the device
before resume notifications were processed. Those notifications released it,
and the resume timer acquired it again. This unnecessary churn did not recur
in the final 12:38 test, but the observed ordering motivates a power-transition
guard: ordinary acquisition is blocked from suspend notification until the
resume delay completes. Duplicate resume notifications share one delay.

That guard is a **later change**, not part of the e147067 baseline above.
The subsequent 1893b60 hardware check is recorded below.

## Power-gate retest: build 1893b6083278

The user tested the new artifact without performing a reset afterward. The
additional snapshot `gpu_switcher(5).log` identifies the executable as
`1893b6083278` (full commit `1893b6083278b1b3857bb4b21eb6bfbee813fe6d`).

| Time | Log evidence |
| --- | --- |
| 12:49:37 | New build started. |
| 12:49:45 | NVIDIA D3D11 device active. |
| 12:50:21 | Suspend notification; device released. |
| 12:50:31 | One resume scheduling entry. |
| 12:50:32 | One delayed reacquisition; device active again. |
| 12:50:50 | Release transition and clean exit. |

The user reported successful launch, sleep/resume, and panel return to Intel on
Exit. There is no periodic-scan acquisition before the resume timer in this
cycle. This validates that successful path on this laptop, not every possible
Windows event ordering.

## LockApp and intended reset semantics — user clarification

The user observed `lockapp.exe` using NVIDIA after sleep/resume and ordinary
Exit, while the panel returned to Intel. This happened on both tested builds.
The pre-update reset at 12:41 succeeded in 6953 ms, with both adapters verified
after their first enable requests. Resetting then exiting cleared NVIDIA activity
in the user's test.

The user clarified that this is an expected Windows client behavior when NVIDIA
has driven the display, unrelated to a GPU-Switcher defect. Clearing such clients
was the original purpose of the explicit **Restart Display Adapters** command.
Ordinary Exit releases only GPU-Switcher's own context. No further independent
LockApp investigation is required for this task.

The intended reset sequence is: release GPU-Switcher's device, run the elevated
adapter cycle, verify recovery, then exit GPU-Switcher **without relaunching**.
The earlier tested builds instead relaunched automatically after successful
reset; those historical observations above remain accurate, but they do not
match the original intended behavior. Automatic relaunch entered the repository
in commit `65f60ab` (before the current development session), and was carried into
this draft. The subsequent correction removes it on verified success.

Partial or failed resets retain supervision/recovery and a visible warning;
UAC cancellation retains the previous reacquisition behavior. Clearing existing
contexts cannot prevent another application from acquiring the GPU later.

## Reset-to-exit retest: build 8fe69f0d61d2

The user tested the corrected artifact and reported it working as intended.
The supplied `gpu_switcher(6).log` identifies build `8fe69f0d61d2` (full commit
`8fe69f0d61d280e8e2ffe7b9f12f32a496172b38`).

| Time | Log evidence |
| --- | --- |
| 13:11:18–13:11:26 | Process started and acquired the NVIDIA D3D11 device. |
| 13:11:35 | Entered Resetting and requested Restart Display Adapters. |
| 13:11:37 | Elevated helper started; both adapters were initially started and enabled. |
| 13:11:41 | Intel recovered after its first enable request, with problem 0. |
| 13:11:44 | NVIDIA recovered after its first enable request, with problem 0; helper reported Success after 6547 ms. |
| 13:11:46 | Parent explicitly logged exit without relaunch, followed by clean exit. |

No subsequent startup appears in this snapshot. Together with the user's
confirmation, this passes the corrected successful reset-to-exit scenario on
this laptop. The log establishes reset recovery and process exit; final display
routing and GPU inactivity remain hardware observations.

## Scope and remaining validation

These results cover successful paths on one laptop and the tested profile. They
do not establish universal compatibility or failure recovery. Timeout UI,
concurrent production reset helpers, cross-account/cross-session elevation,
pre-disabled adapter preservation, permanent enable failure, and real multi-GPU
fallback were not manually exercised. Relevant policy and lock regression tests
run in CI without touching physical devices.
130 changes: 130 additions & 0 deletions docs/VALIDATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# 1.2 validation

## Automated checks

The Windows build workflow compiles the application and runs acquisition, reset
policy, power-transition, and cross-process reset-lock regressions with CTest. The tests use the same ordering and replacement helper
as the application, with simulated resources and creation failures.

```powershell
cmake -B build -G "Visual Studio 17 2022" -A x64
cmake --build build --config Release
ctest --test-dir build -C Release --output-on-failure
```

The acquisition, reset-policy, and power-transition tests are independent of Windows headers:

```sh
g++ -std=c++17 -Wall -Wextra -Werror -pedantic -Isrc tests/acquisition_tests.cpp -o acquisition_tests
./acquisition_tests
g++ -std=c++17 -Wall -Wextra -Werror -pedantic -Isrc tests/reset_policy_tests.cpp -o reset_policy_tests
./reset_policy_tests
g++ -std=c++17 -Wall -Wextra -Werror -pedantic -Isrc tests/power_gate_tests.cpp -o power_gate_tests
./power_gate_tests
```

Coverage includes preferred/current/score ordering, equal-score ties, successful
replacement, failed preferred creation retaining the current device, subsequent
candidate attempts when no healthy device exists, all candidates failing, a
healthy device omitted from enumeration, and loss of the current device during
a preferred acquisition attempt. These tests do not exercise DXGI, driver calls,
MUX routing, suspend/resume delivery, or physical adapter resets.

The vendor and memory classification thresholds remain heuristics. Passing the
acquisition tests does not establish that those thresholds classify all GPUs
correctly.

Reset-policy tests cover timeout followed by completion, wait failure retaining
exclusion, initial-state eligibility, refusing to enable untouched devices,
transient enable failure, failed startup verification followed by recovery, and
bounded permanent failure. Windows lock tests launch separate test processes to
check rejection while locked, admission after release, and conservative failure
when the mutex name is occupied by a different object type. No test calls the
physical device-reset APIs. Cross-account UAC and cross-session behavior still
require manual validation.

Power-transition tests cover acquisition exclusion while suspended, the resume
delay, duplicate resume notifications, stale/early timers, another suspend during
a pending wake, and rearming a resume timer cancelled by reset. The gate is used
by the actual acquisition path; the tests do not simulate Windows message delivery.

## Timer and tray failure handling

The health check can complete an elapsed resume delay if the dedicated resume
timer was not delivered or could not be created. Power-transition regressions
cover this delayed fallback, reject early completion, and preserve a subsequent
suspend. Normal resume still uses the 1500 ms timer; fallback may take until the
next 30-second health check.

Startup checks tray registration, callback-version negotiation, and creation of
the health timer before acquiring a GPU. Initialization failure logs an error,
shows an error dialog, and exits. After Explorer restarts, failed tray restoration
releases the held device and blocks acquisition until registration succeeds on a
health check. Reset supervision continues while the icon is unavailable.
These Win32 failure paths were reviewed in source; automated tests do not inject
Shell_NotifyIcon or SetTimer failures. Explorer restart and the new startup order
require a Windows smoke test on the final artifact.

## Recorded hardware baseline

See [Legion results for build e147067](LEGION-VALIDATION-2026-10-22.md). The report separately records the successful 1893b60 power-gate retest and an
expected report of LockApp retaining NVIDIA activity after ordinary Exit. Panel return
to Intel and complete dGPU inactivity are distinct results.

## Legion hardware checks

Use the Windows CI artifact for the exact commit under test. Record the commit,
Windows build, graphics driver versions, power source, and display connections.
Use **View Log** and save the log after each independent test from
`%LOCALAPPDATA%\GPU-Switcher\gpu_switcher.log` before its rolling limit discards it.

1. **Cold launch:** with the documented NVIDIA profile configured, launch the
utility. On the Legion with Intel integrated graphics and an RTX 4060, expect
the NVIDIA icon and model. Check actual panel routing separately in NVIDIA's
controls; the tray icon reports the held device. Verify readable UTF-8 log
output, including non-ASCII characters when present.
2. **Duplicate launch and exit:** a second launch should notify the existing
instance. Exit should remove the icon and release the held device.
3. **Sleep/resume:** test independently of reset. After resume, confirm device
reacquisition, icon state, and actual routing. Save the log.
4. **Cancel reset:** select Restart Display Adapters and cancel UAC. Confirm the
utility reacquires its device and remains usable.
5. **Display reset:** with work saved and graphics workloads closed, request a
reset. Success should verify every successfully cycled target and exit the
utility without launching a replacement. Confirm the icon and process disappear,
then check whether the NVIDIA GPU becomes inactive. Partial/failure status
should keep the tray instance alive and report the incomplete reset. Save the
log; do not infer GPU inactivity from panel routing alone.
6. **Resume followed by reset:** verify from the log that a resume timer does not
create a D3D11 device while the helper is resetting adapters. A timer already
queued when reset starts must also be ignored.

Real multi-dGPU/eGPU fallback validation requires additional hardware. The
automated regressions simulate failed preferred creation; a single-dGPU Legion
cannot validate the complete physical fallback scenario.

7. **Slow helper / reopening:** do not manufacture a hardware hang. The automated
policy tests cover the 60-second warning and eventual completion. If a real
operation runs long, verify that the warning appears once, the tooltip stays
in the waiting state, Exit and another reset remain blocked, and completion
is still handled. If the tray was forcibly terminated and reopened, it should
detect the active helper lock and wait; the external helper's final result is
unknown to that new instance and must not be reported as verified success.
8. **Recovery diagnostics:** check the per-device initial state, disable result,
enable attempts, final locate/status CONFIGRET, problem code, and elapsed time.
Preserve deliberately disabled devices when testing on a suitable setup; do
not disable the Legion's panel-driving adapter merely to exercise this case.

## Remaining limits

The helper can still fail to restore an adapter, or be forcibly terminated. There
is no persistent recovery journal for a terminated helper. Recovery is bounded;
an unresolved device is reported, not guaranteed repaired. Logging across two
processes or different elevation accounts remains a follow-up (View Log opens
only the current account's log).

A default mutex security descriptor is used; lock access/inspection errors block
operations rather than bypassing exclusion. This is conservative handling, not
proof of compatibility across every account/session arrangement. Device startup
verification does not guarantee application-level rendering or display routing
has recovered.
2 changes: 1 addition & 1 deletion src/app.manifest
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity
version="1.1.0.0"
version="1.2.0.0"
processorArchitecture="*"
name="Tiflit.GPUSwitcher"
type="win32"
Expand Down
2 changes: 2 additions & 0 deletions src/build_info.h.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#pragma once
#define GPU_SWITCHER_BUILD_REVISION L"@GPU_SWITCHER_BUILD_REVISION@"
43 changes: 43 additions & 0 deletions src/gpu/acquisition.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#pragma once

#include <utility>

namespace gpu
{
struct CandidatePriority
{
bool preferred;
bool current;
int score;
};

inline bool HigherPriority(const CandidatePriority& left, const CandidatePriority& right)
{
const int leftTier = left.preferred ? 0 : (left.current ? 1 : 2);
const int rightTier = right.preferred ? 0 : (right.current ? 1 : 2);
return leftTier != rightTier ? leftTier < rightTier : left.score > right.score;
}

enum class AcquisitionResult { Replaced, Retained, Failed };

// Creation returns an optional pending resource. Only a successful creation may
// commit it; a healthy current device remains usable throughout failed attempts.
template <typename Candidates, typename IsHealthy, typename IsCurrent, typename Create, typename Commit>
AcquisitionResult AcquireInOrder(const Candidates& candidates, IsHealthy currentHealthy,
IsCurrent isCurrent, Create create, Commit commit)
{
for (const auto& candidate : candidates)
{
if (currentHealthy() && isCurrent(candidate))
return AcquisitionResult::Retained;

auto pending = create(candidate);
if (pending)
{
commit(candidate, std::move(*pending));
return AcquisitionResult::Replaced;
}
}
return currentHealthy() ? AcquisitionResult::Retained : AcquisitionResult::Failed;
}
}
Loading
Loading