Skip to content

Commit 96e61bc

Browse files
Roveyclaude
andcommitted
fix: the tray survives a driver reset; --boot on replaces the old task
Hardware checks 25 and 33 failed on the reference machine: - `--boot on` could not create \GpuAutoOptimizer\BootApply while the pre-P4b task \GpuAutoOptimizer (a file in the task root) existed: it blocks the folder. The old task is now removed first. - On a forced TDR the tray crashed with an access violation inside nvml.dll on its telemetry tick, so the watchdog never re-applied the tune. Periodic hardware work is now guarded; after a reset (UI device removed, a failed read-back, or a fault) NVML and NVAPI are re-created five seconds later and the watchdog re-applies as designed. Also records checks 23, 24 and 26 (pass). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 7f5f063 commit 96e61bc

3 files changed

Lines changed: 96 additions & 22 deletions

File tree

‎docs/hardware-checks.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,17 @@ root; adjust the path for a Debug build.
4646
| 20 | Three strikes stop it | set `"boot_strikes": 3` in `gao.json`, log off and on, `--status`; then `--boot on` | yes | last boot line `not applied: disabled after 3 crashes ...`; after `--boot on`, `strikes: 0 of 3` | Pass (2026-09-26). With `boot_strikes: 3`, log off/on: `last boot: ... not applied: disabled after 3 crashes; run `gao --boot on` to retry`; `--boot on` then showed `strikes: 0 of 3`. Note: the card still carried the profile from the previous logon, because logging off does not reset the driver; in the real case (three crashes) each crash reboots the machine, which does. |
4747
| 21 | A driver change blocks it | set `"driver": "000.00"` in `gao.json`, run `--apply`; restore the value | yes | `not applied: driver changed (000.00 -> ...)` | Pass (2026-09-26). With `"driver": "000.00"`: `--apply` printed `not applied: driver changed (000.00 -> 610.74); run `gao --optimize` again`, exit 1. Value restored afterwards. |
4848
| 22 | Boot-apply off | `--boot off`, then `schtasks /Query /TN GpuAutoOptimizer` | yes | `boot-apply off: task removed`; schtasks reports the task does not exist | Pass (2026-09-26). `--boot off`: `boot-apply off: task removed`; `schtasks /Query /TN GpuAutoOptimizer`: `ERROR: The system cannot find the file specified.`; `--status`: `boot-apply: off`. |
49-
| 23 | Memory search stops at the bandwidth peak | `--optimize best` (elevated) | yes | log shows `mem +N: bandwidth X GB/s` per step and `mem: bandwidth peak +N`; the peak is where bandwidth stops rising, not the top of the range | |
50-
| 24 | Edges are confirmed | same run as 23 | yes | `confirm core +N` and `confirm ... mem +N` lines with 30 s probes; `RESULT:` reports the confirmed values the margin was applied to | |
51-
| 25 | Boot-apply runs the installed copy | `--boot on`, `schtasks /Query /TN \GpuAutoOptimizer\BootApply /XML`, log off and on, `--status` | yes | exe in `%ProgramFiles%\GpuAutoOptimizer\`; XML shows `DisallowStartIfOnBatteries false`, `ExecutionTimeLimit PT5M`, `IgnoreNew`, the Program Files path; `last boot: ... applied`; `boot copy: up to date` | |
52-
| 26 | State folder is admin-only | `icacls "%ProgramData%\GpuAutoOptimizer"`; `--status` unelevated; a user-created folder is refused | no / yes | SYSTEM and Administrators full, Users read & execute, no inherited entries; `--status` works; a folder created by the user before the first elevated run makes `--optimize` refuse | |
49+
| 23 | Memory search stops at the bandwidth peak | `--optimize best` (elevated) | yes | log shows `mem +N: bandwidth X GB/s` per step and `mem: bandwidth peak +N`; the peak is where bandwidth stops rising, not the top of the range | Pass (2026-09-27, driver 610.74): one `bandwidth` line per 50 MHz step. On this card bandwidth kept rising to the top of the range (454.3 GB/s at +0, 513.0 at +1500), so there was no peak inside it. The 1 % tie band picked +1400 (509.3 GB/s), the lowest offset within 1 % of the best. The earlier guess that +1050 was already past the peak was wrong. |
50+
| 24 | Edges are confirmed | same run as 23 | yes | `confirm core +N` and `confirm ... mem +N` lines with 30 s probes; `RESULT:` reports the confirmed values the margin was applied to | Pass (2026-09-27): `confirm core +195 / mem +0` STABLE, `confirm core +135 / mem +1400` STABLE. `RESULT: power 105 %, core +135 MHz (confirmed +195), mem +950 MHz (confirmed +1400)`. Soak STABLE, score 5764 -> 6008 it/s (+4.2 %). |
51+
| 25 | Boot-apply runs the installed copy | `--boot on`, `schtasks /Query /TN \GpuAutoOptimizer\BootApply /XML`, log off and on, `--status` | yes | exe in `%ProgramFiles%\GpuAutoOptimizer\`; XML shows `DisallowStartIfOnBatteries false`, `ExecutionTimeLimit PT0S`, `Priority 5`, `--tray`, `IgnoreNew`, the Program Files path; `last boot: ... applied`; `boot copy: up to date` | Failed (2026-09-27): `could not create the task (schtasks exit 1)`. The old task `\GpuAutoOptimizer` is a file in the task root, and it blocks creating the `\GpuAutoOptimizer\` folder. Fixed: `--boot on` now removes the old task first. Both executables were copied to Program Files correctly. Re-run pending. |
52+
| 26 | State folder is admin-only | `icacls "%ProgramData%\GpuAutoOptimizer"`; `--status` unelevated; a user-created folder is refused | no / yes | SYSTEM and Administrators full, Users read & execute, no inherited entries; `--status` works; a folder created by the user before the first elevated run makes `--optimize` refuse | Pass (2026-09-27): a folder the user created (owner `Gebruiker`) is refused (`not owned by Administrators or SYSTEM`); a junction is refused (`is a link or not a folder`) and its target's ACL is unchanged. The folder gao created: SYSTEM and Administrators `(OI)(CI)(F)`, Users `(OI)(CI)(RX)`, no inherited entries. |
5353
| 27 | No DLL planting | copy `C:\Windows\System32\version.dll` next to the exe as `d3d11.dll`, `dxgi.dll`, `d3dcompiler_47.dll`, `msvcp140.dll`, `vcruntime140.dll` and `vcruntime140_1.dll`; run `--stress 5`, `--bandwidth`, `--status`; delete the copies | no | all three work normally (a loaded fake would break the import) | Pass (2026-09-27, driver 610.74). `--stress 5` `STABLE` (5768 it/s), `--bandwidth` 491.8 GB/s, `--status` normal. `dumpbin /dependents`: static imports only `KERNEL32`, `ADVAPI32`, `SHELL32`, `ole32` (KnownDLLs; the CRT is linked statically); `d3d11`, `dxgi`, `D3DCOMPILER_47` delay-loaded after `SetDefaultDllDirectories`. |
5454
| 28 | Old journal is not imported | first elevated run after the move | yes | the new journal starts empty; nothing under `%LOCALAPPDATA%` is read by the elevated process | Dropped as a migration on 2026-09-27: reading a user-writable file from the elevated process could be redirected to disclose other files (P4b review). The old journal on the reference machine had no open entries (136 lines, 0 freezes), so no ceiling was lost. |
5555
| 29 | The window renders and idles | start `GpuAutoOptimizer.exe` unelevated | no | GPU name, driver, telemetry, saved/applied tune and presets shown; under 1 % of one CPU core while idle | Pass (2026-09-27): all shown (screenshot); 0.05 CPU-seconds over 5 s idle (~1 %); 0.02 after the P5 fix pass. |
5656
| 30 | Optimize from the window | Restart as administrator, pick Best, Optimize | yes | run screen with live temperature/power and the probe log; results screen with before/after | |
5757
| 31 | Abort from the window | Abort during the core search | yes | the run ends with `aborted`, card at stock | |
5858
| 32 | Tray app at logon | apply at logon on, log off and on | yes (task) | tray icon present, tune applied, `boot.log` `applied`, strike cleared after 2 min | |
59-
| 33 | Watchdog re-applies after a TDR | tray running, `dxcap -forcetdr`, wait 45 s, `gao --status` | yes | `applied:` equals the saved tune again; `boot.log` has `watchdog: ... re-applied`; a balloon said so | |
59+
| 33 | Watchdog re-applies after a TDR | tray running, `dxcap -forcetdr`, wait 45 s, `gao --status` | yes | `applied:` equals the saved tune again; `boot.log` has `watchdog: ... re-applied`; a balloon said so | Failed (2026-09-27): the tray crashed on the forced TDR. It hit an access violation inside `nvml.dll`, called from the 1 s telemetry tick (crash dump in the state folder), so nothing re-applied the tune. Fixed: the tray's periodic hardware work is now guarded. After a reset (UI device removed, a failed read-back, or a fault) NVML and NVAPI are re-created 5 s later. A build with an injected access violation recovered and kept running. Re-run pending. |
6060
| 34 | Watchdog backs off from another tool | tray running, set an offset in MSI Afterburner and Apply | no | a balloon says another program changed the settings; the watchdog does not fight it | |
6161
| 35 | One instance | start the app twice | no | the second start brings the first window forward and exits | Pass (2026-09-27, after the P5 fix pass): the second start exits with code 0, the first keeps running. |
6262
| 36 | CLI and tray side by side | tray running with the tune kept applied; `gao --reset`, wait 45 s, `gao --status`; then `gao --optimize` from a shell while the window runs an optimize | yes | after `--reset` the card stays at stock (the tray notes it and does not re-apply); after `gao --apply` the tray keeps the tune applied again; the second optimize refuses with `another optimize is already running` | |

‎src/app/common.cpp‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,17 @@ bool enable_boot(std::string* message) {
232232
const std::string xml = boot_task_xml(std::string(u8.begin(), u8.end()), "--tray", sid, "PT0S");
233233
const auto xml_path = app_dir() / L"BootApply.xml";
234234
if (!write_utf16_file(xml_path, xml)) return say("could not write " + xml_path.string(), false);
235+
// The pre-P4b task is a file named \GpuAutoOptimizer in the task root, which
236+
// blocks creating the \GpuAutoOptimizer\ folder: it has to go first.
237+
const bool had_legacy = boot_task_legacy_exists();
238+
if (had_legacy && boot_task_remove_legacy() != 0) return say("could not remove the old logon task \\GpuAutoOptimizer", false);
235239
const int code = boot_task_create_xml(xml_path);
236240
std::error_code ec;
237241
std::filesystem::remove(xml_path, ec);
238-
if (code != 0) return say("could not create the task (schtasks exit " + std::to_string(code) + ")", false);
239-
boot_task_remove_legacy(); // only once the new task exists
242+
if (code != 0)
243+
return say("could not create the task (schtasks exit " + std::to_string(code) + ")" +
244+
(had_legacy ? "; the old logon task was already removed, so nothing applies the tune at logon" : ""),
245+
false);
240246
Config cfg = load_config();
241247
cfg.boot_strikes = 0;
242248
if (!save_config(cfg)) return say("task created, but could not reset the strike counter", false);

‎src/app/gui/main_gui.cpp‎

Lines changed: 83 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,12 @@ struct App {
6666
ComPtr<ID3D11RenderTargetView> rtv;
6767
bool device_lost = false; // retried every second until a new device exists
6868

69-
gao::Nvml nvml;
70-
gao::Nvapi nvapi;
69+
// Pointers: re-created after a driver reset (see hw_lost()).
70+
std::unique_ptr<gao::Nvml> nvml;
71+
std::unique_ptr<gao::Nvapi> nvapi;
7172
bool nvml_ok = false, nvapi_ok = false;
73+
bool hw_lost = false; // re-create them at hw_retry_at
74+
ULONGLONG hw_retry_at = 0;
7275
gao::GpuControl gpu;
7376

7477
gao::gui::UiState ui;
@@ -246,8 +249,8 @@ void refresh_status(bool with_task) {
246249
g.ui.profile = cfg.profile;
247250
g.ui.strikes = cfg.boot_strikes;
248251
if (g.nvml_ok) {
249-
g.ui.driver = g.nvml.DriverVersion();
250-
const std::string gpu_id = g.nvml.GpuUuid(kGpu);
252+
g.ui.driver = g.nvml->DriverVersion();
253+
const std::string gpu_id = g.nvml->GpuUuid(kGpu);
251254
g.ui.profile_driver_ok = cfg.profile && !g.ui.driver.empty() && g.ui.driver == cfg.profile->driver;
252255
g.ui.profile_gpu_ok = cfg.profile && !gpu_id.empty() && gpu_id == cfg.profile->gpu;
253256
}
@@ -295,12 +298,13 @@ bool refuse_while_tuning() {
295298

296299
void act_apply() {
297300
if (refuse_while_tuning()) return;
301+
if (!g.nvml_ok || !g.nvapi_ok) { g.ui.message = "The NVIDIA driver is not available right now."; return; }
298302
std::string why;
299303
if (!gao::app::prepare_state(&why)) { g.ui.message = why; return; }
300304
gao::Config cfg = gao::app::load_config();
301305
cfg.boot_strikes = 0; // strikes only gate the logon apply
302-
const std::string driver = g.nvml_ok ? g.nvml.DriverVersion() : std::string();
303-
const auto d = gao::decide_boot(cfg, driver, g.nvml_ok ? g.nvml.GpuUuid(kGpu) : std::string());
306+
const std::string driver = g.nvml_ok ? g.nvml->DriverVersion() : std::string();
307+
const auto d = gao::decide_boot(cfg, driver, g.nvml_ok ? g.nvml->GpuUuid(kGpu) : std::string());
304308
if (d != gao::BootDecision::Apply) { g.ui.message = "Not applied: " + gao::app::decision_text(d, cfg, driver); return; }
305309
if (!gao::apply_profile(g.gpu, *cfg.profile, &why)) { g.ui.message = "Not applied: " + why; return; }
306310
g.watch = true;
@@ -311,6 +315,7 @@ void act_apply() {
311315

312316
void act_revert() {
313317
if (refuse_while_tuning()) return;
318+
if (!g.nvml_ok || !g.nvapi_ok) { g.ui.message = "The NVIDIA driver is not available right now."; return; }
314319
const bool ok = g.gpu.reset_to_stock && g.gpu.reset_to_stock();
315320
g.watch = false; // stock by choice: the watchdog must not undo it
316321
g.ui.message = ok ? "Back at stock. The saved tune is not re-applied until you apply it again."
@@ -346,12 +351,72 @@ void render() {
346351
if (hr == DXGI_ERROR_DEVICE_REMOVED || hr == DXGI_ERROR_DEVICE_RESET) recover_device();
347352
}
348353

354+
// ---------------------------------------------------------------- hardware
355+
356+
// A driver reset (TDR) can leave NVML and NVAPI state in this long-running
357+
// process stale, and a call during the reset has faulted inside nvml.dll
358+
// (hardware check 33). The tray exists to survive exactly that: its periodic
359+
// hardware work runs guarded, and after a reset the libraries are re-created.
360+
361+
void init_hw() {
362+
g.nvml = std::make_unique<gao::Nvml>();
363+
g.nvapi = std::make_unique<gao::Nvapi>();
364+
g.nvml_ok = g.nvml->Init();
365+
g.nvapi_ok = g.nvapi->Init();
366+
g.gpu = g.nvml_ok && g.nvapi_ok ? gao::make_gpu_control(*g.nvml, *g.nvapi, kGpu) : gao::GpuControl{};
367+
}
368+
369+
// Structured exceptions, not C++ ones: an access violation inside a driver DLL.
370+
// A function with __try may not hold destructible objects, hence the function
371+
// pointer. Objects in the frames it skips are not destroyed (/EHsc); that leak
372+
// is the price of keeping the watchdog alive.
373+
bool guarded(void (*fn)(void*), void* ctx) {
374+
__try {
375+
fn(ctx);
376+
return true;
377+
} __except (GetExceptionCode() == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH) {
378+
return false;
379+
}
380+
}
381+
template <class F> bool guarded(F f) {
382+
return guarded([](void* p) { (*static_cast<F*>(p))(); }, &f);
383+
}
384+
385+
// Stop using the libraries and re-create them a few seconds from now, when
386+
// the driver is back. The watchdog then finds the tune gone and re-applies it.
387+
void hw_lost() {
388+
g.gpu = {};
389+
g.nvml_ok = g.nvapi_ok = false;
390+
g.hw_lost = true;
391+
g.hw_retry_at = GetTickCount64() + 5000;
392+
// Shutting down a library that just faulted may fault again; then leave it.
393+
gao::Nvml* nvml = g.nvml.release();
394+
gao::Nvapi* nvapi = g.nvapi.release();
395+
guarded([nvml] { delete nvml; });
396+
guarded([nvapi] { delete nvapi; });
397+
}
398+
399+
void retry_hw() {
400+
if (!g.hw_lost || GetTickCount64() < g.hw_retry_at) return;
401+
if (!guarded([] { init_hw(); }) || !g.nvml_ok || !g.nvapi_ok) {
402+
hw_lost(); // not back yet
403+
return;
404+
}
405+
g.hw_lost = false;
406+
refresh_status(false);
407+
}
408+
349409
// ---------------------------------------------------------------- timers
350410

351411
void on_telemetry() {
352-
if (g.device_lost) recover_device();
412+
// A removed UI device is the first sign of a driver reset; the window is
413+
// often hidden, so no Present() would report it.
414+
const bool reset = g.device && g.device->GetDeviceRemovedReason() != S_OK;
415+
if (g.device_lost || reset) recover_device();
416+
if (reset && !g.hw_lost) hw_lost();
417+
retry_hw();
353418
if (g.nvml_ok) {
354-
g.ui.telemetry = g.nvml.Read(kGpu);
419+
g.ui.telemetry = g.nvml->Read(kGpu);
355420
gao::gui::push_history(g.ui.temp_history, static_cast<float>(std::max(g.ui.telemetry.temp_c, 0)));
356421
gao::gui::push_history(g.ui.power_history, static_cast<float>(std::max(g.ui.telemetry.power_w, 0)));
357422
}
@@ -381,8 +446,13 @@ void on_watchdog() {
381446
if (!g.ui.elevated || !g.nvml_ok || !g.nvapi_ok) return;
382447
const gao::Config cfg = gao::app::load_config();
383448
if (!cfg.profile) return;
384-
const std::string driver = g.nvml.DriverVersion(), gpu_id = g.nvml.GpuUuid(kGpu);
385-
const auto action = g.watchdog.check(*cfg.profile, g.gpu.read_applied(), !driver.empty() && driver == cfg.profile->driver,
449+
const auto applied = g.gpu.read_applied();
450+
if (!applied) { // NVAPI handles go stale after a driver reset: re-create
451+
hw_lost();
452+
return;
453+
}
454+
const std::string driver = g.nvml->DriverVersion(), gpu_id = g.nvml->GpuUuid(kGpu);
455+
const auto action = g.watchdog.check(*cfg.profile, applied, !driver.empty() && driver == cfg.profile->driver,
386456
!gpu_id.empty() && gpu_id == cfg.profile->gpu, std::chrono::steady_clock::now());
387457
switch (action) {
388458
case gao::WatchAction::None: return;
@@ -473,8 +543,8 @@ LRESULT CALLBACK wnd_proc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
473543
}
474544
return 0;
475545
case WM_TIMER:
476-
if (wp == kTimerTelemetry) on_telemetry();
477-
else if (wp == kTimerWatchdog) on_watchdog();
546+
if (wp == kTimerTelemetry) { if (!guarded([] { on_telemetry(); })) hw_lost(); }
547+
else if (wp == kTimerWatchdog) { if (!guarded([] { on_watchdog(); })) hw_lost(); }
478548
else if (wp == kTimerStrike) { clear_strike(); refresh_status(false); }
479549
return 0;
480550
case WM_APP_WAKE:
@@ -588,9 +658,7 @@ int WINAPI wWinMain(HINSTANCE inst, HINSTANCE, PWSTR cmdline, int) {
588658
ImGui_ImplDX11_Init(g.device.Get(), g.ctx.Get());
589659

590660
g.worker = std::make_unique<gao::gui::OptimizeWorker>([] { PostMessageW(g.hwnd, WM_APP_WAKE, 0, 0); });
591-
g.nvml_ok = g.nvml.Init();
592-
g.nvapi_ok = g.nvapi.Init();
593-
if (g.nvml_ok && g.nvapi_ok) g.gpu = gao::make_gpu_control(g.nvml, g.nvapi, kGpu);
661+
init_hw();
594662
g.ui.gpu_name = gao::nvidia_adapter_name();
595663

596664
tray_icon(NIM_ADD);

0 commit comments

Comments
 (0)